- feat(025): favoritos locais com FavoritesContext, HeartButton, PublicFavoritesPage
- feat(026): central de contatos admin (leads/contatos unificados)
- feat(027): configuração da página de contato via admin
- feat(028): trabalhe conosco - candidaturas com upload e admin
- feat(029): UX área do cliente - visitas, comparação, perfil
- feat(030): navbar UX - menu mobile, ThemeToggle, useFavorites
- feat(031): hero light/dark - imagens separadas por tema, upload, preview, seed
- feat(032): performance homepage - Promise.all parallel fetches, sessionStorage cache,
preload hero image, loading=lazy nos cards, useInView hook, will-change carrossel,
keyframes em index.css, AgentsCarousel e HomeScrollScene via props
- fix: light mode HomeScrollScene - gradiente, cores de texto, scroll hint
migrations: g1h2i3j4k5l6 (source em leads), h1i2j3k4l5m6 (contact_config),
i1j2k3l4m5n6 (job_applications), j2k3l4m5n6o7 (hero theme images)
19 KiB
Tasks: Central de Contatos com Rastreamento de Origem
Input: Design documents from /specs/026-central-contatos/
Prerequisites: spec.md ✅ · contexto técnico do autor ✅
Branch: 026-central-contatos
Format: [ID] [P?] [Story?] Description
- [P]: pode ser executada em paralelo (arquivos distintos, sem dependência de tarefa incompleta)
- [Story]: a qual user story pertence (
US1,US2,US3,US4,US5) - Caminhos exatos incluídos em cada tarefa
Phase 1: Foundational — Modelo e Esquemas de Lead
Purpose: Adicionar os campos source e source_detail ao banco e às camadas Python. Este é o pré-requisito de todas as user stories — nenhuma pode ser implementada sem estes dados disponíveis.
⚠️ BLOQUEANTE para todas as US: nenhuma tarefa de US1–US5 pode começar até T003 estar completo.
-
T001 Criar migration Alembic para adicionar colunas
source VARCHAR(100) NULLesource_detail VARCHAR(255) NULLà tabelacontact_leadsembackend/migrations/versions/<timestamp>_add_source_to_contact_leads.py- Detalhes: usar
op.add_column('contact_leads', sa.Column('source', sa.String(100), nullable=True))eop.add_column('contact_leads', sa.Column('source_detail', sa.String(255), nullable=True));downgradedeve executarop.drop_columnnas duas colunas; gerar comalembic revision --autogeneratee revisar o arquivo gerado. - Done:
alembic upgrade headexecuta sem erro;\d contact_leadsmostra colunassourceesource_detailcomo nullable;alembic downgrade -1remove as colunas sem erro.
- Detalhes: usar
-
T002 Adicionar atributos
sourceesource_detailao modeloContactLeadembackend/app/models/lead.py- Detalhes: adicionar
source = db.Column(db.String(100), nullable=True)esource_detail = db.Column(db.String(255), nullable=True)após a colunamessageexistente; sem quebrar campos existentes (id,property_id,name,email,phone,message,created_at). - Done:
ContactLead()instancia sem erro; os dois campos sãoNonepor default; SQLAlchemy reflete corretamente no ORM após migration aplicada.
- Detalhes: adicionar
-
T003 Atualizar schemas de lead em
backend/app/schemas/lead.pypara aceitarsourceesource_detailopcionais- Detalhes: em
ContactLeadIn(schema de entrada), adicionarsource: Optional[str] = Noneesource_detail: Optional[str] = None; emContactLeadCreatedOut(schema de saída), adicionarsource: Optional[str] = Noneesource_detail: Optional[str] = None; importarOptionaldetypingse ainda não importado. - Done:
ContactLeadIn(name="X", email="x@x.com", phone="99", message="msg")valida sem erro (campos opcionais ausentes);ContactLeadIn(..., source="imovel", source_detail="Apto Centro")também valida; schema de saída serializa os dois campos.
- Detalhes: em
Checkpoint Phase 1: alembic upgrade head OK; modelo ORM com os dois novos campos; schemas Pydantic validando opcionalmente. Base para todas as demais fases.
Phase 2: Foundational — Endpoints Backend
Purpose: Expor o endpoint público de contato geral (POST /contact) e atualizar o roteamento do admin para filtrar por source. Estes dois endpoints são pré-requisitos de US1 e US4, respectivamente, e podem ser implementados em paralelo após Phase 1.
-
T004 Criar endpoint
POST /api/v1/contact(contato geral sem property) embackend/app/routes/properties.pyou em novo arquivobackend/app/routes/contact.py- Detalhes: aceitar body
ContactLeadInviarequest.get_json(); validar com Pydantic; criarContactLead(property_id=None, source="contato", source_detail=None, **data.model_dump(exclude={"source","source_detail"}), name=data.name, email=data.email, phone=data.phone, message=data.message); persistir comdb.session.add+db.session.commit(); retornarContactLeadCreatedOut.model_validate(lead).model_dump()com status 201; registrar blueprint embackend/app/__init__.pyse novo arquivo criado. - Done:
POST /api/v1/contactcom payload válido retorna 201 eiddo lead criado; lead salvo no banco comsource="contato"eproperty_id=NULL; payload semnameretorna 422.
- Detalhes: aceitar body
-
T005 [P] Atualizar rota
GET /admin/leadsembackend/app/routes/admin.pypara filtrar por?source=- Detalhes: após o filtro existente
?property_id, adicionarsource = request.args.get("source"); sesourcenão é None nem string vazia, aplicarquery = query.filter(ContactLead.source == source); manter retorno de todos os leads quandosourcenão é passado; incluir os campossourceesource_detailna serialização de saída (via schema oulead.__dict__). - Done:
GET /admin/leadssem parâmetros retorna todos os leads;GET /admin/leads?source=imovelretorna somente leads comsource="imovel";GET /admin/leads?source=contatoretorna somente leads comsource="contato"; leads comsource=NULLaparecem em "Todos" mas não nos filtros específicos.
- Detalhes: após o filtro existente
Checkpoint Phase 2: curl POST /api/v1/contact retorna 201 ✓; curl GET /admin/leads?source=contato filtra corretamente ✓.
Phase 3: US1 — Página de Contato Geral /contato (Priority: P1) 🎯 MVP
Goal: Disponibilizar página pública /contato com formulário funcional que cria leads com source = "contato".
Independent Test: Acessar /contato sem autenticação, preencher nome, e-mail, telefone, assunto e mensagem e submeter. Verificar mensagem de confirmação na tela e lead salvo no banco com source = "contato".
Dependências: T004 (endpoint POST /api/v1/contact) deve estar completo.
-
T006 [US1] Criar função
contactGeneral(data)emfrontend/src/services/properties.ts- Detalhes: exportar
async function contactGeneral(data: { name: string; email: string; phone: string; subject: string; message: string }): Promise<void>; fazerPOST /api/v1/contactcom axios passando{ ...data, source: "contato" }; lançar erro em caso de resposta não-2xx. - Done: função exportada e tipada; chama o endpoint correto com
source: "contato"no payload.
- Detalhes: exportar
-
T007 [US1] Criar
frontend/src/pages/ContactPage.tsx- Detalhes: campos controlados:
name(text, obrigatório),email(email, obrigatório, validar formato com regex),phone(tel, obrigatório),subject(select com opções: "Informações", "Anúncio", "Parceria", "Outro", obrigatório),message(textarea, obrigatório); estadoloading: boolean,success: boolean,errors: Record<string, string>; validação client-side antes do submit (todos obrigatórios + formato de e-mail); ao submeter, chamarcontactGeneral(data); em sucesso, exibir mensagem de confirmação inline e desabilitar/resetar o formulário; em erro do servidor, exibir alerta de erro genérico; estilizar com Tailwind CSS consistente com o restante do projeto. - Done: formulário renderiza; validação destaca campos obrigatórios ausentes sem submeter; e-mail inválido exibe erro inline; submissão bem-sucedida mostra confirmação e bloqueia re-envio; submissão com erro de servidor mostra feedback de erro.
- Detalhes: campos controlados:
-
T008 [US1] Registrar rota
/contatoemfrontend/src/App.tsx- Detalhes: importar
ContactPagede./pages/ContactPage; adicionar<Route path="/contato" element={<ContactPage />} />dentro do bloco de rotas públicas existente (dentro de<Routes>); não alterar nenhuma outra rota. - Done: navegar para
/contatorenderizaContactPage; rotas existentes não quebram.
- Detalhes: importar
Checkpoint US1: Acessar /contato → página renderiza ✓. Submeter com todos os campos → confirmação aparece ✓. Submeter sem nome → erro inline ✓. Lead salvo com source="contato" ✓.
Phase 4: US2 — Página de Cadastro de Residência /cadastro-residencia (Priority: P1)
Goal: Disponibilizar página pública /cadastro-residencia para proprietários interessados em anunciar imóvel, criando lead com source = "cadastro_residencia".
Independent Test: Acessar /cadastro-residencia, preencher todos os campos obrigatórios (nome, e-mail, telefone, endereço, tipo, finalidade) e submeter. Verificar lead criado com source = "cadastro_residencia" e source_detail contendo identificação do imóvel.
Dependências: T004 (endpoint POST /api/v1/contact) deve estar completo.
-
T009 [US2] Criar
frontend/src/pages/CadastroResidenciaPage.tsx- Detalhes: campos controlados:
name(text, obrigatório),email(email, obrigatório, validar formato),phone(tel, obrigatório),address(text, obrigatório),propertyType(select: "Casa", "Apartamento", "Comercial", obrigatório),area(number opcional, validar que é positivo quando preenchido),purpose(select: "Venda", "Aluguel", obrigatório),notes(textarea, opcional); estadoloading,success,errors; ao submeter, construirsource_detailcomo"${propertyType} • ${purpose} • ${address}"e enviar paraPOST /api/v1/contactcom{ name, email, phone, message: notes || "(sem observações)", source: "cadastro_residencia", source_detail }; exibir confirmação em sucesso; estilizar com Tailwind CSS. - Done: formulário renderiza todos os campos; campos obrigatórios ausentes são destacados; área não-numérica ou negativa exibe erro inline; submissão cria lead com
source="cadastro_residencia"esource_detailidentificável; confirmação aparece após sucesso.
- Detalhes: campos controlados:
-
T010 [US2] Registrar rota
/cadastro-residenciaemfrontend/src/App.tsx- Detalhes: importar
CadastroResidenciaPagede./pages/CadastroResidenciaPage; adicionar<Route path="/cadastro-residencia" element={<CadastroResidenciaPage />} />nas rotas públicas; não alterar outras rotas. - Done: navegar para
/cadastro-residenciarenderizaCadastroResidenciaPage; rotas existentes não quebram.
- Detalhes: importar
Checkpoint US2: Acessar /cadastro-residencia → página renderiza ✓. Submeter com todos os campos → lead criado com source="cadastro_residencia" ✓. Área negativa → erro inline ✓.
Phase 5: US4 — Central de Leads no Painel Admin (Priority: P1)
Goal: Página /admin/leads com listagem de todos os leads ordenada por data decrescente e filtro por origem (badge colorido por source).
Independent Test: Autenticar como admin, acessar /admin/leads, verificar leads das três origens listados. Aplicar filtro "Cadastro de Residência" e confirmar que apenas source = "cadastro_residencia" aparece.
Dependências: T005 (GET /admin/leads?source=) deve estar completo.
-
T011 [US4] Criar
frontend/src/pages/admin/AdminLeadsPage.tsx- Detalhes: ao montar, buscar
GET /api/v1/admin/leads(com token de autenticação via axios interceptor existente); estadoleads,loading,error,sourceFilter: string(default""); quandosourceFilternão vazio, refazer fetch com?source=${sourceFilter}; renderizar barra de filtros com botões/tabs: "Todos", "Contato" (source=contato), "Imóvel" (source=imovel), "Cadastro de Residência" (source=cadastro_residencia); tabela com colunas: Origem (badge colorido por source — contato: azul, imovel: verde, cadastro_residencia: laranja, null/desconhecido: cinza), Nome, E-mail, Telefone, Prévia da mensagem (truncada em 60 chars), Data (formatadadd/MM/yyyy); badge "Imóvel" exibesource_detailcomo subtitle ou tooltip; ordenar porcreated_at DESC; exibir estado vazio ("Nenhum lead encontrado") quando lista vazia; suportar paginação básica (se a listagem retornar campo de total, exibir link "carregar mais" ou navegação por páginas). - Done: página renderiza leads de todas as origens; filtro por source recarrega a lista; badge colorido por origem;
source_detailvisível para leads de imóvel; data formatada; estado de loading e erro tratados.
- Detalhes: ao montar, buscar
-
T012 [US4] Adicionar item "Leads" ao menu lateral em
frontend/src/layouts/AdminLayout.tsx- Detalhes: localizar o array/lista de
navLinksou itens de menu doAdminLayout; adicionar entrada{ label: "Leads", path: "/admin/leads" }(ou equivalente JSX<NavLink to="/admin/leads">Leads</NavLink>) após o item existente que melhor se encaixa na ordem do menu (ex.: após "Imóveis" ou "Configurações"); não remover nem reordenar itens existentes. - Done: menu lateral exibe item "Leads"; clicar navega para
/admin/leads; link ativo é destacado pelo mecanismo existente.
- Detalhes: localizar o array/lista de
-
T013 [US4] Registrar rota
/admin/leadsemfrontend/src/App.tsx- Detalhes: importar
AdminLeadsPagede./pages/admin/AdminLeadsPage; adicionar<Route path="/admin/leads" element={<AdminLeadsPage />} />dentro do bloco de rotas protegidas de admin (dentro do layout de admin existente); não alterar outras rotas. - Done: navegar para
/admin/leadscomo admin autenticado renderizaAdminLeadsPagedentro doAdminLayout; não autenticado redireciona para login.
- Detalhes: importar
Checkpoint US4: Acessar /admin/leads autenticado → listagem completa ✓. Filtrar por "Imóvel" → somente source="imovel" ✓. Badge colorido por origem ✓.
Phase 6: US3 — Rastreamento de Origem no Formulário de Imóvel (Priority: P2)
Goal: O formulário de contato existente na página de detalhes do imóvel passa automaticamente source = "imovel" e source_detail = property.title sem alterar a experiência visual do usuário.
Independent Test: Acessar página de detalhes de um imóvel, enviar formulário de contato. Verificar no banco que o lead criado tem source = "imovel" e source_detail com o título do imóvel.
Dependências: T003 (schemas atualizados), T001/T002 (migration e modelo), mais T004 não é necessário — a rota de imóvel já existe. As tasks de frontend dependem de T003.
-
T014 [US3] Atualizar rota
POST /properties/<slug>/contactembackend/app/routes/properties.pypara salvarsourceesource_detail- Detalhes: após criar o objeto
data = ContactLeadIn(**request.get_json()), ao instanciarContactLead, definirsource = data.source or "imovel"esource_detail = data.source_detail; o campoproperty_idjá é definido pela slug da rota; o resto do handler permanece idêntico. - Done:
POST /properties/<slug>/contactsalvasource="imovel"quando o frontend não passasource; quando o frontend passasource, usa o valor recebido;property_idcontinua sendo preenchido pela slug.
- Detalhes: após criar o objeto
-
T015 [P] [US3] Atualizar a função
contactPropertyemfrontend/src/services/properties.tspara aceitar e enviarsourceesource_detail- Detalhes: adicionar
source?: stringesource_detail?: stringao tipo do parâmetrodata(ou ao tipoContactDatase existir); incluir esses campos no payload da chamada axios existente:{ ...data, source, source_detail }. - Done:
contactProperty(slug, { name, email, phone, message, source: "imovel", source_detail: "Título" })envia o payload completo; chamadas semsource/source_detailcontinuam funcionando (campos opcionais).
- Detalhes: adicionar
-
T016 [US3] Atualizar
frontend/src/pages/PropertyDetailPage.tsxpara passarsourceesource_detailao chamarcontactProperty- Detalhes: localizar a chamada existente a
contactProperty(slug, data)no handler de submit do formulário; adicionarsource: "imovel"esource_detail: property?.title ?? ""ao objetodatapassado; nenhuma mudança visual ou nos campos do formulário. - Done: ao submeter o formulário de contato em
/imoveis/<slug>, o lead criado temsource="imovel"esource_detailcom o título do imóvel; a aparência do formulário é idêntica.
- Detalhes: localizar a chamada existente a
Checkpoint US3: Enviar formulário de contato em /imoveis/<slug> → lead com source="imovel" e source_detail=título no banco ✓. Formulário sem mudanças visuais ✓.
Phase 7: US5 — Correção dos Links da Navbar (Priority: P3)
Goal: Os links "Contato" e "Sobre" da navbar navegam para rotas internas /contato e /sobre em vez de âncoras na homepage.
Independent Test: Em qualquer página, clicar "Contato" → URL muda para /contato. Clicar "Sobre" → URL muda para /sobre. Sem reload da página inteira.
Dependências: nenhuma — tarefa completamente independente; pode ser executada a qualquer momento após a branch ser criada.
- T017 [P] [US5] Corrigir links "Sobre" e "Contato" em
frontend/src/components/Navbar.tsx- Detalhes: localizar o array
navLinks(ou equivalente); alterar a entrada com label/text "Sobre" dehref="/#sobre"(outo="/#sobre") parato="/sobre"; alterar a entrada "Contato" dehref="/#contato"(outo="/#contato") parato="/contato"; se os links usam tag<a>, converter para<Link>ou<NavLink>do react-router-dom para navegação client-side. - Done: clicar "Contato" na navbar navega para
/contatosem reload; clicar "Sobre" navega para/sobresem reload; o link ativo é destacado pelo mecanismo existente do NavLink.
- Detalhes: localizar o array
Checkpoint US5: Navbar em qualquer página: "Contato" → /contato ✓. "Sobre" → /sobre ✓. Navegação client-side (sem reload) ✓.
Polish & Cross-Cutting
Purpose: Ajustes finais de UX e consistência que dependem de todas as user stories anteriores estarem completas.
-
T018 Verificar responsividade dos formulários de
/contatoe/cadastro-residenciaem viewport mobile (< 768px) — ajustar classes Tailwind se necessário emfrontend/src/pages/ContactPage.tsxefrontend/src/pages/CadastroResidenciaPage.tsx- Done: formulários renderizam sem overflow horizontal em 375px de largura; inputs e botões têm tamanho mínimo de toque adequado.
-
T019 [P] Garantir que leads com
source = NULL(criados antes da feature) aparecem na listagem admin sem errors emfrontend/src/pages/admin/AdminLeadsPage.tsx- Detalhes: o badge de origem deve exibir "Desconhecida" (cinza) quando
sourceénullouundefined; nenhum crash ouundefinedvisível na UI. - Done: leads sem
sourceexibem badge cinza "Desconhecida"; filtros "Contato", "Imóvel" e "Cadastro de Residência" não incluem esses leads; filtro "Todos" os inclui.
- Detalhes: o badge de origem deve exibir "Desconhecida" (cinza) quando
Dependency Graph
T001 → T002 → T003 ──┬──→ T004 ──→ T006 → T007 → T008 [US1]
│ └──→ T009 → T010 [US2]
├──→ T005 [US4 backend]
├──→ T014 [US3 backend]
└──→ T015 → T016 [US3 frontend]
T005 ──→ T011 → T012 → T013 [US4 frontend]
T017 [US5 — independente]
T018, T019 ── aguardam todas as US [Polish]
Parallel Execution
Após T003 completo, as seguintes trilhas podem avançar em paralelo:
| Trilha A (US1) | Trilha B (US2) | Trilha C (US4 backend) | Trilha D (US3) | Trilha E (US5) |
|---|---|---|---|---|
| T004 → T006 → T007 → T008 | T004 → T009 → T010 | T005 | T014 + T015 → T016 | T017 |
T004 é compartilhado entre US1 e US2 — completar antes de iniciar as duas trilhas.
Implementation Strategy
MVP mínimo (US1 + US4): Phase 1 → Phase 2 → Phase 3 → Phase 5. Resultado: formulário /contato funcional + admin pode visualizar e filtrar leads por origem.
Incremento 2 (+ US2): adicionar Phase 4. Proprietários já podem cadastrar imóveis para anúncio.
Incremento 3 (+ US3): adicionar Phase 6. Rastreamento completo também para contatos originados de imóveis específicos.
Incremento 4 (+ US5 + Polish): Phase 7 + Polish. Navbar corrigida e ajustes de responsividade.