33 KiB
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
SavedPropertycom colunas:id(UUID PK,default=uuid4),user_id(UUID NOT NULL, FK →client_users.idondelete="CASCADE"),property_id(UUID NULL, FK →properties.idondelete="SET NULL"),created_at(DateTime NOT NULL,server_default=func.now()); constraint únicauq_saved_property_user_property (user_id, property_id); relacionamentosuser→ClientUser(lazy="joined") eproperty→Property(lazy="joined") —backend/app/models/saved_property.py- Done when:
from app.models.saved_property import SavedPropertyimporta sem erro;SavedProperty.__tablename__ == "saved_properties";SavedProperty.user_id.property.foreign_keysaponta paraclient_users.id;SavedProperty.property_idtemnullable=Truee FK paraproperties.id;flask db migratedetecta o novo modelo.
- Done when:
-
T002 [P] Criar modelo SQLAlchemy
VisitRequestcom colunas:id(UUID PK,default=uuid4),user_id(UUID NULL, FK →client_users.idondelete="SET NULL"),property_id(UUID NULL, FK →properties.idondelete="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()); relacionamentosuser→ClientUser(lazy="select") eproperty→Property(lazy="joined") —backend/app/models/visit_request.py- Done when:
from app.models.visit_request import VisitRequestimporta sem erro;VisitRequest.__tablename__ == "visit_requests";VisitRequest.status.default.arg == "pending";VisitRequest.scheduled_attemnullable=True;VisitRequest.messagetemnullable=False.
- Done when:
-
T003 [P] Criar modelo SQLAlchemy
Boletocom colunas:id(UUID PK,default=uuid4),user_id(UUID NULL, FK →client_users.idondelete="SET NULL"),property_id(UUID NULL, FK →properties.idondelete="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()); relacionamentosuser→ClientUser(lazy="select") eproperty→Property(lazy="joined") —backend/app/models/boleto.py- Done when:
from app.models.boleto import Boletoimporta sem erro;Boleto.__tablename__ == "boletos";Boleto.amounté instância dedb.Numeric(12, 2);Boleto.urltemnullable=True;Boleto.due_dateusadb.Date.
- Done when:
-
T004 Importar
SavedProperty,VisitRequesteBoletoembackend/app/models/__init__.pypara 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, Boletoimporta sem erro;flask db migrateexecutado em branco não reporta novas tabelas (confirmando que os modelos já estão registrados no metadata do SQLAlchemy).
- Done when:
-
T005 Gerar e revisar migration Alembic que cria as tabelas
saved_properties,visit_requestseboletoscom todas as colunas, foreign keys ON DELETE e constraint única conformedata-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 confirmaop.create_table("saved_properties", ...),op.create_table("visit_requests", ...)eop.create_table("boletos", ...)com todas as colunas listadas emdata-model.md;op.create_unique_constraint("uq_saved_property_user_property", "saved_properties", ["user_id", "property_id"])está presente;flask db upgradeexecuta sem erro;flask db downgrade -1reverte sem erro;flask db upgradere-aplica sem erro.
- Done when:
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 commodel_config = ConfigDict(from_attributes=True)—backend/app/schemas/client_area.py- Done when:
from app.schemas.client_area import FavoriteOut, VisitRequestOut, BoletoOut, CreateBoletoIn, UpdateVisitStatusInimporta sem erro;CreateBoletoIn(user_id=uuid4(), description="X", amount=Decimal("100"), due_date=date.today())valida sem erro;CreateBoletoIn(..., amount=Decimal("-1"), ...)levantaValidationError;UpdateVisitStatusIn(status="invalid")levantaValidationError;UpdateVisitStatusIn(status="confirmed")valida sem erro.
- Done when:
-
T007 Criar blueprint
client_bpcom prefixo/api/v1/me; todos os endpoints decorados com@require_auth(Feature 005):GET /favorites→ filtraSavedPropertyporuser_id = g.current_user_id, retorna lista deFavoriteOut(200), incluiproperty=Nonepara imóveis deletados;POST /favorites→ aceita{"property_id": "<uuid>"}, criaSavedProperty, retorna 201 — ou 409{"error": "Já adicionado aos favoritos"}se registro duplicado;DELETE /favorites/<property_id>→ removeSavedPropertydo usuário, retorna 204 — ou 404{"error": "Favorito não encontrado"}se não existir;GET /visits→ retorna lista deVisitRequestOutdo usuário ordenada porcreated_at DESC(200);GET /boletos→ retorna lista deBoletoOutdo usuário ordenada pordue_date ASC(200) —backend/app/routes/client_area.py- Done when:
from app.routes.client_area import client_bpimporta sem erro;GET /api/v1/me/favoritessem token retorna 401; com token válido retorna 200 + lista JSON;POST /api/v1/me/favoritescomproperty_idválido retorna 201; segunda POST com mesmoproperty_idretorna 409;DELETE /api/v1/me/favorites/<id>com id não favoritado retorna 404; com id favoritado retorna 204;GET /api/v1/me/visitsretorna 200 + lista ordenada porcreated_at DESC;GET /api/v1/me/boletosretorna 200 + lista ordenada pordue_date ASC.
- Done when:
-
T008 Criar blueprint
admin_bpcom 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→ validaCreateBoletoIn, buscaClientUserporuser_id(retorna 404{"error": "Cliente não encontrado"}se inexistente), persisteBoleto, retorna 201 comBoletoOut;PUT /visits/<id>/status→ validaUpdateVisitStatusIn, buscaVisitRequestporid(retorna 404{"error": "Visita não encontrada"}se inexistente), atualizastatusescheduled_at, retorna 200 comVisitRequestOut—backend/app/routes/admin.py- Done when:
from app.routes.admin import admin_bpimporta sem erro;POST /api/v1/admin/boletoscom campos obrigatórios retorna 201 comid,description,amount,status="pending";user_idinexistente retorna 404{"error": "Cliente não encontrado"}; campos obrigatórios ausentes retornam 422;PUT /api/v1/admin/visits/<uuid>/statuscom{"status": "confirmed", "scheduled_at": "2026-05-01T10:00:00"}retorna 200 comidestatus="confirmed"; id inexistente retorna 404;status="invalido"retorna 422.
- Done when:
-
T009 Registrar
client_bpeadmin_bpna factorycreate_app()comapp.register_blueprint(client_bp)eapp.register_blueprint(admin_bp)—backend/app/__init__.py- Done when: Flask inicia sem erros após a alteração;
GET /api/v1/me/favoritessem token retorna 401 (rota existe);POST /api/v1/admin/boletossem token retorna 401 (rota existe);flask routeslista as rotasclient_bp.*eadmin_bp.*.
- Done when: Flask inicia sem erros após a alteração;
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.statusaceita apenas os 4 literais;Boleto.statusaceita apenas "pending" | "paid" | "overdue";Boleto.urléstring | null;ComparisonState.propertiesusa o tipoPropertyde@/types/property.
- Done when:
-
T011 [P] [US1] Criar
frontend/src/services/clientArea.tsexportando: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;addFavoriteenviaPOST /api/v1/me/favoritescom{ property_id: id }no body;removeFavoriteenviaDELETE /api/v1/me/favorites/<id>; build TypeScript sem erros.
- Done when:
-
T013 [US1] Criar
FavoritesContextcomFavoritesProviderque: ao montar, verifica autenticação viaAuthContexte, se autenticado, carrega favoritos viagetFavorites()armazenandofavoriteIds: string[]; expõeisFavorite(id: string): booleanetoggleFavorite(id: string): Promise<void>— quando não autenticadotoggleFavoriteredireciona para/loginviauseNavigatesem chamar a API; quando autenticado, chamaaddFavoriteouremoveFavoriteconforme estado atual e atualizafavoriteIdslocalmente —frontend/src/contexts/FavoritesContext.tsx- Done when:
import { useFavorites, FavoritesProvider } from '@/contexts/FavoritesContext'compila sem erro TypeScript;isFavorite(id)retornatruepara id presente emfavoriteIds;toggleFavorite(id)dispararemoveFavoritequando já favoritado eaddFavoritequando não favoritado; usuário não autenticado ao chamartoggleFavoriteé navegado para/loginsem chamada à API; build TypeScript sem erros.
- Done when:
-
T014 [US1] Criar componente funcional
HeartButtonrecebendopropertyId: string; usauseFavorites()para obterisFavorite(propertyId)etoggleFavorite; exibe SVG de coração preenchido (favoritado, cor#7170ff) ou vazio (não favoritado, cortext-gray-400) com Tailwind; exibe spinner ou opacidade reduzida durante loading da operação; temaria-labeldinâmico ("Adicionar aos favoritos" / "Remover dos favoritos");onClickchamatoggleFavorite(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óstoggleFavorite;aria-labelreflete o estado; build TypeScript sem erros.
- Done when:
-
T023 [US1] Adicionar
HeartButtonao canto superior direito da imagem doPropertyCard.tsx(sobreposição comabsolute top-2 right-2); adicionar botão "Comparar" / "Remover da comparação" ao lado doHeartButtonusandouseComparison()para chamartoggleComparison(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 dePropertyDetailPage.tsx—frontend/src/components/PropertyCard.tsxefrontend/src/pages/PropertyDetailPage.tsx- Done when:
PropertyCardexibeHeartButtone botão Comparar sobrepostos na imagem sem quebrar layout; clicar no coração persiste favorito viaFavoritesContext; clicar em "Comparar" adiciona aoComparisonContext;PropertyDetailPageexibe ambos os botões; botão Comparar exibe "Remover da comparação" quando imóvel já está na lista; botão Comparar comdisablede tooltip explicativo ao tentar adicionar 4º item; build TypeScript sem erros.
- Done when:
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
ClientLayoutcom 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 sidebarbg-[#0f1011], textotext-[#e2e2e2], item ativo com borda/fundo#5e6ad2; renderiza<Outlet />para a página filha; rota protegida — se!isAuthenticatedredireciona para/loginvia<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.
- Done when:
-
T018 [US2] Criar
FavoritesPageque fetchagetFavorites()no mount; exibe grade dePropertyCardcomHeartButtonpara cada favorito; ao desfavoritar um item viatoggleFavorite, remove o card da lista localmente (re-fetch ou filtro porfavoriteIds); 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.
- Done when:
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
ClientDashboardPageque ao montar fetcha em paralelogetFavorites(),getVisits()egetBoletos(); 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 viaLinkpara 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.
- Done when:
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
ComparisonContextcomComparisonProviderque: persisteids: string[]emlocalStoragesob chavecomparison_idseproperties: Property[]sobcomparison_properties; restaura estado do localStorage na inicialização (ignora silenciosamente ids cujos dados não estejam disponíveis); expõecomparisonItems: Property[],isInComparison(id: string): boolean,toggleComparison(property: Property): void— quando lista tem 3 itens e o imóvel não está nela, exibealertoutoast"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)retornatrue; segundotoggleComparison(p1)remove; ao ter 3 items,toggleComparison(p4)não modifica o array e exibe feedback;localStorageatualizado após cada operação; reload da página restaura os items;clearComparison()limpa array e localStorage; build TypeScript sem erros.
- Done when:
-
T015 [US4] Criar
ComparisonBarbarra flutuante renderizada no rodapé quandocomparisonItems.length > 0: posiçãofixed bottom-0 left-0 right-0 z-50; fundobg-[#0f1011]com borda superiorborder-t border-[#5e6ad2]; exibe thumbnails (foto + título truncado) dos imóveis selecionados; botão "×" por thumbnail chamatoggleComparison(item)para remover; contador "N imóvel(is)"; botão "Ver Comparação" navega para/area-do-cliente/comparar; botão "Limpar" chamaclearComparison(); 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.
- Done when:
-
T019 [US4] Criar
ComparisonPageem/area-do-cliente/comparar: quandocomparisonItems.length > 0exibe 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 chamatoggleComparisone remove a coluna; estado vazio exibe mensagem com link para/imoveis; build TypeScript sem erros.
- Done when:
-
T024 [US4] Renderizar
<ComparisonBar />noApp.tsxfora do bloco<Routes>(após</Routes>), dentro dos providersComparisonProvider, 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;ComparisonBarnão aparece quando lista de comparação está vazia; build TypeScript sem erros (esta task é executada junto com T022).
- Done when:
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
VisitsPageque fetchagetVisits()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" quandopropertyfor null), mensagem enviada, badge de status colorido (pending=cinza/azul,confirmed=verde,cancelled=vermelho,completed=roxo) e data agendada formatada comoDD/MM/YYYY HH:mmquandoscheduled_atnã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=nullexibe "Imóvel removido" sem erro;scheduled_atformatado quando presente; estado vazio exibe "Nenhuma visita agendada"; build TypeScript sem erros.
- Done when:
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
BoletosPageque fetchagetBoletos()e exibe tabela com colunas: Imóvel (título dopropertyquando vinculado, ou "—"), Descrição, Valor (formatado como BRL, ex:R$ 3.500,00), Vencimento (formatado comoDD/MM/YYYY), Status (badge:pending=amarelo,paid=verde,overdue=vermelho), Ação (botão "Acessar Boleto" comtarget="_blank"erel="noopener noreferrer"— desabilitado/oculto quandourlé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" temtarget="_blank"erel="noopener noreferrer"; botão desabilitado quandourl=null; badge "Pago" exibe cor verde parastatus="paid"; estado vazio exibe "Nenhum boleto disponível"; build TypeScript sem erros.
- Done when:
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.tsxpara 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 buildcompleta sem erros TypeScript;/area-do-clienterenderizaClientDashboardPage;/area-do-cliente/favoritosrenderizaFavoritesPage;/area-do-cliente/compararrenderizaComparisonPage;/area-do-cliente/visitasrenderizaVisitsPage;/area-do-cliente/boletosrenderizaBoletosPage; 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.
- Done when:
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 |