feat: features 025-032 - favoritos, contatos, trabalhe-conosco, area-cliente, navbar, hero-light-dark, performance-homepage
Some checks failed
CI/CD → Deploy via SSH / Build & Push Docker Images (push) Successful in 1m0s
CI/CD → Deploy via SSH / Deploy via SSH (push) Successful in 4m35s
CI/CD → Deploy via SSH / Validate HTTPS & Endpoints (push) Failing after 46s

- 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)
This commit is contained in:
MatheusAlves96 2026-04-22 22:35:17 -03:00
parent 6ef5a7a17e
commit cf5603243c
106 changed files with 11927 additions and 1367 deletions

View file

@ -0,0 +1,206 @@
# 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 US1US5 pode começar até T003 estar completo.
- [ ] T001 Criar migration Alembic para adicionar colunas `source VARCHAR(100) NULL` e `source_detail VARCHAR(255) NULL` à tabela `contact_leads` em `backend/migrations/versions/<timestamp>_add_source_to_contact_leads.py`
- **Detalhes**: usar `op.add_column('contact_leads', sa.Column('source', sa.String(100), nullable=True))` e `op.add_column('contact_leads', sa.Column('source_detail', sa.String(255), nullable=True))`; `downgrade` deve executar `op.drop_column` nas duas colunas; gerar com `alembic revision --autogenerate` e revisar o arquivo gerado.
- **Done**: `alembic upgrade head` executa sem erro; `\d contact_leads` mostra colunas `source` e `source_detail` como nullable; `alembic downgrade -1` remove as colunas sem erro.
- [ ] T002 Adicionar atributos `source` e `source_detail` ao modelo `ContactLead` em `backend/app/models/lead.py`
- **Detalhes**: adicionar `source = db.Column(db.String(100), nullable=True)` e `source_detail = db.Column(db.String(255), nullable=True)` após a coluna `message` existente; sem quebrar campos existentes (`id`, `property_id`, `name`, `email`, `phone`, `message`, `created_at`).
- **Done**: `ContactLead()` instancia sem erro; os dois campos são `None` por default; SQLAlchemy reflete corretamente no ORM após migration aplicada.
- [ ] T003 Atualizar schemas de lead em `backend/app/schemas/lead.py` para aceitar `source` e `source_detail` opcionais
- **Detalhes**: em `ContactLeadIn` (schema de entrada), adicionar `source: Optional[str] = None` e `source_detail: Optional[str] = None`; em `ContactLeadCreatedOut` (schema de saída), adicionar `source: Optional[str] = None` e `source_detail: Optional[str] = None`; importar `Optional` de `typing` se 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.
**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) em `backend/app/routes/properties.py` ou em novo arquivo `backend/app/routes/contact.py`
- **Detalhes**: aceitar body `ContactLeadIn` via `request.get_json()`; validar com Pydantic; criar `ContactLead(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 com `db.session.add` + `db.session.commit()`; retornar `ContactLeadCreatedOut.model_validate(lead).model_dump()` com status 201; registrar blueprint em `backend/app/__init__.py` se novo arquivo criado.
- **Done**: `POST /api/v1/contact` com payload válido retorna 201 e `id` do lead criado; lead salvo no banco com `source="contato"` e `property_id=NULL`; payload sem `name` retorna 422.
- [ ] T005 [P] Atualizar rota `GET /admin/leads` em `backend/app/routes/admin.py` para filtrar por `?source=`
- **Detalhes**: após o filtro existente `?property_id`, adicionar `source = request.args.get("source")`; se `source` não é None nem string vazia, aplicar `query = query.filter(ContactLead.source == source)`; manter retorno de todos os leads quando `source` não é passado; incluir os campos `source` e `source_detail` na serialização de saída (via schema ou `lead.__dict__`).
- **Done**: `GET /admin/leads` sem parâmetros retorna todos os leads; `GET /admin/leads?source=imovel` retorna somente leads com `source="imovel"`; `GET /admin/leads?source=contato` retorna somente leads com `source="contato"`; leads com `source=NULL` aparecem em "Todos" mas não nos filtros específicos.
**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)` em `frontend/src/services/properties.ts`
- **Detalhes**: exportar `async function contactGeneral(data: { name: string; email: string; phone: string; subject: string; message: string }): Promise<void>`; fazer `POST /api/v1/contact` com 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.
- [ ] 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); estado `loading: boolean`, `success: boolean`, `errors: Record<string, string>`; validação client-side antes do submit (todos obrigatórios + formato de e-mail); ao submeter, chamar `contactGeneral(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.
- [ ] T008 [US1] Registrar rota `/contato` em `frontend/src/App.tsx`
- **Detalhes**: importar `ContactPage` de `./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 `/contato` renderiza `ContactPage`; rotas existentes não quebram.
**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); estado `loading`, `success`, `errors`; ao submeter, construir `source_detail` como `"${propertyType} • ${purpose} • ${address}"` e enviar para `POST /api/v1/contact` com `{ 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"` e `source_detail` identificável; confirmação aparece após sucesso.
- [ ] T010 [US2] Registrar rota `/cadastro-residencia` em `frontend/src/App.tsx`
- **Detalhes**: importar `CadastroResidenciaPage` de `./pages/CadastroResidenciaPage`; adicionar `<Route path="/cadastro-residencia" element={<CadastroResidenciaPage />} />` nas rotas públicas; não alterar outras rotas.
- **Done**: navegar para `/cadastro-residencia` renderiza `CadastroResidenciaPage`; rotas existentes não quebram.
**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); estado `leads`, `loading`, `error`, `sourceFilter: string` (default `""`); quando `sourceFilter` nã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 (formatada `dd/MM/yyyy`); badge "Imóvel" exibe `source_detail` como subtitle ou tooltip; ordenar por `created_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_detail` visível para leads de imóvel; data formatada; estado de loading e erro tratados.
- [ ] T012 [US4] Adicionar item "Leads" ao menu lateral em `frontend/src/layouts/AdminLayout.tsx`
- **Detalhes**: localizar o array/lista de `navLinks` ou itens de menu do `AdminLayout`; 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.
- [ ] T013 [US4] Registrar rota `/admin/leads` em `frontend/src/App.tsx`
- **Detalhes**: importar `AdminLeadsPage` de `./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/leads` como admin autenticado renderiza `AdminLeadsPage` dentro do `AdminLayout`; não autenticado redireciona para login.
**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>/contact` em `backend/app/routes/properties.py` para salvar `source` e `source_detail`
- **Detalhes**: após criar o objeto `data = ContactLeadIn(**request.get_json())`, ao instanciar `ContactLead`, definir `source = data.source or "imovel"` e `source_detail = data.source_detail`; o campo `property_id` já é definido pela slug da rota; o resto do handler permanece idêntico.
- **Done**: `POST /properties/<slug>/contact` salva `source="imovel"` quando o frontend não passa `source`; quando o frontend passa `source`, usa o valor recebido; `property_id` continua sendo preenchido pela slug.
- [ ] T015 [P] [US3] Atualizar a função `contactProperty` em `frontend/src/services/properties.ts` para aceitar e enviar `source` e `source_detail`
- **Detalhes**: adicionar `source?: string` e `source_detail?: string` ao tipo do parâmetro `data` (ou ao tipo `ContactData` se 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 sem `source`/`source_detail` continuam funcionando (campos opcionais).
- [ ] T016 [US3] Atualizar `frontend/src/pages/PropertyDetailPage.tsx` para passar `source` e `source_detail` ao chamar `contactProperty`
- **Detalhes**: localizar a chamada existente a `contactProperty(slug, data)` no handler de submit do formulário; adicionar `source: "imovel"` e `source_detail: property?.title ?? ""` ao objeto `data` passado; nenhuma mudança visual ou nos campos do formulário.
- **Done**: ao submeter o formulário de contato em `/imoveis/<slug>`, o lead criado tem `source="imovel"` e `source_detail` com o título do imóvel; a aparência do formulário é idêntica.
**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" de `href="/#sobre"` (ou `to="/#sobre"`) para `to="/sobre"`; alterar a entrada "Contato" de `href="/#contato"` (ou `to="/#contato"`) para `to="/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 `/contato` sem reload; clicar "Sobre" navega para `/sobre` sem reload; o link ativo é destacado pelo mecanismo existente do NavLink.
**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 `/contato` e `/cadastro-residencia` em viewport mobile (< 768px) ajustar classes Tailwind se necessário em `frontend/src/pages/ContactPage.tsx` e `frontend/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 em `frontend/src/pages/admin/AdminLeadsPage.tsx`
- **Detalhes**: o badge de origem deve exibir "Desconhecida" (cinza) quando `source` é `null` ou `undefined`; nenhum crash ou `undefined` visível na UI.
- **Done**: leads sem `source` exibem badge cinza "Desconhecida"; filtros "Contato", "Imóvel" e "Cadastro de Residência" não incluem esses leads; filtro "Todos" os inclui.
---
## 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.