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
36
.specify/features/006-client-area/checklists/requirements.md
Normal file
36
.specify/features/006-client-area/checklists/requirements.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Specification Quality Checklist: Área do Cliente
|
||||
|
||||
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||
**Created**: 2026-04-13
|
||||
**Feature**: [spec.md](../spec.md)
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] No implementation details (languages, frameworks, APIs)
|
||||
- [x] Focused on user value and business needs
|
||||
- [x] Written for non-technical stakeholders
|
||||
- [x] All mandatory sections completed
|
||||
|
||||
## Requirement Completeness
|
||||
|
||||
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||
- [x] Requirements are testable and unambiguous
|
||||
- [x] Success criteria are measurable
|
||||
- [x] Success criteria are technology-agnostic (no implementation details)
|
||||
- [x] All acceptance scenarios are defined
|
||||
- [x] Edge cases are identified
|
||||
- [x] Scope is clearly bounded
|
||||
- [x] Dependencies and assumptions identified
|
||||
|
||||
## Feature Readiness
|
||||
|
||||
- [x] All functional requirements have clear acceptance criteria
|
||||
- [x] User scenarios cover primary flows
|
||||
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||
- [x] No implementation details leak into specification
|
||||
|
||||
## Notes
|
||||
|
||||
- All items passed on first validation pass.
|
||||
- API Contract section included as supplementary reference (not part of spec template); aligns with Constitution Principle II (separation of concerns, API contract documented before implementation).
|
||||
- Spec is ready for `/speckit.plan`.
|
||||
143
.specify/features/006-client-area/contracts/admin.md
Normal file
143
.specify/features/006-client-area/contracts/admin.md
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
# Contract: Admin Endpoints
|
||||
|
||||
**Blueprint**: `admin_bp` — prefix `/api/v1/admin`
|
||||
**Auth**: JWT Bearer — `require_auth` — MVP: qualquer ClientUser autenticado
|
||||
**Dívida Técnica**: Verificação de role admin adiada para feature pós-MVP (ver Constitution Check no plan.md)
|
||||
|
||||
---
|
||||
|
||||
# POST /api/v1/admin/boletos
|
||||
|
||||
Cria um boleto para um cliente, opcionalmente vinculado a um imóvel.
|
||||
|
||||
## Request
|
||||
|
||||
```
|
||||
POST /api/v1/admin/boletos
|
||||
Authorization: Bearer <jwt_token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||
"property_id": "4gb96g75-6828-5673-c4gd-3d074g77bg33",
|
||||
"description": "Aluguel referente a Maio/2026",
|
||||
"amount": 3500.00,
|
||||
"due_date": "2026-05-10",
|
||||
"url": "https://boleto.banco.com.br/abc123"
|
||||
}
|
||||
```
|
||||
|
||||
Campos obrigatórios: `user_id`, `description`, `amount`, `due_date`.
|
||||
Campos opcionais: `property_id`, `url`.
|
||||
|
||||
---
|
||||
|
||||
## Response 201 Created
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "d5be07h8-9012-6784-d5he-4e185h88ch33",
|
||||
"description": "Aluguel referente a Maio/2026",
|
||||
"amount": "3500.00",
|
||||
"due_date": "2026-05-10",
|
||||
"status": "pending",
|
||||
"url": "https://boleto.banco.com.br/abc123"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Response 404 Not Found
|
||||
|
||||
```json
|
||||
{ "error": "Cliente não encontrado" }
|
||||
```
|
||||
|
||||
Quando `user_id` não corresponde a nenhum `ClientUser` existente.
|
||||
|
||||
---
|
||||
|
||||
## Response 422 Unprocessable Entity
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "Dados inválidos",
|
||||
"details": ["user_id: field required", "amount: field required"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Response 401 Unauthorized
|
||||
|
||||
```json
|
||||
{ "error": "Token inválido ou ausente" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# PUT /api/v1/admin/visits/<id>/status
|
||||
|
||||
Atualiza o status de uma VisitRequest e opcionalmente define a data/hora agendada.
|
||||
|
||||
## Request
|
||||
|
||||
```
|
||||
PUT /api/v1/admin/visits/b3fc85f6-1234-4562-b3fc-2c963f66af11/status
|
||||
Authorization: Bearer <jwt_token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"status": "confirmed",
|
||||
"scheduled_at": "2026-05-01T10:00:00"
|
||||
}
|
||||
```
|
||||
|
||||
`status` obrigatório: `"pending"` | `"confirmed"` | `"cancelled"` | `"completed"`
|
||||
`scheduled_at` opcional: ISO 8601 datetime string.
|
||||
|
||||
---
|
||||
|
||||
## Response 200 OK
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "b3fc85f6-1234-4562-b3fc-2c963f66af11",
|
||||
"property": {
|
||||
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||
"title": "Apartamento 3 quartos Jardins",
|
||||
"slug": "apartamento-3-quartos-jardins"
|
||||
},
|
||||
"message": "Gostaria de visitar no final de semana.",
|
||||
"status": "confirmed",
|
||||
"scheduled_at": "2026-05-01T10:00:00Z",
|
||||
"created_at": "2026-04-13T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Response 404 Not Found
|
||||
|
||||
```json
|
||||
{ "error": "Visita não encontrada" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Response 422 Unprocessable Entity
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "Dados inválidos",
|
||||
"details": ["status: value is not a valid enumeration member"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Response 401 Unauthorized
|
||||
|
||||
```json
|
||||
{ "error": "Token inválido ou ausente" }
|
||||
```
|
||||
52
.specify/features/006-client-area/contracts/me-boletos.md
Normal file
52
.specify/features/006-client-area/contracts/me-boletos.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# Contract: GET /api/v1/me/boletos
|
||||
|
||||
**Blueprint**: `client_bp`
|
||||
**Auth**: JWT Bearer — `require_auth` — ClientUser only
|
||||
|
||||
---
|
||||
|
||||
## Request
|
||||
|
||||
```
|
||||
GET /api/v1/me/boletos
|
||||
Authorization: Bearer <jwt_token>
|
||||
```
|
||||
|
||||
Sem parâmetros de query ou corpo.
|
||||
|
||||
---
|
||||
|
||||
## Response 200 OK
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "d5be07h8-9012-6784-d5he-4e185h88ch33",
|
||||
"description": "Aluguel referente a Maio/2026",
|
||||
"amount": "3500.00",
|
||||
"due_date": "2026-05-10",
|
||||
"status": "pending",
|
||||
"url": "https://boleto.banco.com.br/abc123"
|
||||
},
|
||||
{
|
||||
"id": "e6cf18i9-0123-7895-e6if-5f296i99di44",
|
||||
"description": "Taxa de condomínio Abril/2026",
|
||||
"amount": "450.00",
|
||||
"due_date": "2026-04-30",
|
||||
"status": "paid",
|
||||
"url": null
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Ordenado por `due_date ASC` (vencimentos próximos primeiro). Lista vazia `[]` quando sem boletos.
|
||||
|
||||
`url` é `null` quando o link ainda não foi preenchido pelo admin. O botão de acesso deve ser desabilitado neste caso.
|
||||
|
||||
---
|
||||
|
||||
## Response 401 Unauthorized
|
||||
|
||||
```json
|
||||
{ "error": "Token inválido ou ausente" }
|
||||
```
|
||||
134
.specify/features/006-client-area/contracts/me-favorites.md
Normal file
134
.specify/features/006-client-area/contracts/me-favorites.md
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
# Contract: GET /api/v1/me/favorites
|
||||
|
||||
**Blueprint**: `client_bp`
|
||||
**Auth**: JWT Bearer — `require_auth` — ClientUser only
|
||||
|
||||
---
|
||||
|
||||
## Request
|
||||
|
||||
```
|
||||
GET /api/v1/me/favorites
|
||||
Authorization: Bearer <jwt_token>
|
||||
```
|
||||
|
||||
Sem parâmetros de query ou corpo.
|
||||
|
||||
---
|
||||
|
||||
## Response 200 OK
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"property": {
|
||||
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||
"title": "Apartamento 3 quartos Jardins",
|
||||
"slug": "apartamento-3-quartos-jardins",
|
||||
"price": "850000.00",
|
||||
"condo_fee": "1200.00",
|
||||
"type": "venda",
|
||||
"subtype": { "id": 1, "name": "Apartamento" },
|
||||
"bedrooms": 3,
|
||||
"bathrooms": 2,
|
||||
"parking_spots": 2,
|
||||
"area_m2": 120,
|
||||
"city": { "id": 1, "name": "São Paulo" },
|
||||
"neighborhood": { "id": 5, "name": "Jardins" },
|
||||
"is_featured": true,
|
||||
"photos": [
|
||||
{ "url": "https://...", "alt_text": "", "display_order": 0 }
|
||||
],
|
||||
"amenities": [{ "id": 1, "name": "Piscina" }],
|
||||
"address": "Rua das Flores, 100",
|
||||
"code": "AP001",
|
||||
"description": "Apartamento espaçoso com varanda gourmet."
|
||||
},
|
||||
"created_at": "2026-04-13T10:00:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Lista vazia `[]` quando o cliente não possui favoritos.
|
||||
|
||||
---
|
||||
|
||||
## Response 401 Unauthorized
|
||||
|
||||
```json
|
||||
{ "error": "Token inválido ou ausente" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Contract: POST /api/v1/me/favorites
|
||||
|
||||
## Request
|
||||
|
||||
```
|
||||
POST /api/v1/me/favorites
|
||||
Authorization: Bearer <jwt_token>
|
||||
Content-Type: application/json
|
||||
|
||||
{ "property_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Response 201 Created
|
||||
|
||||
```json
|
||||
{
|
||||
"property": { /* PropertyDetailOut completo */ },
|
||||
"created_at": "2026-04-13T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Response 409 Conflict
|
||||
|
||||
```json
|
||||
{ "error": "Imóvel já está nos favoritos" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Response 404 Not Found
|
||||
|
||||
```json
|
||||
{ "error": "Imóvel não encontrado" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Response 422 Unprocessable Entity
|
||||
|
||||
```json
|
||||
{ "error": "Dados inválidos", "details": ["property_id: field required"] }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Contract: DELETE /api/v1/me/favorites/<property_id>
|
||||
|
||||
## Request
|
||||
|
||||
```
|
||||
DELETE /api/v1/me/favorites/3fa85f64-5717-4562-b3fc-2c963f66afa6
|
||||
Authorization: Bearer <jwt_token>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Response 204 No Content
|
||||
|
||||
Sem corpo.
|
||||
|
||||
---
|
||||
|
||||
## Response 404 Not Found
|
||||
|
||||
```json
|
||||
{ "error": "Favorito não encontrado" }
|
||||
```
|
||||
61
.specify/features/006-client-area/contracts/me-visits.md
Normal file
61
.specify/features/006-client-area/contracts/me-visits.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# Contract: GET /api/v1/me/visits
|
||||
|
||||
**Blueprint**: `client_bp`
|
||||
**Auth**: JWT Bearer — `require_auth` — ClientUser only
|
||||
|
||||
---
|
||||
|
||||
## Request
|
||||
|
||||
```
|
||||
GET /api/v1/me/visits
|
||||
Authorization: Bearer <jwt_token>
|
||||
```
|
||||
|
||||
Sem parâmetros de query ou corpo.
|
||||
|
||||
---
|
||||
|
||||
## Response 200 OK
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "b3fc85f6-1234-4562-b3fc-2c963f66af11",
|
||||
"property": {
|
||||
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||
"title": "Apartamento 3 quartos Jardins",
|
||||
"slug": "apartamento-3-quartos-jardins"
|
||||
},
|
||||
"message": "Gostaria de visitar no final de semana.",
|
||||
"status": "pending",
|
||||
"scheduled_at": null,
|
||||
"created_at": "2026-04-13T10:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": "c4ad96g7-5678-5673-c4gd-3d074g77bg22",
|
||||
"property": {
|
||||
"id": "4gb96g75-6828-5673-c4gd-3d074g77bg33",
|
||||
"title": "Casa 4 quartos Alphaville",
|
||||
"slug": "casa-4-quartos-alphaville"
|
||||
},
|
||||
"message": "Tenho interesse em visitar esta semana.",
|
||||
"status": "confirmed",
|
||||
"scheduled_at": "2026-05-01T10:00:00Z",
|
||||
"created_at": "2026-04-10T08:30:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Ordenado por `created_at DESC`. Lista vazia `[]` quando sem visitas.
|
||||
|
||||
`property` pode ser `null` se o imóvel foi removido do banco.
|
||||
`scheduled_at` é `null` enquanto status for `pending`.
|
||||
|
||||
---
|
||||
|
||||
## Response 401 Unauthorized
|
||||
|
||||
```json
|
||||
{ "error": "Token inválido ou ausente" }
|
||||
```
|
||||
217
.specify/features/006-client-area/data-model.md
Normal file
217
.specify/features/006-client-area/data-model.md
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
# Data Model: Área do Cliente (Feature 006)
|
||||
|
||||
**Date**: 2026-04-13
|
||||
**Depends On**: Feature 005 — `client_users` table (ClientUser model)
|
||||
|
||||
---
|
||||
|
||||
## Entidades Novas
|
||||
|
||||
### SavedProperty
|
||||
|
||||
| Coluna | Tipo SQLAlchemy | Nullable | Restrições |
|
||||
|--------|----------------|----------|------------|
|
||||
| `id` | `UUID(as_uuid=True)` | NOT NULL | PK, default=uuid4 |
|
||||
| `user_id` | `UUID(as_uuid=True)` | NOT NULL | FK → `client_users.id` ON DELETE CASCADE |
|
||||
| `property_id` | `UUID(as_uuid=True)` | NULL | FK → `properties.id` ON DELETE SET NULL |
|
||||
| `created_at` | `DateTime` | NOT NULL | server_default=now() |
|
||||
|
||||
**Unique constraint**: `(user_id, property_id)` — `uq_saved_property_user_property`
|
||||
|
||||
**Relacionamentos**:
|
||||
- `user` → ClientUser (lazy="joined")
|
||||
- `property` → Property (lazy="joined") — usado para retornar detalhes do imóvel na rota favorites
|
||||
|
||||
**Lógica de remoção**: se o imóvel for deletado (`ON DELETE SET NULL`), `property_id` vira NULL; o registro SavedProperty é mantido para não perder histórico. A rota GET /me/favorites filtra registros onde `property.is_active = True` ou exibe badge "Imóvel indisponível".
|
||||
|
||||
---
|
||||
|
||||
### VisitRequest
|
||||
|
||||
| Coluna | Tipo SQLAlchemy | Nullable | Restrições |
|
||||
|--------|----------------|----------|------------|
|
||||
| `id` | `UUID(as_uuid=True)` | NOT NULL | PK, default=uuid4 |
|
||||
| `user_id` | `UUID(as_uuid=True)` | NULL | FK → `client_users.id` ON DELETE SET NULL |
|
||||
| `property_id` | `UUID(as_uuid=True)` | NULL | FK → `properties.id` ON DELETE SET NULL |
|
||||
| `message` | `Text` | NOT NULL | — |
|
||||
| `status` | `VARCHAR(20)` | NOT NULL | default=`'pending'`; valores: `pending`, `confirmed`, `cancelled`, `completed` |
|
||||
| `scheduled_at` | `DateTime` | NULL | Preenchido pelo admin ao confirmar |
|
||||
| `created_at` | `DateTime` | NOT NULL | server_default=now() |
|
||||
|
||||
**Relacionamentos**:
|
||||
- `user` → ClientUser (lazy="select")
|
||||
- `property` → Property (lazy="joined") — para retornar PropertyBrief embutido
|
||||
|
||||
**Transições de status**:
|
||||
```
|
||||
pending → confirmed (admin, com scheduled_at)
|
||||
pending → cancelled (admin)
|
||||
confirmed → completed (admin)
|
||||
confirmed → cancelled (admin)
|
||||
```
|
||||
*(Transições não são validadas em código no MVP — qualquer valor do enum é aceito via API admin)*
|
||||
|
||||
---
|
||||
|
||||
### Boleto
|
||||
|
||||
| Coluna | Tipo SQLAlchemy | Nullable | Restrições |
|
||||
|--------|----------------|----------|------------|
|
||||
| `id` | `UUID(as_uuid=True)` | NOT NULL | PK, default=uuid4 |
|
||||
| `user_id` | `UUID(as_uuid=True)` | NULL | FK → `client_users.id` ON DELETE SET NULL |
|
||||
| `property_id` | `UUID(as_uuid=True)` | NULL | FK → `properties.id` ON DELETE SET NULL |
|
||||
| `description` | `String(200)` | NOT NULL | — |
|
||||
| `amount` | `Numeric(12, 2)` | NOT NULL | — |
|
||||
| `due_date` | `Date` | NOT NULL | — |
|
||||
| `status` | `VARCHAR(20)` | NOT NULL | default=`'pending'`; valores: `pending`, `paid`, `overdue` |
|
||||
| `url` | `String(500)` | NULL | Link externo do boleto/PDF |
|
||||
| `created_at` | `DateTime` | NOT NULL | server_default=now() |
|
||||
|
||||
**Relacionamentos**:
|
||||
- `user` → ClientUser (lazy="select")
|
||||
- `property` → Property (lazy="joined") — para exibir imóvel vinculado na listagem
|
||||
|
||||
---
|
||||
|
||||
## Entidades Existentes com Impacto
|
||||
|
||||
### ClientUser (Feature 005 — pré-requisito)
|
||||
|
||||
Nenhuma alteração de schema. Relacionamentos inversos adicionados opcionalmente:
|
||||
```python
|
||||
saved_properties = db.relationship("SavedProperty", backref="user", ...) # opcional
|
||||
visit_requests = db.relationship("VisitRequest", ...) # opcional
|
||||
boletos = db.relationship("Boleto", ...) # opcional
|
||||
```
|
||||
*(Relacionamentos inversos são adicionados nos novos modelos via `backref`, não em ClientUser diretamente)*
|
||||
|
||||
### Property (existente)
|
||||
|
||||
Nenhuma alteração de schema. Relacionamentos inversos são implícitos via `backref` nos novos modelos.
|
||||
|
||||
---
|
||||
|
||||
## Migração Alembic
|
||||
|
||||
Nome do arquivo: `xxxx_add_saved_properties_visit_requests_boletos.py`
|
||||
|
||||
```sql
|
||||
-- saved_properties
|
||||
CREATE TABLE saved_properties (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES client_users(id) ON DELETE CASCADE,
|
||||
property_id UUID REFERENCES properties(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uq_saved_property_user_property UNIQUE (user_id, property_id)
|
||||
);
|
||||
|
||||
-- visit_requests
|
||||
CREATE TABLE visit_requests (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES client_users(id) ON DELETE SET NULL,
|
||||
property_id UUID REFERENCES properties(id) ON DELETE SET NULL,
|
||||
message TEXT NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
scheduled_at TIMESTAMP,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- boletos
|
||||
CREATE TABLE boletos (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES client_users(id) ON DELETE SET NULL,
|
||||
property_id UUID REFERENCES properties(id) ON DELETE SET NULL,
|
||||
description VARCHAR(200) NOT NULL,
|
||||
amount NUMERIC(12, 2) NOT NULL,
|
||||
due_date DATE NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
url VARCHAR(500),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
**Downgrade**: `DROP TABLE boletos; DROP TABLE visit_requests; DROP TABLE saved_properties;`
|
||||
|
||||
---
|
||||
|
||||
## Schemas Pydantic (backend/app/schemas/client_area.py)
|
||||
|
||||
### Saída
|
||||
|
||||
```python
|
||||
class PropertyBrief(BaseModel):
|
||||
id: UUID
|
||||
title: str
|
||||
slug: str
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
class SavedPropertyOut(BaseModel):
|
||||
property: PropertyDetailOut # reutiliza schema existente de property.py
|
||||
created_at: datetime
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
class VisitRequestOut(BaseModel):
|
||||
id: UUID
|
||||
property: PropertyBrief | None
|
||||
message: str
|
||||
status: str
|
||||
scheduled_at: datetime | None
|
||||
created_at: datetime
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
class BoletoOut(BaseModel):
|
||||
id: UUID
|
||||
description: str
|
||||
amount: Decimal
|
||||
due_date: date
|
||||
status: str
|
||||
url: str | None
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
```
|
||||
|
||||
### Entrada
|
||||
|
||||
```python
|
||||
class FavoriteIn(BaseModel):
|
||||
property_id: UUID
|
||||
|
||||
class VisitStatusIn(BaseModel):
|
||||
status: Literal["pending", "confirmed", "cancelled", "completed"]
|
||||
scheduled_at: datetime | None = None
|
||||
|
||||
class BoletoCreateIn(BaseModel):
|
||||
user_id: UUID
|
||||
property_id: UUID | None = None
|
||||
description: str
|
||||
amount: Decimal
|
||||
due_date: date
|
||||
url: str | None = None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tipos TypeScript (frontend/src/types/clientArea.ts)
|
||||
|
||||
```typescript
|
||||
export interface VisitRequest {
|
||||
id: string;
|
||||
property: { id: string; title: string; slug: string } | null;
|
||||
message: string;
|
||||
status: "pending" | "confirmed" | "cancelled" | "completed";
|
||||
scheduled_at: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface Boleto {
|
||||
id: string;
|
||||
description: string;
|
||||
amount: number;
|
||||
due_date: string;
|
||||
status: "pending" | "paid" | "overdue";
|
||||
url: string | null;
|
||||
}
|
||||
|
||||
export interface ComparisonState {
|
||||
properties: Property[]; // max 3 — Property importado de types/property.ts
|
||||
}
|
||||
```
|
||||
113
.specify/features/006-client-area/plan.md
Normal file
113
.specify/features/006-client-area/plan.md
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
# Implementation Plan: Área do Cliente
|
||||
|
||||
**Branch**: `master` | **Date**: 2026-04-13 | **Spec**: [spec.md](.specify/features/006-client-area/spec.md)
|
||||
**Input**: Feature specification from `.specify/features/006-client-area/spec.md`
|
||||
**Depends On**: Feature 005 — `ClientUser` model, `require_auth` decorator, JWT middleware
|
||||
|
||||
## Summary
|
||||
|
||||
Implementação da Área do Cliente: favoritos (persistidos no backend), comparação de imóveis (localStorage), histórico de visitas e boletos. O backend expõe dois blueprints novos (`/api/v1/me` e `/api/v1/admin`) com autenticação JWT. O frontend adiciona rotas protegidas sob `/area-do-cliente`, contextos React para favoritos e comparação, e componentes de interação (HeartButton, ComparisonBar).
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: Python 3.12 (backend) · TypeScript 5.5 (frontend)
|
||||
**Primary Dependencies**: Flask 3.x, SQLAlchemy 2.x, Pydantic v2, PyJWT (backend) · React 18, react-router-dom v6, Axios, Tailwind CSS 3.4 (frontend)
|
||||
**Storage**: PostgreSQL 16 — 3 novas tabelas: `saved_properties`, `visit_requests`, `boletos`
|
||||
**Testing**: pytest (backend) — testes de rotas com client fixture; Vite build check (frontend)
|
||||
**Target Platform**: Servidor Linux (Docker) + SPA na mesma origem via proxy Vite
|
||||
**Project Type**: Web service (Flask REST API) + SPA (React)
|
||||
**Performance Goals**: Favoritar/desfavoritar < 2 s; tabela de comparação renderizada < 1 s para 3 imóveis
|
||||
**Constraints**: Comparação não persiste no backend; admin sem UI no MVP; sem integração com gateway de pagamento
|
||||
**Scale/Scope**: MVP — funcionalidades essenciais da área logada; admin opera via API direta
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
| Princípio | Status | Observação |
|
||||
|-----------|--------|------------|
|
||||
| I. Design-First | ✅ PASS | Todos os novos componentes React usam tokens do DESIGN.md: fundo `#08090a`, painéis `#0f1011`, acento `#5e6ad2`/`#7170ff`. Nenhum estilo inline fora do sistema. |
|
||||
| II. Separation of Concerns | ✅ PASS | Flask retorna JSON puro; React é SPA. Contextos (`FavoritesContext`, `ComparisonContext`) são camada de estado frontend apenas. |
|
||||
| III. Spec-Driven | ✅ PASS | spec.md aprovado → plan.md (este arquivo) → tasks.md → implementação. |
|
||||
| IV. Data Integrity | ✅ PASS | Todas as entradas via Pydantic schemas. `amount` usa `Numeric(12, 2)`. Novas tabelas via migração Alembic. Foreign keys explicitamente anuláveis ou não-anuláveis conforme modelo. |
|
||||
| V. Security | ⚠️ PARTIAL | `/api/v1/me/*` protegido por `require_auth`. `/api/v1/admin/*` protegido por `require_auth` mas **sem verificação de role no MVP** — qualquer ClientUser autenticado pode acessar rotas admin. FR-018 exige 403 para token de ClientUser; esta verificação é **adiada** e documentada como dívida técnica (ver Complexity Tracking). |
|
||||
| VI. Simplicity First | ✅ PASS | Comparação em localStorage (sem backend). Sem gateway de pagamento. Admin sem UI. Nenhuma abstração nova sem 3+ usos concretos. |
|
||||
|
||||
**POST-DESIGN RE-CHECK**: ✅ Após Phase 1, o modelo de dados não introduz complexidade adicional. Violação de Princípio V documentada como dívida técnica deliberada para MVP.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
.specify/features/006-client-area/
|
||||
├── plan.md # Este arquivo
|
||||
├── research.md # Fase 0 — decisões e alternativas
|
||||
├── data-model.md # Fase 1 — entidades e relacionamentos
|
||||
├── quickstart.md # Fase 1 — como rodar e testar localmente
|
||||
├── contracts/
|
||||
│ ├── me-favorites.md
|
||||
│ ├── me-visits.md
|
||||
│ ├── me-boletos.md
|
||||
│ └── admin.md
|
||||
└── tasks.md # Fase 2 (gerado por /speckit.tasks)
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
|
||||
```text
|
||||
backend/
|
||||
├── app/
|
||||
│ ├── models/
|
||||
│ │ ├── saved_property.py # NOVO — SavedProperty
|
||||
│ │ ├── visit_request.py # NOVO — VisitRequest
|
||||
│ │ ├── boleto.py # NOVO — Boleto
|
||||
│ │ └── client_user.py # PRÉ-REQUISITO (Feature 005)
|
||||
│ ├── schemas/
|
||||
│ │ └── client_area.py # NOVO — todos os schemas de entrada/saída
|
||||
│ ├── routes/
|
||||
│ │ ├── client_area.py # NOVO — blueprint client_bp (/api/v1/me)
|
||||
│ │ └── admin.py # NOVO — blueprint admin_bp (/api/v1/admin)
|
||||
│ └── __init__.py # ATUALIZAR — registrar modelos e blueprints
|
||||
└── migrations/
|
||||
└── versions/
|
||||
└── xxxx_add_saved_properties_visit_requests_boletos.py # NOVO
|
||||
|
||||
frontend/
|
||||
└── src/
|
||||
├── types/
|
||||
│ └── clientArea.ts # NOVO — VisitRequest, Boleto, ComparisonState
|
||||
├── contexts/
|
||||
│ ├── ComparisonContext.tsx # NOVO — estado de comparação (localStorage)
|
||||
│ └── FavoritesContext.tsx # NOVO — favoritos (backend, apenas logado)
|
||||
├── components/
|
||||
│ ├── ComparisonBar.tsx # NOVO — barra flutuante rodapé
|
||||
│ ├── HeartButton.tsx # NOVO — toggle favorito
|
||||
│ ├── PropertyCard.tsx # ATUALIZAR — HeartButton + botão Comparar
|
||||
│ └── PropertyDetail/
|
||||
│ └── ContactSection.tsx # ATUALIZAR — criar VisitRequest se logado
|
||||
├── layouts/
|
||||
│ └── ClientLayout.tsx # NOVO — sidebar da área do cliente
|
||||
├── pages/
|
||||
│ └── client/
|
||||
│ ├── ClientDashboardPage.tsx # NOVO
|
||||
│ ├── FavoritesPage.tsx # NOVO
|
||||
│ ├── ComparisonPage.tsx # NOVO
|
||||
│ ├── VisitsPage.tsx # NOVO
|
||||
│ └── BoletosPage.tsx # NOVO
|
||||
├── services/
|
||||
│ └── clientArea.ts # NOVO — getFavorites, addFavorite, removeFavorite, getVisits, getBoletos
|
||||
└── App.tsx # ATUALIZAR — rotas protegidas + providers de contexto
|
||||
|
||||
backend/tests/
|
||||
├── test_client_area.py # NOVO — testes de rotas /me e /admin
|
||||
└── conftest.py # ATUALIZAR — fixture client_user + auth token
|
||||
```
|
||||
|
||||
**Structure Decision**: Web application (Option 2). Backend Flask REST API + frontend React SPA. Estrutura de arquivos segue o padrão já estabelecido no projeto (um arquivo por model, um arquivo por blueprint, schemas agrupados por domínio).
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
| Violação | Por que necessária | Alternativa mais simples rejeitada porque |
|
||||
|----------|-------------------|-------------------------------------------|
|
||||
| Princípio V: rotas `/api/v1/admin/*` sem verificação de role no MVP | Admin opera via API direta; implementar RBAC completo exige tabela de roles, seed e testes adicionais fora do escopo desta feature | Adicionar `require_admin_role` desde já acoplaria esta feature a Feature 005 e bloquearia o MVP sem benefício real — único usuário da API admin é o próprio dono da aplicação |
|
||||
172
.specify/features/006-client-area/quickstart.md
Normal file
172
.specify/features/006-client-area/quickstart.md
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
# Quickstart: Área do Cliente (Feature 006)
|
||||
|
||||
**Pré-requisito**: Feature 005 (autenticação) implementada e funcionando.
|
||||
|
||||
---
|
||||
|
||||
## 1. Rodar o ambiente de desenvolvimento
|
||||
|
||||
```powershell
|
||||
# Na raiz do projeto
|
||||
.\start.ps1
|
||||
```
|
||||
|
||||
Isso inicia backend (Flask) na porta 5000 e frontend (Vite) na porta 5173 via Docker Compose.
|
||||
|
||||
---
|
||||
|
||||
## 2. Aplicar a migration
|
||||
|
||||
```bash
|
||||
# Dentro do container backend ou com uv no host
|
||||
docker compose exec backend flask db upgrade
|
||||
```
|
||||
|
||||
Verifica que as tabelas `saved_properties`, `visit_requests` e `boletos` foram criadas:
|
||||
|
||||
```bash
|
||||
docker compose exec db psql -U postgres -d imobiliaria -c "\dt"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Obter um token de ClientUser
|
||||
|
||||
```bash
|
||||
# Registrar um cliente (se Feature 005 estiver implementada)
|
||||
curl -X POST http://localhost:5000/api/v1/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "João Silva", "email": "joao@test.com", "password": "Senha123!"}'
|
||||
|
||||
# Login para obter token
|
||||
curl -X POST http://localhost:5000/api/v1/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email": "joao@test.com", "password": "Senha123!"}'
|
||||
# → {"access_token": "eyJ..."}
|
||||
export TOKEN="eyJ..."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Testar rotas de favoritos
|
||||
|
||||
```bash
|
||||
# Listar favoritos (deve retornar [])
|
||||
curl http://localhost:5000/api/v1/me/favorites \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# Adicionar imóvel aos favoritos (usar UUID de um imóvel existente)
|
||||
curl -X POST http://localhost:5000/api/v1/me/favorites \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"property_id": "<uuid-do-imovel>"}'
|
||||
# → 201
|
||||
|
||||
# Tentar adicionar novamente (deve retornar 409)
|
||||
curl -X POST http://localhost:5000/api/v1/me/favorites \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"property_id": "<uuid-do-imovel>"}'
|
||||
# → 409
|
||||
|
||||
# Remover favorito
|
||||
curl -X DELETE http://localhost:5000/api/v1/me/favorites/<uuid-do-imovel> \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
# → 204
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Testar criação de boleto (admin)
|
||||
|
||||
```bash
|
||||
# Primeiro, obter o user_id do cliente criado
|
||||
USER_ID="<uuid-do-client-user>"
|
||||
|
||||
curl -X POST http://localhost:5000/api/v1/admin/boletos \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"user_id\": \"$USER_ID\",
|
||||
\"description\": \"Aluguel Maio/2026\",
|
||||
\"amount\": 3500.00,
|
||||
\"due_date\": \"2026-05-10\"
|
||||
}"
|
||||
# → 201
|
||||
|
||||
# Listar boletos do cliente
|
||||
curl http://localhost:5000/api/v1/me/boletos \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Testar atualização de status de visita (admin)
|
||||
|
||||
```bash
|
||||
# Listar visitas (deve retornar [] inicialmente)
|
||||
curl http://localhost:5000/api/v1/me/visits \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# Criar visita via formulário de contato (com token JWT no header)
|
||||
curl -X POST http://localhost:5000/api/v1/properties/<slug>/contact \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "João Silva", "email": "joao@test.com", "message": "Quero visitar."}'
|
||||
|
||||
# Obter ID da visita criada e atualizar status
|
||||
VISIT_ID="<uuid-da-visita>"
|
||||
curl -X PUT http://localhost:5000/api/v1/admin/visits/$VISIT_ID/status \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"status": "confirmed", "scheduled_at": "2026-05-01T10:00:00"}'
|
||||
# → 200
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Rodar testes de backend
|
||||
|
||||
```bash
|
||||
docker compose exec backend uv run pytest tests/test_client_area.py -v
|
||||
```
|
||||
|
||||
Ou localmente (se Python 3.12 instalado):
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
uv run pytest tests/test_client_area.py -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Acessar a área do cliente no navegador
|
||||
|
||||
1. Abrir http://localhost:5173
|
||||
2. Fazer login com as credenciais criadas no passo 3
|
||||
3. Navegar para http://localhost:5173/area-do-cliente
|
||||
4. Testar favoritar um imóvel do catálogo (coração no card)
|
||||
5. Acessar http://localhost:5173/area-do-cliente/favoritos e verificar o imóvel favoritado
|
||||
6. Adicionar imóveis à comparação (botão "Comparar" nos cards), acessar http://localhost:5173/area-do-cliente/comparar
|
||||
|
||||
---
|
||||
|
||||
## 9. UUID de imóveis existentes
|
||||
|
||||
Para obter UUIDs de imóveis para testar:
|
||||
|
||||
```bash
|
||||
curl http://localhost:5000/api/v1/properties?per_page=3 | python -m json.tool
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Solução de Problemas
|
||||
|
||||
| Problema | Causa Provável | Solução |
|
||||
|----------|----------------|---------|
|
||||
| 401 em todas as rotas `/me` | Feature 005 não implementada ou `require_auth` não disponível | Verificar se `ClientUser` model e `require_auth` decorator existem |
|
||||
| `relation "client_users" does not exist` | Migration da Feature 005 não aplicada | `flask db upgrade` para aplicar todas as migrations pendentes |
|
||||
| `relation "saved_properties" does not exist` | Migration desta feature não aplicada | `flask db upgrade` |
|
||||
| HeartButton não aparece nos cards | `FavoritesContext` não injetado no `App.tsx` | Verificar providers em `App.tsx` |
|
||||
| Rotas `/area-do-cliente/*` não redirecionam para login | `ProtectedRoute` não configurado | Verificar `App.tsx` e componente `ProtectedRoute` de Feature 005 |
|
||||
87
.specify/features/006-client-area/research.md
Normal file
87
.specify/features/006-client-area/research.md
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# Research: Área do Cliente (Feature 006)
|
||||
|
||||
**Date**: 2026-04-13
|
||||
**Status**: Complete — todos os NEEDS CLARIFICATION resolvidos
|
||||
|
||||
---
|
||||
|
||||
## 1. Padrão de favoritos: backend vs. localStorage
|
||||
|
||||
**Decisão**: Favoritos persistidos no **backend** (tabela `saved_properties`).
|
||||
**Rationale**: Requisito explícito no spec (FR-003, SC-001): estado mantido entre sessões/dispositivos. localStorage não atende porque o usuário espera ver seus favoritos ao fazer login em outro dispositivo.
|
||||
**Alternativas consideradas**: localStorage com sync eventual — rejeitado; adiciona complexidade de sincronização sem benefício para o MVP.
|
||||
|
||||
---
|
||||
|
||||
## 2. Comparação: backend vs. localStorage
|
||||
|
||||
**Decisão**: Comparação persistida apenas em **localStorage** (chave `imob_comparison`).
|
||||
**Rationale**: Spec é explícito (FR-006, Assumptions): "usar armazenamento local do navegador é suficiente para os requisitos do MVP". Comparação é sessão temporária de decisão de compra, não dado de longo prazo.
|
||||
**Alternativas consideradas**: Persistir no backend com `comparison_lists` — rejeitado (YAGNI: nenhum requisito exige cross-device para comparação).
|
||||
|
||||
---
|
||||
|
||||
## 3. VisitRequest: criação automática via formulário de contato
|
||||
|
||||
**Decisão**: Integração **opcional no MVP** — apenas se o JWT válido estiver presente no header da requisição de contato. O endpoint `POST /api/v1/properties/<slug>/contact` cria VisitRequest quando autenticado; caso contrário, cria apenas ContactLead.
|
||||
**Rationale**: FR-012 e User Story 5 exigem a integração, mas é marcada como "opcional para MVP" no design. Evitar bloqueio da feature se Feature 005 não estiver completamente integrada. Abordagem: verificar `Authorization` header sem obrigar — se ausente ou inválido, seguir fluxo normal de ContactLead.
|
||||
**Alternativas consideradas**: Novo endpoint separado para criação de VisitRequest — rejeitado; duplica a UX de submissão do formulário de contato.
|
||||
|
||||
---
|
||||
|
||||
## 4. Admin: verificação de role vs. require_auth apenas
|
||||
|
||||
**Decisão**: MVP usa apenas **`require_auth`** nas rotas admin, sem verificação de role.
|
||||
**Rationale**: Único usuário que acessa a API admin no MVP é o proprietário da aplicação. Implementar RBAC completo sem tabela de roles seria over-engineering. Dívida técnica documentada no Constitution Check.
|
||||
**Alternativas consideradas**: `require_admin_role` via campo `is_admin` no ClientUser — possível futuro; não implementar agora (YAGNI).
|
||||
|
||||
---
|
||||
|
||||
## 5. Carregamento do FavoritesContext
|
||||
|
||||
**Decisão**: `FavoritesContext` carrega a lista de favoritos via `GET /api/v1/me/favorites` na inicialização do `AuthContext` (quando `user !== null`). Armazena apenas os `UUIDs` dos imóveis favoritados em um `Set<string>`, não os objetos completos de Property.
|
||||
**Rationale**: O contexto precisa responder rapidamente ao estado "favoritado ou não" para cada card. Armazenar apenas IDs é O(1) para lookup. Os dados completos são carregados pela `FavoritesPage` sob demanda. Evita duplicar grande payload de imóveis no contexto global.
|
||||
**Alternativas consideradas**: Armazenar objetos Property completos no contexto — rejeitado; memory footprint desnecessário para o caso de uso principal (mostrar coração preenchido/vazio).
|
||||
|
||||
---
|
||||
|
||||
## 6. Estrutura do ClientLayout
|
||||
|
||||
**Decisão**: `ClientLayout.tsx` com sidebar lateral fixa em desktop, colapsável em mobile.
|
||||
**Rationale**: Padrão de dashboard consistente com DESIGN.md (fundo `#08090a`, painéis `#0f1011`). Sidebar com links: Dashboard, Favoritos, Comparar, Visitas, Boletos. User info no topo da sidebar.
|
||||
**Alternativas consideradas**: Tabs horizontais no topo — rejeitado; não escala bem com 5+ seções e viola o padrão de dashboard visual estabelecido.
|
||||
|
||||
---
|
||||
|
||||
## 7. Unicidade de SavedProperty
|
||||
|
||||
**Decisão**: Unique constraint na combinação `(user_id, property_id)` no banco de dados (além da validação no código).
|
||||
**Rationale**: FR-001 exige 409 para duplicata. A constraint no banco garante integridade mesmo em race conditions. O código verifica antes de inserir e retorna 409 em `IntegrityError` do SQLAlchemy.
|
||||
**Alternativas consideradas**: Verificar apenas em código — rejeitado; sujeito a race condition em inserts paralelos.
|
||||
|
||||
---
|
||||
|
||||
## 8. Tipo monetário para `Boleto.amount`
|
||||
|
||||
**Decisão**: `Numeric(12, 2)` — mesmo padrão de `Property.price`.
|
||||
**Rationale**: Princípio IV da constituição: "Sensitive fields (e.g., prices, area measurements) MUST use appropriate numeric types — no float for money".
|
||||
**Alternativas consideradas**: `Float` — rejeitado explicitamente pela constituição.
|
||||
|
||||
---
|
||||
|
||||
## 9. Ordenação das listas
|
||||
|
||||
**Decisão**:
|
||||
- `GET /api/v1/me/visits` → ordenado por `created_at DESC`
|
||||
- `GET /api/v1/me/boletos` → ordenado por `due_date ASC` (vencimentos próximos primeiro)
|
||||
- `GET /api/v1/me/favorites` → ordenado por `created_at DESC` (mais recentes primeiro)
|
||||
|
||||
**Rationale**: Visitas: mais recentes primeiro é padrão de log/histórico. Boletos: vencimentos próximos primeiro é mais útil financeiramente. Favoritos: mais recentes primeiro é o comportamento esperado numa lista de "wishlist".
|
||||
|
||||
---
|
||||
|
||||
## 10. Dependência de Feature 005
|
||||
|
||||
**Decisão**: Assumir que `ClientUser`, `require_auth` e JWT middleware estão disponíveis.
|
||||
**Rationale**: Declarado explicitamente como pré-requisito no spec e nas instruções. Se Feature 005 ainda não estiver implementada, as rotas retornarão 500 até que o middleware seja injetado — identificável rapidamente em teste.
|
||||
**Alternativas consideradas**: Mock de `require_auth` para desenvolvimento paralelo — possível, mas não necessário; o plano assume sequência correta de implementação.
|
||||
247
.specify/features/006-client-area/spec.md
Normal file
247
.specify/features/006-client-area/spec.md
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
# Feature Specification: Área do Cliente
|
||||
|
||||
**Feature Branch**: `006-client-area`
|
||||
**Created**: 2026-04-13
|
||||
**Status**: Draft
|
||||
**Depends On**: Feature 005 (Autenticação de Clientes — ClientUser model e JWT middleware)
|
||||
|
||||
---
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 — Favoritar e Desfavoritar Imóvel (Priority: P1)
|
||||
|
||||
Um cliente autenticado pode favoritar um imóvel a partir do card ou da página de detalhes. O estado do botão de coração é persistido no backend e permanece entre sessões.
|
||||
|
||||
**Why this priority**: Favoritos é o recurso mais imediato de retenção do usuário na plataforma. Incentiva o retorno e aumenta o tempo de sessão.
|
||||
|
||||
**Independent Test**: Pode ser testado de forma isolada ao verificar que um cliente autenticado consegue adicionar e remover um imóvel de favoritos, e que ao recarregar a página o estado é mantido.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** o cliente está autenticado e navega pelo catálogo, **When** clica no botão de coração em um PropertyCard, **Then** o imóvel é adicionado aos favoritos, o coração fica preenchido e a ação é persistida na API.
|
||||
2. **Given** o imóvel já está favoritado, **When** o cliente clica no coração novamente, **Then** o imóvel é removido dos favoritos, o coração fica vazio e o backend reflete a remoção.
|
||||
3. **Given** o cliente não está autenticado, **When** clica no coração, **Then** é redirecionado para a página de login e, após autenticar, retorna ao imóvel.
|
||||
4. **Given** o cliente já favoritou o mesmo imóvel, **When** uma segunda requisição de adição é enviada, **Then** o sistema retorna 409 sem duplicar o registro.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 — Página de Favoritos (Priority: P2)
|
||||
|
||||
Um cliente autenticado acessa `/area-do-cliente/favoritos` e visualiza todos os imóveis que marcou como favoritos, podendo desfavoritar diretamente da lista.
|
||||
|
||||
**Why this priority**: Sem a página de favoritos o botão de coração não tem destino, tornando o recurso incompleto do ponto de vista do usuário.
|
||||
|
||||
**Independent Test**: Pode ser testado verificando que a página exibe corretamente todos os imóveis favoritados e permite removê-los da lista.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** o cliente possui imóveis favoritados, **When** acessa `/area-do-cliente/favoritos`, **Then** vê uma grade de PropertyCards com botão de desfavoritar em cada um.
|
||||
2. **Given** o cliente não possui nenhum favorito, **When** acessa a página, **Then** vê o estado vazio: "Nenhum favorito ainda".
|
||||
3. **Given** o cliente clica para desfavoritar na página de favoritos, **When** a remoção é confirmada, **Then** o card desaparece da lista sem recarregar a página inteira.
|
||||
4. **Given** o cliente não está autenticado, **When** acessa a rota diretamente, **Then** é redirecionado para a página de login.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 — Painel Principal (Dashboard) (Priority: P3)
|
||||
|
||||
Um cliente autenticado acessa `/area-do-cliente` e vê um painel de resumo com contadores e atalhos para as seções da área do cliente.
|
||||
|
||||
**Why this priority**: É o ponto de entrada da área do cliente; sem ele o usuário não tem orientação após o login.
|
||||
|
||||
**Independent Test**: Pode ser testado verificando que os contadores exibidos refletem dados reais do cliente (favoritos, visitas pendentes, boletos ativos).
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** o cliente está autenticado, **When** acessa `/area-do-cliente`, **Then** vê cards de resumo mostrando: total de favoritos, visitas pendentes e boletos ativos.
|
||||
2. **Given** o cliente clica em um card de resumo (ex.: "Favoritos"), **Then** é navegado para a subseção correspondente.
|
||||
3. **Given** todos os contadores estão zerados, **When** o cliente acessa o painel, **Then** os cards exibem "0" sem mensagens de erro.
|
||||
|
||||
---
|
||||
|
||||
### User Story 4 — Comparar Imóveis (Priority: P4)
|
||||
|
||||
Um cliente pode adicionar até 3 imóveis a uma lista de comparação e visualizar uma tabela lado a lado em `/area-do-cliente/comparar`. A seleção é mantida localmente durante a sessão.
|
||||
|
||||
**Why this priority**: Recurso diferencial que ajuda na decisão de compra; não requer autenticação de dados no backend, sendo implementável de forma autônoma.
|
||||
|
||||
**Independent Test**: Pode ser testado verificando a barra flutuante de comparação, adição/remoção de imóveis e a renderização correta da tabela comparativa.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** o cliente visualiza um imóvel no catálogo, **When** clica em "Comparar", **Then** o imóvel é adicionado à barra flutuante de comparação no rodapé da tela.
|
||||
2. **Given** o cliente já tem 3 imóveis na comparação, **When** tenta adicionar um quarto, **Then** recebe uma mensagem informando que o limite de 3 imóveis foi atingido e não ocorre adição.
|
||||
3. **Given** o cliente tem ao menos 1 imóvel na barra, **When** clica em "Ver Comparação" ou acessa `/area-do-cliente/comparar`, **Then** vê uma tabela com colunas por imóvel e linhas para: preço, área, quartos, banheiros, vagas, condomínio, tipo, bairro e comodidades.
|
||||
4. **Given** o cliente clica em "Remover" em uma coluna da tabela, **Then** o imóvel é removido e a tabela é atualizada.
|
||||
5. **Given** o cliente recarrega a página, **Then** os imóveis selecionados para comparação são restaurados do armazenamento local.
|
||||
6. **Given** a lista de comparação está vazia e o cliente acessa `/area-do-cliente/comparar`, **Then** vê estado vazio com sugestão de selecionar imóveis do catálogo.
|
||||
|
||||
---
|
||||
|
||||
### User Story 5 — Histórico de Visitas (Priority: P5)
|
||||
|
||||
Um cliente autenticado acessa `/area-do-cliente/visitas` e visualiza o histórico de solicitações de visita com status atual.
|
||||
|
||||
**Why this priority**: Permite ao cliente acompanhar suas solicitações, reduzindo contato direto e dúvidas recorrentes para a equipe de vendas.
|
||||
|
||||
**Independent Test**: Pode ser testado verificando que as visitas criadas ao submeter o formulário de contato (como usuário logado) aparecem listadas com os status corretos.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** o cliente possui visitas cadastradas, **When** acessa `/area-do-cliente/visitas`, **Then** vê uma lista cronológica com: imóvel vinculado, mensagem enviada, status atual (badge colorido) e data agendada (quando confirmada).
|
||||
2. **Given** o status de uma visita é alterado pelo admin, **When** o cliente recarrega a página, **Then** o novo status é refletido.
|
||||
3. **Given** o cliente não tem nenhuma visita, **When** acessa a página, **Then** vê "Nenhuma visita agendada".
|
||||
4. **Given** o cliente está autenticado e submete o formulário de contato na página de um imóvel, **When** a solicitação é enviada, **Then** uma VisitRequest é criada com status "pending" e aparece no histórico.
|
||||
|
||||
---
|
||||
|
||||
### User Story 6 — Boletos (Priority: P6)
|
||||
|
||||
Um cliente autenticado acessa `/area-do-cliente/boletos` e visualiza os boletos criados pelo admin, podendo acessar o link de pagamento ou baixar o PDF.
|
||||
|
||||
**Why this priority**: Acesso a boletos é funcionalidade financeira crítica para clientes em processo de locação ou compra.
|
||||
|
||||
**Independent Test**: Pode ser testado com boletos criados diretamente via API de admin, verificando listagem e acesso ao link.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** o cliente possui boletos vinculados, **When** acessa `/area-do-cliente/boletos`, **Then** vê uma tabela com: imóvel (quando vinculado), descrição, valor, vencimento, badge de status e botão para acessar o boleto.
|
||||
2. **Given** o cliente clica no botão de acesso ao boleto, **Then** é aberto o link/URL do boleto em nova aba.
|
||||
3. **Given** o boleto está com status "paid", **Then** o badge exibe "Pago" em cor distinta dos demais status.
|
||||
4. **Given** o cliente não possui boletos, **When** acessa a página, **Then** vê "Nenhum boleto disponível".
|
||||
|
||||
---
|
||||
|
||||
### User Story 7 — Admin cria Boleto via API (Priority: P7)
|
||||
|
||||
Um administrador autentica na API e cria um boleto para um cliente, opcionalmente vinculado a um imóvel.
|
||||
|
||||
**Why this priority**: Backend necessário para suportar a P6; não há UI de admin no MVP.
|
||||
|
||||
**Independent Test**: Pode ser testado diretamente via chamada à API POST /api/v1/admin/boletos com token de admin.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** o admin envia POST `/api/v1/admin/boletos` com os campos obrigatórios, **Then** o boleto é criado com status "pending" e retorna 201 com os dados do boleto.
|
||||
2. **Given** o campo `user_id` não corresponde a um ClientUser existente, **Then** a API retorna 404.
|
||||
3. **Given** campos obrigatórios estão ausentes (user_id, description, amount, due_date), **Then** a API retorna 422.
|
||||
|
||||
---
|
||||
|
||||
### User Story 8 — Admin atualiza status de Visita via API (Priority: P8)
|
||||
|
||||
Um administrador atualiza o status de uma solicitação de visita e opcionalmente define a data/hora agendada.
|
||||
|
||||
**Why this priority**: Necessário para o ciclo completo de visitas; sem esta operação o cliente nunca vê status diferente de "pending".
|
||||
|
||||
**Independent Test**: Pode ser testado via PUT `/api/v1/admin/visits/<id>/status` e verificando a mudança no retorno de GET /api/v1/me/visits.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** o admin envia PUT `/api/v1/admin/visits/<id>/status` com `{"status": "confirmed", "scheduled_at": "2026-05-01T10:00:00"}`, **Then** a VisitRequest é atualizada e retorna 200.
|
||||
2. **Given** o id não existe, **Then** retorna 404.
|
||||
3. **Given** o valor de `status` não é um dos permitidos (pending/confirmed/cancelled/completed), **Then** retorna 422.
|
||||
|
||||
---
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- O que acontece quando o cliente tenta favoritar um imóvel que foi removido/desativado do catálogo? O registro SavedProperty permanece; o imóvel é exibido com indicação "Imóvel indisponível" ou omitido da listagem de favoritos.
|
||||
- O que acontece com a comparação se um dos imóveis armazenados no localStorage deixar de existir? A aplicação ignora silenciosamente o id inválido ao carregar e exibe apenas os imóveis válidos.
|
||||
- O que acontece quando a VisitRequest é criada e o cliente posteriormente envia o contato como anônimo (para o mesmo imóvel)? O ContactLead é criado normalmente (usuário anônimo) sem afetar a VisitRequest existente.
|
||||
- O que acontece com boletos cujo `url` é nulo? O botão de acesso é desabilitado ou ocultado; o boleto ainda aparece na listagem.
|
||||
- O que acontece se o token JWT expirar durante a navegação na área do cliente? O Axios interceptor (feature 005) redireciona para o login; a rota protegida bloqueia o acesso.
|
||||
|
||||
---
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
**Favoritos**
|
||||
|
||||
- **FR-001**: O sistema DEVE permitir que clientes autenticados adicionem imóveis à lista de favoritos; tentativas de adicionar um imóvel já favoritado DEVEM retornar 409.
|
||||
- **FR-002**: O sistema DEVE permitir que clientes autenticados removam imóveis da lista de favoritos; tentativas de remover um imóvel não favoritado DEVEM retornar 404.
|
||||
- **FR-003**: A lista de favoritos de um cliente DEVE ser persistida no backend e recuperada entre sessões.
|
||||
- **FR-004**: O botão de coração DEVE refletir o estado de favorito do imóvel para o cliente autenticado corrente.
|
||||
|
||||
**Comparação**
|
||||
|
||||
- **FR-005**: A aplicação DEVE permitir que o usuário adicione até 3 imóveis à lista de comparação; a adição de um quarto imóvel DEVE ser bloqueada com mensagem de feedback.
|
||||
- **FR-006**: A lista de comparação DEVE ser persistida no armazenamento local do navegador e restaurada ao recarregar a página.
|
||||
- **FR-007**: A página de comparação DEVE exibir uma tabela lado a lado com as seguintes características: preço, área, quartos, banheiros, vagas, condomínio, tipo, bairro e comodidades.
|
||||
- **FR-008**: Uma barra flutuante de comparação DEVE ser exibida no rodapé sempre que houver ao menos 1 imóvel na lista.
|
||||
|
||||
**Painel do Cliente**
|
||||
|
||||
- **FR-009**: A rota `/area-do-cliente` DEVE exibir cards de resumo com o total de favoritos, o número de visitas com status "pending" e o número de boletos com status "pending".
|
||||
- **FR-010**: Todas as rotas sob `/area-do-cliente` DEVEM ser protegidas; clientes não autenticados DEVEM ser redirecionados para o login.
|
||||
|
||||
**Visitas**
|
||||
|
||||
- **FR-011**: A rota `/area-do-cliente/visitas` DEVE exibir todas as VisitRequests do cliente autenticado, ordenadas por data de criação decrescente.
|
||||
- **FR-012**: Ao submeter o formulário de contato na página de detalhes de um imóvel, se o usuário estiver autenticado, o sistema DEVE criar uma VisitRequest vinculada ao cliente além do ContactLead.
|
||||
- **FR-013**: O admin DEVE poder atualizar o status de uma VisitRequest via API, incluindo opcionalmente a data/hora agendada.
|
||||
|
||||
**Boletos**
|
||||
|
||||
- **FR-014**: A rota `/area-do-cliente/boletos` DEVE exibir todos os boletos do cliente autenticado, ordenados por data de vencimento decrescente.
|
||||
- **FR-015**: O admin DEVE poder criar boletos para qualquer cliente via API, com ou sem vínculo a um imóvel.
|
||||
- **FR-016**: Boletos com `url` preenchida DEVEM exibir um botão de acesso; boletos sem `url` DEVEM exibir o botão desabilitado.
|
||||
|
||||
**API**
|
||||
|
||||
- **FR-017**: Todas as rotas sob `/api/v1/me/` DEVEM exigir token JWT válido de ClientUser; ausência ou token inválido DEVE retornar 401.
|
||||
- **FR-018**: Todas as rotas sob `/api/v1/admin/` DEVEM exigir autenticação de admin; acesso com token de ClientUser DEVE retornar 403.
|
||||
|
||||
---
|
||||
|
||||
### Key Entities
|
||||
|
||||
- **SavedProperty**: Associação entre um cliente e um imóvel favoritado. Atributos: identificador único, referência ao cliente, referência ao imóvel, data de criação. Unicidade garantida por par (cliente, imóvel).
|
||||
- **VisitRequest**: Solicitação de visita feita por um cliente para um imóvel. Atributos: identificador único, referência ao cliente, referência ao imóvel, mensagem livre, status do fluxo (pendente/confirmado/cancelado/concluído), data/hora agendada (opcional), data de criação.
|
||||
- **Boleto**: Documento de cobrança criado pelo admin para um cliente, opcionalmente vinculado a um imóvel. Atributos: identificador único, referência ao cliente, referência ao imóvel (opcional), descrição, valor monetário, data de vencimento, status (pendente/pago/vencido), URL de acesso (opcional), data de criação.
|
||||
- **ComparisonList** (frontend): Lista temporária de até 3 imóveis selecionados para comparação. Armazenada localmente no navegador; não persiste no backend.
|
||||
|
||||
---
|
||||
|
||||
## API Contract
|
||||
|
||||
> Todas as rotas requerem `Authorization: Bearer <token>` (client JWT, exceto rotas `/admin/` que requerem token de admin).
|
||||
|
||||
| Método | Rota | Corpo / Parâmetros | Respostas |
|
||||
|--------|------|--------------------|-----------|
|
||||
| GET | `/api/v1/me/favorites` | — | 200: `[{property completo}]` · 401 |
|
||||
| POST | `/api/v1/me/favorites` | `{property_id}` | 201 · 409 se já favoritado · 401 · 404 se imóvel não existe |
|
||||
| DELETE | `/api/v1/me/favorites/<property_id>` | — | 204 · 404 · 401 |
|
||||
| GET | `/api/v1/me/visits` | — | 200: `[{id, property:{id,title,slug}, message, status, scheduled_at, created_at}]` · 401 |
|
||||
| GET | `/api/v1/me/boletos` | — | 200: `[{id, description, amount, due_date, status, url}]` · 401 |
|
||||
| POST | `/api/v1/admin/boletos` | `{user_id, property_id?, description, amount, due_date, url?}` | 201: boleto criado · 404 cliente não existe · 422 campos inválidos · 401/403 |
|
||||
| PUT | `/api/v1/admin/visits/<id>/status` | `{status, scheduled_at?}` | 200: visita atualizada · 404 · 422 status inválido · 401/403 |
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: Clientes autenticados conseguem favoritar e desfavoritar imóveis em menos de 2 segundos por ação, com estado persistido entre sessões.
|
||||
- **SC-002**: A tabela de comparação é renderizada em menos de 1 segundo para até 3 imóveis; a seleção é restaurada corretamente ao recarregar a página.
|
||||
- **SC-003**: 100% das rotas da área do cliente bloqueiam acesso não autenticado, redirecionando para o login sem expor dados.
|
||||
- **SC-004**: O painel exibe contadores precisos — favoritos, visitas pendentes e boletos ativos — sem discrepâncias em relação ao banco de dados.
|
||||
- **SC-005**: Clientes conseguem localizar e acessar um boleto em até 3 cliques a partir do painel principal.
|
||||
- **SC-006**: Todas as mudanças de status de visita feitas pelo admin refletem no painel do cliente na próxima atualização de página.
|
||||
- **SC-007**: O formulário de contato na página de detalhes, quando submetido por cliente autenticado, cria a VisitRequest e ela aparece imediatamente no histórico de visitas.
|
||||
|
||||
---
|
||||
|
||||
## Assumptions
|
||||
|
||||
- O modelo `ClientUser` e o middleware de autenticação JWT (Feature 005) são pré-requisitos e estarão disponíveis antes da implementação desta feature.
|
||||
- O admin não terá uma interface gráfica no MVP; operações de admin (criação de boletos e atualização de status de visita) são executadas diretamente via API.
|
||||
- Boletos são gerados externamente; o sistema apenas armazena e exibe o link/URL de acesso. Nenhuma integração com gateway de pagamento é necessária no MVP.
|
||||
- A comparação de imóveis não será persistida no backend; usar armazenamento local do navegador é suficiente para os requisitos do MVP.
|
||||
- Uma VisitRequest é criada somente quando o cliente está autenticado. Usuários anônimos continuam gerando ContactLeads sem criar VisitRequests.
|
||||
- O status de boletos pode ser atualizado manualmente pelo admin via chamada à API (implícito no PUT /api/v1/admin/boletos/<id>) o que fica como extensão futura; no MVP o status só é definido na criação.
|
||||
- O design de todos os componentes segue o tema Linear dark definido em `DESIGN.md` (fundo `#08090a`, acento `#5e6ad2`/`#7170ff`).
|
||||
- A lista de favoritos retornada pela API inclui todos os detalhes do imóvel necessários para renderização do PropertyCard, sem chamadas adicionais.
|
||||
294
.specify/features/006-client-area/tasks.md
Normal file
294
.specify/features/006-client-area/tasks.md
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
# Tasks: Área do Cliente
|
||||
|
||||
**Feature**: `006-client-area`
|
||||
**Branch**: `master`
|
||||
**Input**: `spec.md`, `plan.md`, `data-model.md`, `contracts/me-favorites.md`, `contracts/me-visits.md`, `contracts/me-boletos.md`, `contracts/admin.md`, `quickstart.md`
|
||||
**Generated**: 2026-04-13
|
||||
**Depends On**: Feature 005 — `client_users` table, `require_auth` decorator, `ClientUser` model, `AuthProvider`, `AuthContext`
|
||||
**Status**: Ready for implementation
|
||||
|
||||
---
|
||||
|
||||
## Format
|
||||
|
||||
```
|
||||
- [ ] T[NNN] [P?] [USN?] Description — path/to/file.ext
|
||||
```
|
||||
|
||||
- **[P]** — Paralelizável (arquivo diferente, sem dependência de tarefa incompleta)
|
||||
- **[USN]** — User Story associada (US1–US8)
|
||||
- IDs sequenciais na ordem de execução recomendada
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Foundational — Modelos SQLAlchemy e Migration
|
||||
|
||||
**Objetivo**: Criar os três modelos de dados novos (`SavedProperty`, `VisitRequest`, `Boleto`) e gerar a migration Alembic correspondente. Todas as rotas de backend dependem destas tarefas estarem concluídas.
|
||||
|
||||
**⚠️ CRÍTICO**: Nenhum endpoint de `/api/v1/me/*` ou `/api/v1/admin/*` pode ser implementado antes de T001–T005 estarem concluídos.
|
||||
|
||||
| ID | Complexidade | Deps | spec_ref |
|
||||
|----|-------------|------|----------|
|
||||
| T001 | S | Feature 005 | data-model.md §SavedProperty |
|
||||
| T002 | S | Feature 005 | data-model.md §VisitRequest |
|
||||
| T003 | S | Feature 005 | data-model.md §Boleto |
|
||||
| T004 | S | T001, T002, T003 | plan.md §backend/app/models/__init__.py |
|
||||
| T005 | M | T004 | data-model.md §Migração Alembic |
|
||||
|
||||
- [ ] T001 [P] Criar modelo SQLAlchemy `SavedProperty` com colunas: `id` (UUID PK, `default=uuid4`), `user_id` (UUID NOT NULL, FK → `client_users.id` `ondelete="CASCADE"`), `property_id` (UUID NULL, FK → `properties.id` `ondelete="SET NULL"`), `created_at` (DateTime NOT NULL, `server_default=func.now()`); constraint única `uq_saved_property_user_property (user_id, property_id)`; relacionamentos `user` → `ClientUser` (`lazy="joined"`) e `property` → `Property` (`lazy="joined"`) — `backend/app/models/saved_property.py`
|
||||
- **Done when**: `from app.models.saved_property import SavedProperty` importa sem erro; `SavedProperty.__tablename__ == "saved_properties"`; `SavedProperty.user_id.property.foreign_keys` aponta para `client_users.id`; `SavedProperty.property_id` tem `nullable=True` e FK para `properties.id`; `flask db migrate` detecta o novo modelo.
|
||||
|
||||
- [ ] T002 [P] Criar modelo SQLAlchemy `VisitRequest` com colunas: `id` (UUID PK, `default=uuid4`), `user_id` (UUID NULL, FK → `client_users.id` `ondelete="SET NULL"`), `property_id` (UUID NULL, FK → `properties.id` `ondelete="SET NULL"`), `message` (Text NOT NULL), `status` (VARCHAR(20) NOT NULL, `default='pending'`), `scheduled_at` (DateTime NULL), `created_at` (DateTime NOT NULL, `server_default=func.now()`); relacionamentos `user` → `ClientUser` (`lazy="select"`) e `property` → `Property` (`lazy="joined"`) — `backend/app/models/visit_request.py`
|
||||
- **Done when**: `from app.models.visit_request import VisitRequest` importa sem erro; `VisitRequest.__tablename__ == "visit_requests"`; `VisitRequest.status.default.arg == "pending"`; `VisitRequest.scheduled_at` tem `nullable=True`; `VisitRequest.message` tem `nullable=False`.
|
||||
|
||||
- [ ] T003 [P] Criar modelo SQLAlchemy `Boleto` com colunas: `id` (UUID PK, `default=uuid4`), `user_id` (UUID NULL, FK → `client_users.id` `ondelete="SET NULL"`), `property_id` (UUID NULL, FK → `properties.id` `ondelete="SET NULL"`), `description` (String(200) NOT NULL), `amount` (Numeric(12, 2) NOT NULL), `due_date` (Date NOT NULL), `status` (VARCHAR(20) NOT NULL, `default='pending'`), `url` (String(500) NULL), `created_at` (DateTime NOT NULL, `server_default=func.now()`); relacionamentos `user` → `ClientUser` (`lazy="select"`) e `property` → `Property` (`lazy="joined"`) — `backend/app/models/boleto.py`
|
||||
- **Done when**: `from app.models.boleto import Boleto` importa sem erro; `Boleto.__tablename__ == "boletos"`; `Boleto.amount` é instância de `db.Numeric(12, 2)`; `Boleto.url` tem `nullable=True`; `Boleto.due_date` usa `db.Date`.
|
||||
|
||||
- [ ] T004 Importar `SavedProperty`, `VisitRequest` e `Boleto` em `backend/app/models/__init__.py` para que os modelos sejam detectados pelo Flask-SQLAlchemy e pelo Alembic na geração de migrations — `backend/app/models/__init__.py`
|
||||
- **Done when**: `from app.models import SavedProperty, VisitRequest, Boleto` importa sem erro; `flask db migrate` executado em branco não reporta novas tabelas (confirmando que os modelos já estão registrados no metadata do SQLAlchemy).
|
||||
|
||||
- [ ] T005 Gerar e revisar migration Alembic que cria as tabelas `saved_properties`, `visit_requests` e `boletos` com todas as colunas, foreign keys ON DELETE e constraint única conforme `data-model.md §Migração Alembic` — `backend/migrations/versions/<hash>_add_saved_properties_visit_requests_boletos.py`
|
||||
- **Done when**: `flask db migrate -m "add saved_properties visit_requests boletos"` cria o arquivo de migration; revisão manual confirma `op.create_table("saved_properties", ...)`, `op.create_table("visit_requests", ...)` e `op.create_table("boletos", ...)` com todas as colunas listadas em `data-model.md`; `op.create_unique_constraint("uq_saved_property_user_property", "saved_properties", ["user_id", "property_id"])` está presente; `flask db upgrade` executa sem erro; `flask db downgrade -1` reverte sem erro; `flask db upgrade` re-aplica sem erro.
|
||||
|
||||
**Checkpoint Phase 1**: `flask db upgrade` cria as três tabelas no banco; `from app.models import SavedProperty, VisitRequest, Boleto` importa sem erro em contexto de aplicativo Flask.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Foundational — Schemas Pydantic e Blueprints
|
||||
|
||||
**Objetivo**: Criar os schemas Pydantic de entrada/saída e os dois blueprints Flask (`client_bp` e `admin_bp`), registrando-os na factory. Estas tarefas são pré-requisito para qualquer teste de endpoint.
|
||||
|
||||
**⚠️ CRÍTICO**: T007 e T008 (rotas) dependem de T006 (schemas). T009 finaliza o registro na aplicação.
|
||||
|
||||
| ID | Complexidade | Deps | spec_ref |
|
||||
|----|-------------|------|----------|
|
||||
| T006 | M | T001, T002, T003 | contracts/ (todos), data-model.md |
|
||||
| T007 | M | T005, T006, Feature 005 | contracts/me-favorites.md, contracts/me-visits.md, contracts/me-boletos.md |
|
||||
| T008 | M | T005, T006, Feature 005 | contracts/admin.md, spec.md §US7, §US8 |
|
||||
| T009 | S | T007, T008 | plan.md §backend/app/__init__.py |
|
||||
|
||||
- [ ] T006 Criar schemas Pydantic em `backend/app/schemas/client_area.py`: `PropertyBrief` (id: UUID, title: str, slug: str), `FavoriteOut` (property: PropertyBrief | None, created_at: datetime), `VisitRequestOut` (id: UUID, property: PropertyBrief | None, message: str, status: str, scheduled_at: datetime | None, created_at: datetime), `BoletoOut` (id: UUID, description: str, amount: Decimal, due_date: date, status: str, url: str | None), `CreateBoletoIn` (user_id: UUID, description: str max_length=200, amount: Decimal ge=Decimal("0.01"), due_date: date, property_id: UUID | None = None, url: str | None = None, max_length=500), `UpdateVisitStatusIn` (status: Literal["pending", "confirmed", "cancelled", "completed"], scheduled_at: datetime | None = None); todos com `model_config = ConfigDict(from_attributes=True)` — `backend/app/schemas/client_area.py`
|
||||
- **Done when**: `from app.schemas.client_area import FavoriteOut, VisitRequestOut, BoletoOut, CreateBoletoIn, UpdateVisitStatusIn` importa sem erro; `CreateBoletoIn(user_id=uuid4(), description="X", amount=Decimal("100"), due_date=date.today())` valida sem erro; `CreateBoletoIn(..., amount=Decimal("-1"), ...)` levanta `ValidationError`; `UpdateVisitStatusIn(status="invalid")` levanta `ValidationError`; `UpdateVisitStatusIn(status="confirmed")` valida sem erro.
|
||||
|
||||
- [ ] T007 Criar blueprint `client_bp` com prefixo `/api/v1/me`; todos os endpoints decorados com `@require_auth` (Feature 005): `GET /favorites` → filtra `SavedProperty` por `user_id = g.current_user_id`, retorna lista de `FavoriteOut` (200), inclui `property=None` para imóveis deletados; `POST /favorites` → aceita `{"property_id": "<uuid>"}`, cria `SavedProperty`, retorna 201 — ou 409 `{"error": "Já adicionado aos favoritos"}` se registro duplicado; `DELETE /favorites/<property_id>` → remove `SavedProperty` do usuário, retorna 204 — ou 404 `{"error": "Favorito não encontrado"}` se não existir; `GET /visits` → retorna lista de `VisitRequestOut` do usuário ordenada por `created_at DESC` (200); `GET /boletos` → retorna lista de `BoletoOut` do usuário ordenada por `due_date ASC` (200) — `backend/app/routes/client_area.py`
|
||||
- **Done when**: `from app.routes.client_area import client_bp` importa sem erro; `GET /api/v1/me/favorites` sem token retorna 401; com token válido retorna 200 + lista JSON; `POST /api/v1/me/favorites` com `property_id` válido retorna 201; segunda POST com mesmo `property_id` retorna 409; `DELETE /api/v1/me/favorites/<id>` com id não favoritado retorna 404; com id favoritado retorna 204; `GET /api/v1/me/visits` retorna 200 + lista ordenada por `created_at DESC`; `GET /api/v1/me/boletos` retorna 200 + lista ordenada por `due_date ASC`.
|
||||
|
||||
- [ ] T008 Criar blueprint `admin_bp` com prefixo `/api/v1/admin`; endpoints protegidos por `@require_auth` (MVP sem verificação de role — comentário `# TODO: verificar role admin — dívida técnica MVP`): `POST /boletos` → valida `CreateBoletoIn`, busca `ClientUser` por `user_id` (retorna 404 `{"error": "Cliente não encontrado"}` se inexistente), persiste `Boleto`, retorna 201 com `BoletoOut`; `PUT /visits/<id>/status` → valida `UpdateVisitStatusIn`, busca `VisitRequest` por `id` (retorna 404 `{"error": "Visita não encontrada"}` se inexistente), atualiza `status` e `scheduled_at`, retorna 200 com `VisitRequestOut` — `backend/app/routes/admin.py`
|
||||
- **Done when**: `from app.routes.admin import admin_bp` importa sem erro; `POST /api/v1/admin/boletos` com campos obrigatórios retorna 201 com `id`, `description`, `amount`, `status="pending"`; `user_id` inexistente retorna 404 `{"error": "Cliente não encontrado"}`; campos obrigatórios ausentes retornam 422; `PUT /api/v1/admin/visits/<uuid>/status` com `{"status": "confirmed", "scheduled_at": "2026-05-01T10:00:00"}` retorna 200 com `id` e `status="confirmed"`; id inexistente retorna 404; `status="invalido"` retorna 422.
|
||||
|
||||
- [ ] T009 Registrar `client_bp` e `admin_bp` na factory `create_app()` com `app.register_blueprint(client_bp)` e `app.register_blueprint(admin_bp)` — `backend/app/__init__.py`
|
||||
- **Done when**: Flask inicia sem erros após a alteração; `GET /api/v1/me/favorites` sem token retorna 401 (rota existe); `POST /api/v1/admin/boletos` sem token retorna 401 (rota existe); `flask routes` lista as rotas `client_bp.*` e `admin_bp.*`.
|
||||
|
||||
**Checkpoint Phase 2**: `GET /api/v1/me/favorites` com token válido retorna `[]` (sem favoritos); `POST /api/v1/admin/boletos` com dados válidos retorna 201 com corpo JSON.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: US1 — Favoritar e Desfavoritar Imóvel (Priority: P1) 🎯 MVP
|
||||
|
||||
**Goal**: Cliente autenticado consegue favoritar/desfavoritar imóvel pelo botão de coração no card ou na página de detalhe. Estado é persistido no backend e recuperado entre sessões.
|
||||
|
||||
**Independent Test**: Cliente autenticado adiciona favorito → recarrega página → coração permanece preenchido. Clica novamente → removido. Cliente não autenticado clica no coração → redirecionado para `/login`.
|
||||
|
||||
| ID | Complexidade | Deps | spec_ref |
|
||||
|----|-------------|------|----------|
|
||||
| T010 | S | — | plan.md §frontend/types, spec.md §US1 |
|
||||
| T011 | S | T010 | contracts/me-favorites.md, plan.md §services/clientArea.ts |
|
||||
| T013 | M | T010, T011, Feature 005 (AuthContext) | plan.md §FavoritesContext, spec.md §FR-003, FR-004 |
|
||||
| T014 | S | T013 | plan.md §HeartButton, spec.md §US1 SC-001 |
|
||||
| T023 | M | T014, T012 | plan.md §PropertyCard.tsx, spec.md §US1 SC-001, §US4 SC-001 |
|
||||
|
||||
- [ ] T010 [P] [US1] Criar interfaces TypeScript: `PropertyBrief` (id: string, title: string, slug: string), `SavedFavorite` (property: PropertyBrief | null, created_at: string), `VisitRequest` (id: string, property: PropertyBrief | null, message: string, status: "pending" | "confirmed" | "cancelled" | "completed", scheduled_at: string | null, created_at: string), `Boleto` (id: string, description: string, amount: string, due_date: string, status: "pending" | "paid" | "overdue", url: string | null), `ComparisonState` (ids: string[], properties: Property[]) — `frontend/src/types/clientArea.ts`
|
||||
- **Done when**: `import { SavedFavorite, VisitRequest, Boleto, ComparisonState } from '@/types/clientArea'` compila sem erro TypeScript; `VisitRequest.status` aceita apenas os 4 literais; `Boleto.status` aceita apenas "pending" | "paid" | "overdue"; `Boleto.url` é `string | null`; `ComparisonState.properties` usa o tipo `Property` de `@/types/property`.
|
||||
|
||||
- [ ] T011 [P] [US1] Criar `frontend/src/services/clientArea.ts` exportando: `getFavorites(): Promise<SavedFavorite[]>` → `GET /api/v1/me/favorites`; `addFavorite(propertyId: string): Promise<void>` → `POST /api/v1/me/favorites`; `removeFavorite(propertyId: string): Promise<void>` → `DELETE /api/v1/me/favorites/<propertyId>`; `getVisits(): Promise<VisitRequest[]>` → `GET /api/v1/me/visits`; `getBoletos(): Promise<Boleto[]>` → `GET /api/v1/me/boletos`; todas usando a instância axios de `@/services/api` — `frontend/src/services/clientArea.ts`
|
||||
- **Done when**: `import { getFavorites, addFavorite, removeFavorite, getVisits, getBoletos } from '@/services/clientArea'` compila sem erro TypeScript; `addFavorite` envia `POST /api/v1/me/favorites` com `{ property_id: id }` no body; `removeFavorite` envia `DELETE /api/v1/me/favorites/<id>`; build TypeScript sem erros.
|
||||
|
||||
- [ ] T013 [US1] Criar `FavoritesContext` com `FavoritesProvider` que: ao montar, verifica autenticação via `AuthContext` e, se autenticado, carrega favoritos via `getFavorites()` armazenando `favoriteIds: string[]`; expõe `isFavorite(id: string): boolean` e `toggleFavorite(id: string): Promise<void>` — quando não autenticado `toggleFavorite` redireciona para `/login` via `useNavigate` sem chamar a API; quando autenticado, chama `addFavorite` ou `removeFavorite` conforme estado atual e atualiza `favoriteIds` localmente — `frontend/src/contexts/FavoritesContext.tsx`
|
||||
- **Done when**: `import { useFavorites, FavoritesProvider } from '@/contexts/FavoritesContext'` compila sem erro TypeScript; `isFavorite(id)` retorna `true` para id presente em `favoriteIds`; `toggleFavorite(id)` dispara `removeFavorite` quando já favoritado e `addFavorite` quando não favoritado; usuário não autenticado ao chamar `toggleFavorite` é navegado para `/login` sem chamada à API; build TypeScript sem erros.
|
||||
|
||||
- [ ] T014 [US1] Criar componente funcional `HeartButton` recebendo `propertyId: string`; usa `useFavorites()` para obter `isFavorite(propertyId)` e `toggleFavorite`; exibe SVG de coração preenchido (favoritado, cor `#7170ff`) ou vazio (não favoritado, cor `text-gray-400`) com Tailwind; exibe spinner ou opacidade reduzida durante loading da operação; tem `aria-label` dinâmico ("Adicionar aos favoritos" / "Remover dos favoritos"); `onClick` chama `toggleFavorite(propertyId)` e previne propagação do evento — `frontend/src/components/HeartButton.tsx`
|
||||
- **Done when**: `import HeartButton from '@/components/HeartButton'` compila sem erro TypeScript; `<HeartButton propertyId="test-id" />` renderiza sem erro; coração altera visual após `toggleFavorite`; `aria-label` reflete o estado; build TypeScript sem erros.
|
||||
|
||||
- [ ] T023 [US1] Adicionar `HeartButton` ao canto superior direito da imagem do `PropertyCard.tsx` (sobreposição com `absolute top-2 right-2`); adicionar botão "Comparar" / "Remover da comparação" ao lado do `HeartButton` usando `useComparison()` para chamar `toggleComparison(property)` — botão desabilitado com tooltip quando limite de 3 atingido e imóvel não está na lista; adicionar os mesmos dois botões na área de ações do cabeçalho de `PropertyDetailPage.tsx` — `frontend/src/components/PropertyCard.tsx` e `frontend/src/pages/PropertyDetailPage.tsx`
|
||||
- **Done when**: `PropertyCard` exibe `HeartButton` e botão Comparar sobrepostos na imagem sem quebrar layout; clicar no coração persiste favorito via `FavoritesContext`; clicar em "Comparar" adiciona ao `ComparisonContext`; `PropertyDetailPage` exibe ambos os botões; botão Comparar exibe "Remover da comparação" quando imóvel já está na lista; botão Comparar com `disabled` e tooltip explicativo ao tentar adicionar 4º item; build TypeScript sem erros.
|
||||
|
||||
**Checkpoint Phase 3 (US1)**: Click no coração do `PropertyCard` → favoritar → recarregar página → coração preenchido. Usuário não autenticado → redireciona para login.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: US2 — Página de Favoritos (Priority: P2)
|
||||
|
||||
**Goal**: Cliente acessa `/area-do-cliente/favoritos` e vê grade de imóveis favoritados. Pode desfavoritar diretamente da lista sem recarregar a página inteira.
|
||||
|
||||
**Independent Test**: Página exibe grade de `PropertyCard`; desfavoritar remove o card imediatamente; estado vazio "Nenhum favorito ainda" com link para o catálogo.
|
||||
|
||||
| ID | Complexidade | Deps | spec_ref |
|
||||
|----|-------------|------|----------|
|
||||
| T016 | M | Feature 005 (AuthContext) | plan.md §ClientLayout, spec.md §US2–US6 |
|
||||
| T018 | S | T013, T016 | spec.md §US2 |
|
||||
|
||||
- [ ] T016 [US2] Criar `ClientLayout` com sidebar de navegação lateral contendo cinco links: Dashboard (`/area-do-cliente`), Favoritos (`/area-do-cliente/favoritos`), Comparar (`/area-do-cliente/comparar`), Visitas (`/area-do-cliente/visitas`), Boletos (`/area-do-cliente/boletos`); paleta DESIGN.md: fundo sidebar `bg-[#0f1011]`, texto `text-[#e2e2e2]`, item ativo com borda/fundo `#5e6ad2`; renderiza `<Outlet />` para a página filha; rota protegida — se `!isAuthenticated` redireciona para `/login` via `<Navigate replace />` — `frontend/src/layouts/ClientLayout.tsx`
|
||||
- **Done when**: `import ClientLayout from '@/layouts/ClientLayout'` compila sem erro TypeScript; sidebar renderiza os 5 links de navegação; link da rota ativa exibe estilo destacado; `<Outlet />` renderiza a página filha; usuário não autenticado acessando qualquer subrota de `/area-do-cliente` é redirecionado para `/login`; build TypeScript sem erros.
|
||||
|
||||
- [ ] T018 [US2] Criar `FavoritesPage` que fetcha `getFavorites()` no mount; exibe grade de `PropertyCard` com `HeartButton` para cada favorito; ao desfavoritar um item via `toggleFavorite`, remove o card da lista localmente (re-fetch ou filtro por `favoriteIds`); estado vazio exibe "Nenhum favorito ainda" com botão/link "Explorar imóveis" apontando para `/imoveis`; exibe skeleton durante loading — `frontend/src/pages/client/FavoritesPage.tsx`
|
||||
- **Done when**: `import FavoritesPage from '@/pages/client/FavoritesPage'` compila sem erro TypeScript; página exibe grade de cards quando há favoritos; desfavoritar um card o remove sem reload completo; estado vazio exibe "Nenhum favorito ainda" com link para catálogo; build TypeScript sem erros.
|
||||
|
||||
**Checkpoint Phase 4 (US2)**: `/area-do-cliente/favoritos` renderiza grade de favoritos; desfavoritar remove o card imediatamente do DOM.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: US3 — Painel Principal / Dashboard (Priority: P3)
|
||||
|
||||
**Goal**: Cliente acessa `/area-do-cliente` e vê painel com contadores de favoritos, visitas pendentes e boletos ativos, com links diretos para cada seção.
|
||||
|
||||
**Independent Test**: 3 cards de resumo com contadores reais; "0" exibido sem erros quando todos zerados; clicar em card navega para a seção correta.
|
||||
|
||||
| ID | Complexidade | Deps | spec_ref |
|
||||
|----|-------------|------|----------|
|
||||
| T017 | S | T016, T011 | spec.md §US3 |
|
||||
|
||||
- [ ] T017 [US3] Criar `ClientDashboardPage` que ao montar fetcha em paralelo `getFavorites()`, `getVisits()` e `getBoletos()`; calcula: `total de favoritos`, `visitas com status="pending"`, `boletos com status="pending" ou "overdue"`; exibe 3 cards clicáveis com ícone, label e contador; cada card navega via `Link` para a subseção correspondente; exibe skeleton durante loading inicial — `frontend/src/pages/client/ClientDashboardPage.tsx`
|
||||
- **Done when**: `import ClientDashboardPage from '@/pages/client/ClientDashboardPage'` compila sem erro TypeScript; página exibe 3 cards de resumo com contadores; contadores exibem "0" sem erro quando dados vazios; card "Favoritos" navega para `/area-do-cliente/favoritos`; card "Visitas" navega para `/area-do-cliente/visitas`; card "Boletos" navega para `/area-do-cliente/boletos`; build TypeScript sem erros.
|
||||
|
||||
**Checkpoint Phase 5 (US3)**: `/area-do-cliente` renderiza painel com 3 cards; clicar em "Favoritos" navega para `/area-do-cliente/favoritos`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: US4 — Comparar Imóveis (Priority: P4)
|
||||
|
||||
**Goal**: Usuário adiciona até 3 imóveis à comparação (sem backend, apenas localStorage), vê barra flutuante no rodapé e acessa tabela comparativa lado a lado em `/area-do-cliente/comparar`.
|
||||
|
||||
**Independent Test**: Barra flutuante aparece ao adicionar imóvel; limite de 3 bloqueado com feedback; tabela comparativa exibe 9 linhas de atributos; localStorage persiste entre reloads.
|
||||
|
||||
| ID | Complexidade | Deps | spec_ref |
|
||||
|----|-------------|------|----------|
|
||||
| T012 | M | T010 | plan.md §ComparisonContext, spec.md §US4, §FR-005–FR-009 |
|
||||
| T015 | S | T012 | plan.md §ComparisonBar, spec.md §US4 SC-001 |
|
||||
| T019 | M | T012 | spec.md §US4 |
|
||||
| T024 | S | T015, T022 | plan.md §App.tsx, spec.md §US4 SC-001 |
|
||||
|
||||
- [ ] T012 [P] [US4] Criar `ComparisonContext` com `ComparisonProvider` que: persiste `ids: string[]` em `localStorage` sob chave `comparison_ids` e `properties: Property[]` sob `comparison_properties`; restaura estado do localStorage na inicialização (ignora silenciosamente ids cujos dados não estejam disponíveis); expõe `comparisonItems: Property[]`, `isInComparison(id: string): boolean`, `toggleComparison(property: Property): void` — quando lista tem 3 itens e o imóvel não está nela, exibe `alert` ou `toast` "Limite de 3 imóveis para comparação atingido" e retorna sem adicionar; `clearComparison(): void` — limpa lista e localStorage — `frontend/src/contexts/ComparisonContext.tsx`
|
||||
- **Done when**: `import { useComparison, ComparisonProvider } from '@/contexts/ComparisonContext'` compila sem erro TypeScript; `toggleComparison(p1)` adiciona ao array; `isInComparison(p1.id)` retorna `true`; segundo `toggleComparison(p1)` remove; ao ter 3 items, `toggleComparison(p4)` não modifica o array e exibe feedback; `localStorage` atualizado após cada operação; reload da página restaura os items; `clearComparison()` limpa array e localStorage; build TypeScript sem erros.
|
||||
|
||||
- [ ] T015 [US4] Criar `ComparisonBar` barra flutuante renderizada no rodapé quando `comparisonItems.length > 0`: posição `fixed bottom-0 left-0 right-0 z-50`; fundo `bg-[#0f1011]` com borda superior `border-t border-[#5e6ad2]`; exibe thumbnails (foto + título truncado) dos imóveis selecionados; botão "×" por thumbnail chama `toggleComparison(item)` para remover; contador "N imóvel(is)"; botão "Ver Comparação" navega para `/area-do-cliente/comparar`; botão "Limpar" chama `clearComparison()`; barra ausente do DOM quando lista vazia — `frontend/src/components/ComparisonBar.tsx`
|
||||
- **Done when**: `import ComparisonBar from '@/components/ComparisonBar'` compila sem erro TypeScript; barra aparece com 1+ items de comparação; botão "×" remove o item; "Ver Comparação" navega para `/area-do-cliente/comparar`; "Limpar" esvazia a lista; barra não renderiza quando lista vazia; build TypeScript sem erros.
|
||||
|
||||
- [ ] T019 [US4] Criar `ComparisonPage` em `/area-do-cliente/comparar`: quando `comparisonItems.length > 0` exibe tabela HTML com cabeçalho (foto + título + botão "Remover" por coluna) e linhas para: Preço, Área (m²), Quartos, Banheiros, Vagas, Condomínio, Tipo, Bairro e Comodidades; quando lista vazia exibe estado vazio "Selecione imóveis no catálogo para comparar" com link `<Link to="/imoveis">` — `frontend/src/pages/client/ComparisonPage.tsx`
|
||||
- **Done when**: `import ComparisonPage from '@/pages/client/ComparisonPage'` compila sem erro TypeScript; tabela exibe colunas para cada imóvel na comparação; linha "Quartos" exibe valor correto para cada imóvel; botão "Remover" na coluna chama `toggleComparison` e remove a coluna; estado vazio exibe mensagem com link para `/imoveis`; build TypeScript sem erros.
|
||||
|
||||
- [ ] T024 [US4] Renderizar `<ComparisonBar />` no `App.tsx` fora do bloco `<Routes>` (após `</Routes>`), dentro dos providers `ComparisonProvider`, para que seja visível em todas as páginas — `frontend/src/App.tsx`
|
||||
- **Done when**: `ComparisonBar` é visível no catálogo de imóveis ao adicionar um imóvel; barra persiste ao navegar entre páginas; `ComparisonBar` não aparece quando lista de comparação está vazia; build TypeScript sem erros (esta task é executada junto com T022).
|
||||
|
||||
**Checkpoint Phase 6 (US4)**: Adicionar imóvel no catálogo → barra flutuante aparece no rodapé; recarregar página → imóveis restaurados do localStorage; `/area-do-cliente/comparar` exibe tabela comparativa.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: US5 — Histórico de Visitas (Priority: P5)
|
||||
|
||||
**Goal**: Cliente acessa `/area-do-cliente/visitas` e vê histórico de solicitações de visita com status atual, data agendada quando confirmada e imóvel vinculado.
|
||||
|
||||
**Independent Test**: Listagem exibe visitas com badge de status correto; `property=null` exibe "Imóvel removido"; estado vazio "Nenhuma visita agendada".
|
||||
|
||||
| ID | Complexidade | Deps | spec_ref |
|
||||
|----|-------------|------|----------|
|
||||
| T020 | S | T016, T011 | spec.md §US5, contracts/me-visits.md |
|
||||
|
||||
- [ ] T020 [US5] Criar `VisitsPage` que fetcha `getVisits()` e exibe lista cronológica (mais recente primeiro); cada item exibe: imóvel vinculado (link para `/imoveis/<slug>` com título, ou texto "Imóvel removido" quando `property` for null), mensagem enviada, badge de status colorido (`pending`=cinza/azul, `confirmed`=verde, `cancelled`=vermelho, `completed`=roxo) e data agendada formatada como `DD/MM/YYYY HH:mm` quando `scheduled_at` não for null; estado vazio "Nenhuma visita agendada"; skeleton durante loading — `frontend/src/pages/client/VisitsPage.tsx`
|
||||
- **Done when**: `import VisitsPage from '@/pages/client/VisitsPage'` compila sem erro TypeScript; página exibe lista de visitas com badge colorido por status; `property=null` exibe "Imóvel removido" sem erro; `scheduled_at` formatado quando presente; estado vazio exibe "Nenhuma visita agendada"; build TypeScript sem erros.
|
||||
|
||||
**Checkpoint Phase 7 (US5)**: `/area-do-cliente/visitas` renderiza histórico de visitas com badges de status corretos para cada item.
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: US6 — Boletos (Priority: P6)
|
||||
|
||||
**Goal**: Cliente acessa `/area-do-cliente/boletos` e vê tabela de boletos com valor, vencimento, badge de status e botão de acesso ao link (desabilitado quando `url=null`).
|
||||
|
||||
**Independent Test**: Tabela com colunas corretas; badge "Pago" em verde; botão "Acessar Boleto" desabilitado quando `url=null`; estado vazio "Nenhum boleto disponível".
|
||||
|
||||
| ID | Complexidade | Deps | spec_ref |
|
||||
|----|-------------|------|----------|
|
||||
| T021 | S | T016, T011 | spec.md §US6, contracts/me-boletos.md |
|
||||
|
||||
- [ ] T021 [US6] Criar `BoletosPage` que fetcha `getBoletos()` e exibe tabela com colunas: Imóvel (título do `property` quando vinculado, ou "—"), Descrição, Valor (formatado como BRL, ex: `R$ 3.500,00`), Vencimento (formatado como `DD/MM/YYYY`), Status (badge: `pending`=amarelo, `paid`=verde, `overdue`=vermelho), Ação (botão "Acessar Boleto" com `target="_blank"` e `rel="noopener noreferrer"` — desabilitado/oculto quando `url` é `null`); estado vazio "Nenhum boleto disponível" — `frontend/src/pages/client/BoletosPage.tsx`
|
||||
- **Done when**: `import BoletosPage from '@/pages/client/BoletosPage'` compila sem erro TypeScript; tabela exibe todas as 6 colunas; botão "Acessar Boleto" tem `target="_blank"` e `rel="noopener noreferrer"`; botão desabilitado quando `url=null`; badge "Pago" exibe cor verde para `status="paid"`; estado vazio exibe "Nenhum boleto disponível"; build TypeScript sem erros.
|
||||
|
||||
**Checkpoint Phase 8 (US6)**: `/area-do-cliente/boletos` renderiza tabela de boletos; boleto com `url=null` desabilita botão de acesso.
|
||||
|
||||
---
|
||||
|
||||
## Phase 9: US7+US8 — Endpoints de Admin (Priority: P7/P8)
|
||||
|
||||
**Goal**: Admin cria boleto via `POST /api/v1/admin/boletos` (US7) e atualiza status de visita via `PUT /api/v1/admin/visits/<id>/status` (US8). Sem UI no MVP — operação exclusivamente via API.
|
||||
|
||||
*Ambos os endpoints foram implementados em T008 (Phase 2). Nenhuma task adicional nesta fase.*
|
||||
|
||||
**⚠️ Dívida Técnica MVP**: Verificação de role admin está ausente — qualquer `ClientUser` autenticado pode acessar estas rotas. Documentado em `plan.md §Constitution Check §V. Security` e marcado com comentário `# TODO` em `backend/app/routes/admin.py`.
|
||||
|
||||
**Checkpoint Phase 9 (US7+US8)**: Verificado no Checkpoint Phase 2. `POST /api/v1/admin/boletos` cria boleto; `PUT /api/v1/admin/visits/<id>/status` atualiza status.
|
||||
|
||||
---
|
||||
|
||||
## Phase 10: Polish — Integração React (App.tsx + Providers)
|
||||
|
||||
**Goal**: Conectar todos os contextos e rotas protegidas da área do cliente no `App.tsx`; garantir que `AuthProvider`, `FavoritesProvider` e `ComparisonProvider` englobam toda a árvore de rotas; adicionar `ComparisonBar` globalmente.
|
||||
|
||||
| ID | Complexidade | Deps | spec_ref |
|
||||
|----|-------------|------|----------|
|
||||
| T022 | M | T012, T013, T016, T017, T018, T019, T020, T021, Feature 005 (AuthProvider) | plan.md §App.tsx, spec.md §FR-012 |
|
||||
|
||||
- [ ] T022 Atualizar `App.tsx` para envolver toda a árvore de rotas com `<AuthProvider>` (externo), `<FavoritesProvider>` e `<ComparisonProvider>` (nesta ordem de fora para dentro); adicionar bloco de rotas `<Route path="/area-do-cliente" element={<ClientLayout />}>` com rotas filhas: `index` → `<ClientDashboardPage />`, `favoritos` → `<FavoritesPage />`, `comparar` → `<ComparisonPage />`, `visitas` → `<VisitsPage />`, `boletos` → `<BoletosPage />`; renderizar `<ComparisonBar />` após `</Routes>` dentro dos providers (já cobre T024) — `frontend/src/App.tsx`
|
||||
- **Done when**: `vite build` completa sem erros TypeScript; `/area-do-cliente` renderiza `ClientDashboardPage`; `/area-do-cliente/favoritos` renderiza `FavoritesPage`; `/area-do-cliente/comparar` renderiza `ComparisonPage`; `/area-do-cliente/visitas` renderiza `VisitsPage`; `/area-do-cliente/boletos` renderiza `BoletosPage`; usuário não autenticado acessando `/area-do-cliente` é redirecionado para `/login`; `<ComparisonBar />` está visível em qualquer rota quando há itens de comparação; `useFavorites()` funciona em qualquer componente filho; `useComparison()` funciona em qualquer componente filho.
|
||||
|
||||
**Checkpoint Phase 10 (Polish)**: `vite build` passa sem erros; fluxo end-to-end: favoritar imóvel no catálogo → acessar `/area-do-cliente/favoritos` → ver imóvel na lista → desfavoritar → imóvel removido da lista.
|
||||
|
||||
---
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
```
|
||||
Feature 005 (pré-requisito obrigatório)
|
||||
│
|
||||
├── T001 [P] ──┐
|
||||
├── T002 [P] ──┼── T004 ── T005 ── T006 ── T007 ──┐
|
||||
└── T003 [P] ──┘ T008 ──┘── T009
|
||||
│
|
||||
┌──────────────────────────────────────────┘
|
||||
│ (backend pronto — frontend independente começa em paralelo)
|
||||
│
|
||||
T010 [P] ─── T011 [P]
|
||||
│ │
|
||||
T013 ──── T014 T012 [P]
|
||||
│ │
|
||||
T023 ──────────────────┘
|
||||
│
|
||||
T016 (ClientLayout — base de todas as páginas)
|
||||
/ | | | \
|
||||
T017 T018 T019 T020 T021
|
||||
\
|
||||
T022 (App.tsx — conecta tudo + T024)
|
||||
```
|
||||
|
||||
## Estratégia de Implementação (MVP Incremental)
|
||||
|
||||
| Incremento | Tasks | Entregável verificável |
|
||||
|------------|-------|------------------------|
|
||||
| **MVP (US1)** | T001→T004→T005→T006→T007(favorites)→T009→T010→T011→T013→T014→T023→T022(parcial) | Favoritar/desfavoritar no catálogo com persistência |
|
||||
| **Incremento 1 (US2+US3)** | T016→T017→T018 | Área do cliente com dashboard e página de favoritos |
|
||||
| **Incremento 2 (US4)** | T012→T015→T019→T024→T022 | Comparação com barra flutuante e tabela |
|
||||
| **Incremento 3 (US5+US6)** | T020→T021 | Histórico de visitas e boletos |
|
||||
| **Incremento 4 (US7+US8)** | T008 (já feito) | Admin cria boletos e atualiza visitas via API |
|
||||
|
||||
---
|
||||
|
||||
## Sumário
|
||||
|
||||
| Métrica | Valor |
|
||||
|---------|-------|
|
||||
| Total de tasks | 24 |
|
||||
| Tasks backend | 9 (T001–T009) |
|
||||
| Tasks frontend | 15 (T010–T024) |
|
||||
| Tasks paralelizáveis [P] | 8 (T001, T002, T003, T010, T011, T012, T016's predecessor) |
|
||||
| User stories cobertas | 8 (US1–US8) |
|
||||
| Fases | 10 |
|
||||
| MVP mínimo (US1 only) | 12 tasks |
|
||||
Loading…
Add table
Add a link
Reference in a new issue