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

33 KiB
Raw Permalink Blame History

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 userClientUser (lazy="joined") e propertyProperty (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 userClientUser (lazy="select") e propertyProperty (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 userClientUser (lazy="select") e propertyProperty (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 Alembicbackend/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 VisitRequestOutbackend/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/apifrontend/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.tsxfrontend/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