# 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/_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": ""}`, cria `SavedProperty`, retorna 201 — ou 409 `{"error": "Já adicionado aos favoritos"}` se registro duplicado; `DELETE /favorites/` → 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/` 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//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//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` → `GET /api/v1/me/favorites`; `addFavorite(propertyId: string): Promise` → `POST /api/v1/me/favorites`; `removeFavorite(propertyId: string): Promise` → `DELETE /api/v1/me/favorites/`; `getVisits(): Promise` → `GET /api/v1/me/visits`; `getBoletos(): Promise` → `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/`; 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` — 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; `` 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 `` para a página filha; rota protegida — se `!isAuthenticated` redireciona para `/login` via `` — `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; `` 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 `` — `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 `` no `App.tsx` fora do bloco `` (após ``), 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/` 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//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//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 `` (externo), `` e `` (nesta ordem de fora para dentro); adicionar bloco de rotas `}>` com rotas filhas: `index` → ``, `favoritos` → ``, `comparar` → ``, `visitas` → ``, `boletos` → ``; renderizar `` após `` 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`; `` 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 |