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
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