- 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)
217 lines
14 KiB
Markdown
217 lines
14 KiB
Markdown
---
|
||
description: "Task list para a feature 028 - Trabalhe Conosco"
|
||
---
|
||
|
||
# Tasks: Trabalhe Conosco (028)
|
||
|
||
**Input**: Design documents de `specs/028-trabalhe-conosco/`
|
||
**Prerequisites**: plan.md ✅ · spec.md ✅ · data-model.md ✅ · contracts/jobs-api.md ✅
|
||
|
||
## Format: `[ID] [P?] [Story?] Description — arquivo`
|
||
|
||
- **[P]**: Pode executar em paralelo (arquivo diferente, sem bloqueadores incompletos)
|
||
- **[Story]**: User story correspondente (US1, US2, US3, US4)
|
||
- Arquivo exato indicado em cada task
|
||
|
||
---
|
||
|
||
## Phase 1: Foundational — Backend (Bloqueador de tudo)
|
||
|
||
**Purpose**: Migration, model, schemas e rotas Flask precisam existir antes que qualquer integração frontend possa ser testada contra o servidor real.
|
||
|
||
**⚠️ CRÍTICO**: Nenhuma fase de user story pode começar até esta fase estar completa.
|
||
|
||
- [ ] T001 Criar migration Alembic `i1j2k3l4m5n6` em `backend/migrations/versions/i1j2k3l4m5n6_add_job_applications.py` com `down_revision = "h1i2j3k4l5m6"` — implementar `upgrade()` criando a tabela `job_applications` (9 colunas conforme data-model.md: id SERIAL PK, name VARCHAR(150) NOT NULL, email VARCHAR(254) NOT NULL, phone VARCHAR(30) NULL, role_interest VARCHAR(100) NOT NULL, message TEXT NOT NULL, file_name VARCHAR(255) NULL, status VARCHAR(50) NOT NULL server_default `'pending'`, created_at TIMESTAMP NOT NULL server_default `now()`) + 2 índices (`ix_job_applications_created_at` em `created_at`, `ix_job_applications_status` em `status`); `downgrade()` remove índices e tabela na ordem inversa
|
||
|
||
- [ ] T002 Criar modelo SQLAlchemy `JobApplication` em `backend/app/models/job_application.py` — classe com `__tablename__ = "job_applications"`, 9 colunas mapeando o schema da tabela (status com `default="pending"`, created_at com `server_default=db.func.now()`), constante `ROLE_INTEREST_OPTIONS = ["Corretor(a)", "Assistente Administrativo", "Estagiário(a)", "Outro"]` e `__repr__` com id + email
|
||
|
||
- [ ] T003 [P] Criar schemas Pydantic em `backend/app/schemas/job.py` — definir 3 classes:
|
||
- `JobApplicationIn(BaseModel)`: campos `name: str` (strip, não vazio), `email: EmailStr`, `phone: str | None = None`, `role_interest: str` (validado contra `ROLE_INTEREST_OPTIONS` via `@field_validator`), `message: str` (max_length=5000, não vazio), `file_name: str | None = None`
|
||
- `JobApplicationOut(BaseModel)`: todos os campos de `JobApplicationIn` + `id: int`, `status: str`, `created_at: datetime`; `model_config = ConfigDict(from_attributes=True)`
|
||
- `PaginatedJobApplications(BaseModel)`: `items: list[JobApplicationOut]`, `total: int`, `page: int`, `per_page: int`, `pages: int`
|
||
|
||
- [ ] T004 Criar rotas em `backend/app/routes/jobs.py` com dois blueprints:
|
||
- `jobs_public_bp = Blueprint("jobs_public", __name__)`: endpoint `POST /jobs/apply` público — valida body via `JobApplicationIn` (retorna 422 com `{"error": "Dados inválidos", "details": ...}` em ValidationError), cria e salva `JobApplication` via `db.session`, retorna `{"message": "Candidatura recebida com sucesso"}` com status 201
|
||
- `jobs_admin_bp = Blueprint("jobs_admin", __name__)`: endpoint `GET /jobs` decorado com `@require_admin` — lê query params `page` (default 1, ≥ 1) e `per_page` (default 20, clamp 1–100), consulta `JobApplication.query.order_by(JobApplication.created_at.desc()).paginate(...)`, serializa via `PaginatedJobApplications` e retorna JSON 200
|
||
|
||
- [ ] T005 Registrar model e blueprints em `backend/app/__init__.py`:
|
||
- Na seção de imports de models, adicionar `from app.models import job_application as _job_application_models`
|
||
- Registrar `jobs_public_bp` com `url_prefix="/api/v1"` e `jobs_admin_bp` com `url_prefix="/api/v1/admin"` na função `create_app()`
|
||
|
||
- [ ] T006 Aplicar migration no container e verificar schema: `docker-compose exec backend flask db upgrade` → confirmar tabela com `docker-compose exec db psql -U postgres -d saas_imobiliaria -c "\d job_applications"`
|
||
|
||
**Checkpoint**: `curl -X POST http://localhost:5000/api/v1/jobs/apply` com body válido retorna 201. `GET /api/v1/admin/jobs` sem token retorna 401.
|
||
|
||
---
|
||
|
||
## Phase 2: User Story 1 — Candidato Envia Formulário (Priority: P1) 🎯 MVP
|
||
|
||
**Goal**: Página `/trabalhe-conosco` com formulário funcional que submete via `POST /api/v1/jobs/apply`, exibe confirmação de sucesso e mantém dados em caso de erro de rede.
|
||
|
||
**Independent Test**: Acessar `/trabalhe-conosco` sem autenticação, preencher todos os campos obrigatórios (nome, e-mail, cargo de interesse, mensagem) e clicar em "Enviar Candidatura". Verificar que uma mensagem de confirmação é exibida e que `GET /api/v1/admin/jobs` (com token admin) lista a candidatura recebida.
|
||
|
||
- [ ] T007 [P] [US1] Criar interface TypeScript em `frontend/src/types/jobApplication.ts`:
|
||
```typescript
|
||
export interface JobApplicationPayload {
|
||
name: string;
|
||
email: string;
|
||
phone?: string;
|
||
role_interest: string;
|
||
message: string;
|
||
file_name?: string;
|
||
}
|
||
|
||
export const ROLE_INTEREST_OPTIONS = [
|
||
"Corretor(a)",
|
||
"Assistente Administrativo",
|
||
"Estagiário(a)",
|
||
"Outro",
|
||
] as const;
|
||
```
|
||
|
||
- [ ] T008 [P] [US1] Criar `frontend/src/services/jobsService.ts` com função `submitApplication(data: JobApplicationPayload): Promise<void>` — chama `api.post("/api/v1/jobs/apply", data)` via instância Axios do projeto e relança o erro para tratamento no componente
|
||
|
||
- [ ] T009 [US1] Criar `frontend/src/pages/JobsPage.tsx` com formulário de candidatura:
|
||
- Campos controlados com `useState`: `name`, `email`, `phone`, `role_interest` (select com `ROLE_INTEREST_OPTIONS`), `message` (textarea, contador de caracteres até 5000), `file_name` (input file decorativo — apenas registra `e.target.files?.[0]?.name`)
|
||
- Validação frontend antes do submit: e-mail formato válido, campos obrigatórios não vazios, message ≤ 5000 chars, arquivo (se presente) deve ser PDF e ≤ 2 MB
|
||
- Estado `submitting: boolean` para desabilitar o botão durante o envio
|
||
- Submit: chama `submitApplication()`, em sucesso exibe mensagem de confirmação e limpa o formulário; em erro de rede exibe mensagem genérica sem apagar os dados preenchidos
|
||
- Estilo: dark theme do projeto (`bg-panel`, `border-borderSubtle`, accent `#5e6ad2`, tipografia Inter, Tailwind CSS)
|
||
|
||
- [ ] T010 [US1] Adicionar rota `/trabalhe-conosco` em `frontend/src/App.tsx`: importar `JobsPage` e inserir `<Route path="/trabalhe-conosco" element={<JobsPage />} />` entre as rotas públicas
|
||
|
||
**Checkpoint**: Formulário em `/trabalhe-conosco` envia candidatura, recebe 201 e exibe confirmação. Erro de rede exibe mensagem sem apagar campos.
|
||
|
||
---
|
||
|
||
## Phase 3: User Story 2 — Visitante Descobre a Oportunidade (Priority: P1)
|
||
|
||
**Goal**: Links "Trabalhe Conosco" no footer (coluna "A Imobiliária") e na página `/corretores` tornam a página de candidatura descobrível organicamente.
|
||
|
||
**Independent Test**: Acessar qualquer página e verificar que o footer contém o link "Trabalhe Conosco" na coluna "A Imobiliária". Acessar `/corretores` e verificar que existe elemento com texto "Trabalhe Conosco" que navega para `/trabalhe-conosco`.
|
||
|
||
- [ ] T011 [P] [US2] Adicionar link "Trabalhe Conosco" em `frontend/src/components/Footer.tsx` — localizar a coluna "A Imobiliária" e inserir `<Link to="/trabalhe-conosco">Trabalhe Conosco</Link>` seguindo o mesmo padrão visual dos demais links da coluna
|
||
|
||
- [ ] T012 [P] [US2] Adicionar link/botão "Trabalhe Conosco" em `frontend/src/pages/AgentsPage.tsx` — inserir elemento (link `<Link>` ou botão secundário) com texto "Trabalhe Conosco" e `href`/`to="/trabalhe-conosco"` em posição visível na página (ex.: ao final da seção de equipe ou como chamada à ação após o grid de corretores)
|
||
|
||
**Checkpoint**: Footer exibe link em todas as páginas. `/corretores` exibe elemento que navega para `/trabalhe-conosco`.
|
||
|
||
---
|
||
|
||
## Phase 4: User Story 3 — Administrador Visualiza Candidaturas (Priority: P2)
|
||
|
||
**Goal**: Endpoint `GET /api/v1/admin/jobs` (implementado na Phase 1) retorna listagem paginada e corretamente serializada. Serviço Axios disponível para consumo futuro no painel admin.
|
||
|
||
**Independent Test**: Autenticar como admin e consultar `GET /api/v1/admin/jobs?page=1&per_page=20`. Verificar: resposta 200 com campos `items`, `total`, `page`, `per_page`, `pages`; cada item contém id, name, email, phone, role_interest, message, file_name, status, created_at. Sem token: 401. Token não-admin: 403.
|
||
|
||
- [ ] T013 [US3] Adicionar função `listApplications(page?: number, perPage?: number): Promise<PaginatedJobApplications>` em `frontend/src/services/jobsService.ts` — chama `api.get("/api/v1/admin/jobs", { params: { page, per_page: perPage } })` com header Authorization via instância autenticada do Axios; adicionar tipo `PaginatedJobApplications` em `frontend/src/types/jobApplication.ts` espelhando o schema do contrato (`items: JobApplicationItem[]`, `total`, `page`, `per_page`, `pages`)
|
||
|
||
**Checkpoint**: `listApplications()` pode ser chamado do console do browser (após login admin) e retorna dados paginados com a estrutura correta.
|
||
|
||
---
|
||
|
||
## Phase 5: User Story 4 — Conteúdo Institucional (Priority: P3)
|
||
|
||
**Goal**: Página `/trabalhe-conosco` enriquecida com hero section e seção "Por que trabalhar conosco?" com 3 cards de benefícios, posicionados acima do formulário.
|
||
|
||
**Independent Test**: Acessar `/trabalhe-conosco` e verificar: hero section com título principal e subtítulo no topo; seção "Por que trabalhar conosco?" com exatamente 3 cards de benefícios antes do formulário; layout responsivo sem sobreposição em mobile.
|
||
|
||
- [ ] T014 [US4] Adicionar hero section no topo de `frontend/src/pages/JobsPage.tsx` — bloco com título principal (ex.: "Faça parte da nossa equipe") e subtítulo descritivo; seguir design tokens dark (`text-primary`, `text-secondary`, fundo com gradiente sutil ou `bg-surface`); posicionar acima dos cards de benefícios e do formulário
|
||
|
||
- [ ] T015 [US4] Adicionar seção "Por que trabalhar conosco?" em `frontend/src/pages/JobsPage.tsx` com 3 cards de benefícios estáticos — cada card tem ícone SVG, título e descrição; layout em grid responsivo (`grid-cols-1 md:grid-cols-3`); estilo `bg-panel border border-borderSubtle rounded-xl`; posicionar entre o hero e o formulário de candidatura. Sugestão de conteúdo dos cards: "Crescimento Profissional" / "Ambiente Colaborativo" / "Comissões Competitivas"
|
||
|
||
**Checkpoint**: `/trabalhe-conosco` exibe hero → 3 benefit cards → formulário nessa ordem. Em mobile (375 px) os cards empilham verticalmente sem overflow horizontal.
|
||
|
||
---
|
||
|
||
## Phase 6: Polish & Verificação Final
|
||
|
||
- [ ] T016 Executar verificação end-to-end manualmente:
|
||
1. `GET /api/v1/admin/jobs` sem token → 401
|
||
2. `POST /api/v1/jobs/apply` com body válido → 201, candidatura registrada
|
||
3. `POST /api/v1/jobs/apply` com e-mail inválido → 422 com `details`
|
||
4. `GET /api/v1/admin/jobs?page=1` com token admin → 200 com a candidatura enviada
|
||
5. Browser: `/trabalhe-conosco` renderiza hero + 3 cards + formulário
|
||
6. Browser: footer → link "Trabalhe Conosco" → navega para `/trabalhe-conosco`
|
||
7. Browser: `/corretores` → link "Trabalhe Conosco" → navega para `/trabalhe-conosco`
|
||
|
||
---
|
||
|
||
## Dependencies & Execution Order
|
||
|
||
### Dependências entre fases
|
||
|
||
```
|
||
Phase 1 (Foundational Backend)
|
||
│
|
||
├──→ Phase 2 (US1 — Formulário) ──→ Phase 4 (US3 — Admin service)
|
||
│ │
|
||
│ └──→ Phase 3 (US2 — Links de entrada)
|
||
│
|
||
└──→ Phase 5 (US4 — Conteúdo institucional, extensão da Phase 2)
|
||
│
|
||
└──→ Phase 6 (Polish)
|
||
```
|
||
|
||
- **Phase 1**: Sem dependências — começa imediatamente
|
||
- **Phase 2**: T007 e T008 podem começar em paralelo com Phase 1 (sem necessidade do backend para criar os arquivos TS); T009 depende de T007 + T008; T010 depende de T009
|
||
- **Phase 3**: T011 e T012 são paralelos entre si e independentes do backend; dependem apenas de T010 (rota já existir no App.tsx)
|
||
- **Phase 4**: T013 depende de T007/T008 (padrão do serviço) e do endpoint já implementado em T004
|
||
- **Phase 5**: T014 e T015 são modificações em JobsPage.tsx criado em T009 — devem ser feitas sequencialmente em relação a T009
|
||
- **Phase 6**: Depende de todas as fases anteriores
|
||
|
||
### Dependências por task
|
||
|
||
| Task | Depende de | Pode ir em paralelo com |
|
||
|------|----------------|------------------------|
|
||
| T001 | — | T003 |
|
||
| T002 | T001 | T003 |
|
||
| T003 | — | T001, T002 |
|
||
| T004 | T002, T003 | — |
|
||
| T005 | T002, T004 | — |
|
||
| T006 | T005 | — |
|
||
| T007 | — | T001–T006, T008, T011, T012 |
|
||
| T008 | T007 | T011, T012 |
|
||
| T009 | T007, T008 | T011, T012 |
|
||
| T010 | T009 | T011, T012 |
|
||
| T011 | T010 | T012 |
|
||
| T012 | T010 | T011 |
|
||
| T013 | T007, T008 | T011, T012 |
|
||
| T014 | T009 | T013 |
|
||
| T015 | T014 | T013 |
|
||
| T016 | T006, T015 | — |
|
||
|
||
---
|
||
|
||
## Parallel Execution Examples
|
||
|
||
### Fluxo MVP (US1 apenas — Phase 1 + Phase 2)
|
||
|
||
```
|
||
Stream A (Backend): T001 → T002 → T004 → T005 → T006
|
||
Stream B (Schemas): T003 (paralelo a T001-T002)
|
||
Stream C (Frontend): T007 → T008 → T009 → T010
|
||
```
|
||
|
||
### Fluxo completo
|
||
|
||
```
|
||
Stream A (Backend): T001 → T002 → T004 → T005 → T006
|
||
Stream B (Schemas): T003
|
||
Stream C (Frontend): T007 → T008 → T009 → T010 → T014 → T015
|
||
Stream D (Links): T011 (paralelo após T010)
|
||
Stream E (Links): T012 (paralelo após T010)
|
||
Stream F (Admin svc): T013 (paralelo após T008)
|
||
```
|
||
|
||
---
|
||
|
||
## Implementation Strategy
|
||
|
||
**MVP Scope** (Phase 1 + Phase 2): Formulário público funcional com persistência — entrega o núcleo da feature (US1 P1).
|
||
|
||
**Incremento 2** (Phase 3): Links de descoberta — sem novos arquivos backend, apenas modificações pontuais em Footer e AgentsPage (US2 P1).
|
||
|
||
**Incremento 3** (Phase 4): Serviço admin no frontend — prepara consumo da listagem (US3 P2); página admin React adiada para iteração futura.
|
||
|
||
**Incremento 4** (Phase 5): Conteúdo institucional (hero + cards) sobre a base já existente de JobsPage (US4 P3).
|