sass-imobiliaria/.specify/features/006-client-area/tasks.md

294 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 (US1US8)
- 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 T001T005 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 §US2US6 |
| 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-005FR-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 (T001T009) |
| Tasks frontend | 15 (T010T024) |
| Tasks paralelizáveis [P] | 8 (T001, T002, T003, T010, T011, T012, T016's predecessor) |
| User stories cobertas | 8 (US1US8) |
| Fases | 10 |
| MVP mínimo (US1 only) | 12 tasks |