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,36 @@
# Specification Quality Checklist: Página "Trabalhe Conosco"
**Purpose**: Validar completude e qualidade da especificação antes de avançar para o planejamento
**Created**: 2026-04-21
**Feature**: [spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- Upload real de arquivo está explicitamente fora do escopo (Assumption documentada)
- Listagem no painel admin cobre apenas a API; UI React de `/admin/jobs` pode ser entregue em iteração futura
- Spec pronta para `/speckit.plan`

View file

@ -0,0 +1,210 @@
# API Contracts: Trabalhe Conosco
**Feature**: 028-trabalhe-conosco
**Phase**: 1 — Design & Contracts
**Base URL**: `/api/v1`
---
## Endpoints
| Método | Path | Auth | Descrição |
|--------|-------------------------|--------------|------------------------------------------|
| POST | `/jobs/apply` | Nenhuma | Submeter candidatura (público) |
| GET | `/admin/jobs` | `@require_admin` (JWT) | Listar candidaturas paginadas (admin) |
---
## POST /api/v1/jobs/apply
Endpoint público. Recebe os dados textuais da candidatura e persiste na tabela `job_applications`.
### Request
**Headers**
```
Content-Type: application/json
```
**Body** (JSON)
| Campo | Tipo | Obrigatório | Validações |
|-----------------|----------|-------------|-------------------------------------------------------------------------|
| `name` | string | Sim | Não pode ser vazio ou apenas espaços; strip aplicado |
| `email` | string | Sim | Formato de e-mail válido (RFC-5321 via `pydantic.EmailStr`) |
| `phone` | string | Não | Qualquer string; sem validação de formato nesta versão |
| `role_interest` | string | Sim | Deve ser exatamente um de: `"Corretor(a)"`, `"Assistente Administrativo"`, `"Estagiário(a)"`, `"Outro"` |
| `message` | string | Sim | Não pode ser vazio; máximo 5000 caracteres |
| `file_name` | string | Não | Nome do arquivo de currículo; sem conteúdo binário |
**Exemplo de request**
```json
{
"name": "Ana Lima",
"email": "ana.lima@email.com",
"phone": "(11) 98765-4321",
"role_interest": "Corretor(a)",
"message": "Tenho 5 anos de experiência no mercado imobiliário e gostaria de integrar a equipe.",
"file_name": "curriculo-ana-lima.pdf"
}
```
### Responses
#### 201 Created — Candidatura registrada com sucesso
```json
{
"message": "Candidatura recebida com sucesso"
}
```
#### 422 Unprocessable Entity — Dados inválidos
```json
{
"error": "Dados inválidos",
"details": [
{
"type": "value_error",
"loc": ["role_interest"],
"msg": "Value error, role_interest deve ser um de: Assistente Administrativo, Corretor(a), Estagiário(a), Outro",
"input": "Diretor",
"url": "https://errors.pydantic.dev/..."
}
]
}
```
#### 400 Bad Request — Body ausente ou não é JSON válido
```json
{
"error": "Dados inválidos",
"details": [...]
}
```
---
## GET /api/v1/admin/jobs
Endpoint protegido. Retorna listagem paginada de todas as candidaturas em ordem decrescente de `created_at`.
### Request
**Headers**
```
Authorization: Bearer <jwt_token>
Content-Type: application/json
```
**Query Parameters**
| Parâmetro | Tipo | Default | Restrições | Descrição |
|------------|---------|---------|-----------------|------------------------|
| `page` | integer | `1` | ≥ 1 | Número da página |
| `per_page` | integer | `20` | 1 100 | Registros por página |
**Exemplo de request**
```
GET /api/v1/admin/jobs?page=1&per_page=20
```
### Responses
#### 200 OK — Lista retornada com sucesso
```json
{
"items": [
{
"id": 7,
"name": "Ana Lima",
"email": "ana.lima@email.com",
"phone": "(11) 98765-4321",
"role_interest": "Corretor(a)",
"message": "Tenho 5 anos de experiência no mercado imobiliário...",
"file_name": "curriculo-ana-lima.pdf",
"status": "pending",
"created_at": "2026-04-21T14:35:00"
},
{
"id": 6,
"name": "Carlos Souza",
"email": "carlos@email.com",
"phone": null,
"role_interest": "Estagiário(a)",
"message": "Estudante de Administração em busca do primeiro emprego.",
"file_name": null,
"status": "pending",
"created_at": "2026-04-20T09:12:00"
}
],
"total": 42,
"page": 1,
"per_page": 20,
"pages": 3
}
```
**Schema do item** (`JobApplicationOut`)
| Campo | Tipo | Nullable | Descrição |
|-----------------|------------------|----------|----------------------------------|
| `id` | integer | Não | Identificador único |
| `name` | string | Não | Nome completo do candidato |
| `email` | string | Não | E-mail do candidato |
| `phone` | string \| null | Sim | Telefone (opcional) |
| `role_interest` | string | Não | Cargo de interesse selecionado |
| `message` | string | Não | Mensagem/apresentação |
| `file_name` | string \| null | Sim | Nome do arquivo de currículo |
| `status` | string | Não | Estado: `"pending"` (padrão) |
| `created_at` | string (ISO 8601)| Não | Data/hora do envio |
**Schema de paginação**
| Campo | Tipo | Descrição |
|------------|---------|-------------------------------------|
| `total` | integer | Total de candidaturas no sistema |
| `page` | integer | Página atual |
| `per_page` | integer | Registros retornados nesta página |
| `pages` | integer | Total de páginas |
#### 200 OK — Nenhuma candidatura registrada
```json
{
"items": [],
"total": 0,
"page": 1,
"per_page": 20,
"pages": 0
}
```
#### 401 Unauthorized — Token ausente ou inválido
```json
{
"error": "Token inválido ou ausente"
}
```
#### 403 Forbidden — Usuário autenticado sem permissão de admin
```json
{
"error": "Acesso negado"
}
```
---
## Notas de Implementação
- O endpoint `POST /api/v1/jobs/apply` **não** possui autenticação — qualquer cliente pode submeter.
- O endpoint `GET /api/v1/admin/jobs` usa o decorator `@require_admin` já existente no projeto, que valida o JWT e verifica a flag de administrador.
- O campo `created_at` é serializado pelo Pydantic como ISO 8601 sem timezone (`TIMESTAMP WITHOUT TIME ZONE` no PostgreSQL).
- `per_page` deve ser limitado a 100 no backend para evitar queries excessivamente grandes.
- Campos ausentes no body do POST são tratados pelo Pydantic: obrigatórios geram erro 422, opcionais recebem `None`.

View file

@ -0,0 +1,215 @@
# Data Model: Trabalhe Conosco
**Feature**: 028-trabalhe-conosco
**Phase**: 1 — Design & Contracts
**Source**: spec.md
---
## Entidade: JobApplication (Candidatura)
### Tabela: `job_applications`
| Coluna | Tipo SQL | Nullable | Default | Restrições |
|------------------|---------------------------------|----------|------------------|--------------------------------------------|
| `id` | `SERIAL` (INTEGER PK) | NOT NULL | auto-increment | PRIMARY KEY |
| `name` | `VARCHAR(150)` | NOT NULL | — | campo obrigatório |
| `email` | `VARCHAR(254)` | NOT NULL | — | formato e-mail válido (validado no backend)|
| `phone` | `VARCHAR(30)` | NULL | — | opcional conforme spec |
| `role_interest` | `VARCHAR(100)` | NOT NULL | — | enum: Corretor(a), Assistente Administrativo, Estagiário(a), Outro |
| `message` | `TEXT` | NOT NULL | — | apresentação/mensagem do candidato |
| `file_name` | `VARCHAR(255)` | NULL | — | nome do arquivo de currículo (sem upload) |
| `status` | `VARCHAR(50)` | NOT NULL | `'pending'` | estado da candidatura (pending / reviewed) |
| `created_at` | `TIMESTAMP WITHOUT TIME ZONE` | NOT NULL | `now()` (server) | imutável após criação |
### Índices
| Índice | Colunas | Motivo |
|------------------------------------|-----------------|------------------------------------------------|
| `ix_job_applications_created_at` | `created_at` | ordenação DESC na listagem admin |
| `ix_job_applications_status` | `status` | filtragem futura por estado |
### Invariantes
1. `name`, `email`, `role_interest` e `message` nunca são deixados em branco (validação Pydantic).
2. `email` deve ser validado com `pydantic.EmailStr` — formato RFC-5321.
3. `role_interest` deve ser um dos valores permitidos: `"Corretor(a)"`, `"Assistente Administrativo"`, `"Estagiário(a)"`, `"Outro"`.
4. `message` não pode ultrapassar 5000 caracteres (validação frontend + Pydantic `max_length`).
5. `phone` é opcional — sem validação de formato nesta versão.
6. `file_name` armazena apenas o nome do arquivo informado, sem conteúdo binário.
7. Múltiplas candidaturas do mesmo `email` são permitidas (sem deduplicação nesta versão).
8. Nenhum `DELETE` físico é exposto; o campo `status` permite rastreabilidade futura.
### Diagrama ER
```
job_applications
├── id PK SERIAL
├── 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 DEFAULT 'pending'
└── created_at TIMESTAMP NOT NULL DEFAULT now()
```
Sem relacionamentos com outras tabelas nesta versão. Entidade standalone.
---
## Modelo SQLAlchemy: `backend/app/models/job_application.py`
```python
from app.extensions import db
ROLE_INTEREST_OPTIONS = [
"Corretor(a)",
"Assistente Administrativo",
"Estagiário(a)",
"Outro",
]
class JobApplication(db.Model):
__tablename__ = "job_applications"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(150), nullable=False)
email = db.Column(db.String(254), nullable=False)
phone = db.Column(db.String(30), nullable=True)
role_interest = db.Column(db.String(100), nullable=False)
message = db.Column(db.Text, nullable=False)
file_name = db.Column(db.String(255), nullable=True)
status = db.Column(db.String(50), nullable=False, default="pending")
created_at = db.Column(
db.DateTime, nullable=False, server_default=db.func.now()
)
def __repr__(self) -> str:
return f"<JobApplication id={self.id} email={self.email!r}>"
```
---
## Migration Alembic: `backend/migrations/versions/i1j2k3l4m5n6_add_job_applications.py`
```python
"""add job_applications table
Revision ID: i1j2k3l4m5n6
Revises: h1i2j3k4l5m6
Create Date: 2026-04-21 00:00:00.000000
"""
import sqlalchemy as sa
from alembic import op
revision = "i1j2k3l4m5n6"
down_revision = "h1i2j3k4l5m6"
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
"job_applications",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("name", sa.String(length=150), nullable=False),
sa.Column("email", sa.String(length=254), nullable=False),
sa.Column("phone", sa.String(length=30), nullable=True),
sa.Column("role_interest", sa.String(length=100), nullable=False),
sa.Column("message", sa.Text(), nullable=False),
sa.Column("file_name", sa.String(length=255), nullable=True),
sa.Column(
"status",
sa.String(length=50),
nullable=False,
server_default=sa.text("'pending'"),
),
sa.Column(
"created_at",
sa.DateTime(),
nullable=False,
server_default=sa.func.now(),
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
"ix_job_applications_created_at", "job_applications", ["created_at"]
)
op.create_index(
"ix_job_applications_status", "job_applications", ["status"]
)
def downgrade():
op.drop_index("ix_job_applications_status", table_name="job_applications")
op.drop_index("ix_job_applications_created_at", table_name="job_applications")
op.drop_table("job_applications")
```
---
## Schemas Pydantic: `backend/app/schemas/job_application.py`
```python
from __future__ import annotations
from datetime import datetime
from pydantic import BaseModel, ConfigDict, EmailStr, field_validator
VALID_ROLES = {"Corretor(a)", "Assistente Administrativo", "Estagiário(a)", "Outro"}
class JobApplicationIn(BaseModel):
name: str
email: EmailStr
phone: str | None = None
role_interest: str
message: str
file_name: str | None = None
@field_validator("name")
@classmethod
def name_not_empty(cls, v: str) -> str:
v = v.strip()
if not v:
raise ValueError("name não pode ser vazio")
return v
@field_validator("role_interest")
@classmethod
def role_must_be_valid(cls, v: str) -> str:
if v not in VALID_ROLES:
raise ValueError(f"role_interest deve ser um de: {', '.join(sorted(VALID_ROLES))}")
return v
@field_validator("message")
@classmethod
def message_not_empty(cls, v: str) -> str:
v = v.strip()
if not v:
raise ValueError("message não pode ser vazia")
if len(v) > 5000:
raise ValueError("message não pode ultrapassar 5000 caracteres")
return v
class JobApplicationOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str
email: str
phone: str | None
role_interest: str
message: str
file_name: str | None
status: str
created_at: datetime
```

View file

@ -0,0 +1,354 @@
# Implementation Plan: Trabalhe Conosco
**Branch**: `028-trabalhe-conosco` | **Date**: 2026-04-21 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `/specs/028-trabalhe-conosco/spec.md`
## Summary
Criar a página pública `/trabalhe-conosco` com hero section, seção de benefícios (3 cards estáticos) e formulário de candidatura. O formulário submete via `POST /api/v1/jobs/apply` (endpoint público sem auth). As candidaturas são persistidas na tabela `job_applications` e recuperáveis pelo administrador via `GET /api/v1/admin/jobs` (paginado, protegido por `@require_admin`). Links adicionados no footer (coluna "A Imobiliária") e em `AgentsPage.tsx`. Dois novos blueprints Flask, novo model SQLAlchemy, migration Alembic, schemas Pydantic e uma nova página React com serviço Axios.
---
## Technical Context
**Language/Version**: Python 3.12 (backend) / TypeScript 5.5 (frontend)
**Primary Dependencies**: Flask 3.x, SQLAlchemy 2.x, Pydantic v2, PyJWT, Alembic (backend) · React 18, Tailwind CSS 3.4, react-router-dom v6, Axios (frontend)
**Storage**: PostgreSQL 16 — nova tabela `job_applications`
**Testing**: pytest (backend)
**Target Platform**: Linux server (Docker container)
**Project Type**: web-service (Flask REST API) + SPA (React)
**Performance Goals**: página pública carrega em < 2s (SC-006); listagem admin paginada (20/página)
**Constraints**: sem upload real de arquivo (apenas `file_name` como texto); sem envio de e-mail; sem rate limiting nesta versão; múltiplas candidaturas do mesmo e-mail são permitidas
**Scale/Scope**: volume baixo de candidaturas; paginação padrão 20/página
---
## Constitution Check
| Princípio | Status | Observação |
|-----------|--------|------------|
| **I. Design-First** | ✅ PASS | Hero, cards de benefícios e formulário seguem design tokens dark do `DESIGN.md`; cores `#5e6ad2`, tipografia Inter, cards com `bg-panel border-borderSubtle` |
| **II. Separation of Concerns** | ✅ PASS | Flask retorna JSON puro; React SPA consome via Axios; zero lógica de renderização no backend |
| **III. Spec-Driven** | ✅ PASS | `spec.md` com user stories P1/P2/P3 e acceptance scenarios; plan derivado do spec |
| **IV. Data Integrity** | ✅ PASS | Migration Alembic (`i1j2k3l4m5n6`); Pydantic valida todos os inputs; `email: EmailStr`; sem raw SQL |
| **V. Security** | ✅ PASS | Endpoint admin protegido por `@require_admin` (JWT); endpoint público não expõe dados internos; sem exposição de stack traces em erro 500 |
| **VI. Simplicity First** | ✅ PASS | Sem upload binário (justificado na spec), sem e-mail transacional, sem rate limiting nesta versão; página de admin React adiada para iteração futura (conforme Assumptions da spec) |
**Veredicto**: Sem violações. Pode prosseguir com implementação.
---
## Project Structure
### Documentação (esta feature)
```text
specs/028-trabalhe-conosco/
├── spec.md # Especificação de produto
├── data-model.md # Entidade JobApplication, migration, schemas
├── plan.md # Este arquivo
├── contracts/
│ └── jobs-api.md # Contratos dos 2 endpoints REST
└── tasks.md # (Phase 2 — gerado por /speckit.tasks)
```
### Código-fonte (raiz do repositório)
```text
backend/
├── app/
│ ├── models/
│ │ └── job_application.py # NOVO — modelo SQLAlchemy JobApplication
│ ├── schemas/
│ │ └── job_application.py # NOVO — JobApplicationIn, JobApplicationOut
│ ├── routes/
│ │ └── jobs.py # NOVO — jobs_public_bp + jobs_admin_bp
│ └── __init__.py # MODIFICAR — importar model + registrar blueprints
└── migrations/
└── versions/
└── i1j2k3l4m5n6_add_job_applications.py # NOVO — cria tabela + índices
frontend/
└── src/
├── types/
│ └── jobApplication.ts # NOVO — interface JobApplication
├── services/
│ └── jobs.ts # NOVO — submitApplication(), listApplications()
├── pages/
│ └── JobsPage.tsx # NOVO — página pública /trabalhe-conosco
├── App.tsx # MODIFICAR — adicionar rota /trabalhe-conosco
└── components/
└── Footer.tsx # MODIFICAR — link "Trabalhe Conosco" em "A Imobiliária"
└── pages/
└── AgentsPage.tsx # MODIFICAR — link/botão "Trabalhe Conosco"
```
---
## Backend: Arquitetura Técnica
### Model: `backend/app/models/job_application.py`
Entidade standalone com 9 campos. Ver [data-model.md](data-model.md) para schema completo e invariantes.
Campos principais:
- `id` — PK SERIAL
- `name`, `email`, `role_interest`, `message` — obrigatórios
- `phone`, `file_name` — opcionais
- `status` — default `"pending"` (extensível futuramente)
- `created_at` — server_default `now()`, imutável
### Schemas Pydantic: `backend/app/schemas/job_application.py`
**`JobApplicationIn`** (entrada do endpoint público):
- Valida `name` (não vazio, strip), `email` (EmailStr), `role_interest` (enum de 4 opções), `message` (não vazio, max 5000 chars)
- `phone` e `file_name` opcionais
**`JobApplicationOut`** (saída do endpoint admin):
- Retorna todos os campos incluindo `id`, `status` e `created_at`
- `model_config = ConfigDict(from_attributes=True)` para serialização ORM
### Blueprints: `backend/app/routes/jobs.py`
Dois blueprints no mesmo arquivo, seguindo o padrão de `routes/agents.py`:
```python
jobs_public_bp = Blueprint("jobs_public", __name__, url_prefix="/api/v1")
jobs_admin_bp = Blueprint("jobs_admin", __name__, url_prefix="/api/v1/admin")
```
**`POST /api/v1/jobs/apply`** (público, sem autenticação):
1. `request.get_json(silent=True) or {}`
2. `JobApplicationIn.model_validate(data)` → 422 com `exc.errors()` se inválido
3. Instanciar `JobApplication(...)` e `db.session.add` + `db.session.commit()`
4. Retornar `{"message": "Candidatura recebida com sucesso"}`, HTTP 201
**`GET /api/v1/admin/jobs`** (protegido por `@require_admin`):
1. Query params: `page` (default 1), `per_page` (default 20, max 100)
2. `JobApplication.query.order_by(JobApplication.created_at.desc()).paginate(page, per_page, error_out=False)`
3. Serializar com `JobApplicationOut` e retornar envelope paginado:
```json
{
"items": [...],
"total": 42,
"page": 1,
"per_page": 20,
"pages": 3
}
```
4. `@require_admin` dispara 401/403 automaticamente
### Registro em `backend/app/__init__.py`
Dois patches necessários:
```python
# Importar model (para Flask-Migrate detectar)
from app.models import job_application as _job_application_models # noqa: F401
# Importar e registrar blueprints
from app.routes.jobs import jobs_public_bp, jobs_admin_bp
app.register_blueprint(jobs_public_bp)
app.register_blueprint(jobs_admin_bp)
```
### Migration Alembic
Arquivo: `i1j2k3l4m5n6_add_job_applications.py`
- `down_revision = "h1i2j3k4l5m6"` (migration atual mais recente: `create_contact_config`)
- Cria tabela `job_applications` com 9 colunas
- Cria índices: `ix_job_applications_created_at`, `ix_job_applications_status`
- `downgrade()` desfaz índices e tabela
Ver código completo em [data-model.md](data-model.md).
---
## Frontend: Arquitetura Técnica
### Types: `frontend/src/types/jobApplication.ts`
```typescript
export interface JobApplicationPayload {
name: string
email: string
phone?: string
role_interest: string
message: string
file_name?: string
}
export interface JobApplication {
id: number
name: string
email: string
phone: string | null
role_interest: string
message: string
file_name: string | null
status: string
created_at: string
}
export interface JobApplicationsResponse {
items: JobApplication[]
total: number
page: number
per_page: number
pages: number
}
```
### Service: `frontend/src/services/jobs.ts`
```typescript
import api from './api'
import type { JobApplicationPayload, JobApplicationsResponse } from '../types/jobApplication'
export async function submitApplication(payload: JobApplicationPayload): Promise<void> {
await api.post('/api/v1/jobs/apply', payload)
}
export async function listApplications(
page = 1,
perPage = 20
): Promise<JobApplicationsResponse> {
const { data } = await api.get('/api/v1/admin/jobs', {
params: { page, per_page: perPage },
})
return data
}
```
### Página: `frontend/src/pages/JobsPage.tsx`
Estrutura da página (3 seções, de cima para baixo):
#### 1. Hero Section
```
bg-canvas | max-w-[1200px] mx-auto px-6 pt-16 pb-10
├── eyebrow: "Faça parte do nosso time" (text-[#5e6ad2] uppercase tracking-widest)
├── h1: "Trabalhe Conosco" (text-3xl md:text-4xl font-semibold text-textPrimary)
└── subtítulo: texto descritivo (text-textSecondary)
```
#### 2. Seção "Por que trabalhar conosco?" (3 cards estáticos)
```
max-w-[1200px] mx-auto px-6 py-10
├── h2: "Por que trabalhar conosco?" (text-xl font-semibold text-textPrimary mb-6)
└── grid grid-cols-1 md:grid-cols-3 gap-5
├── Card 1: ícone + "Ambiente Colaborativo" + descrição
├── Card 2: ícone + "Crescimento Profissional" + descrição
└── Card 3: ícone + "Remuneração Competitiva" + descrição
(cada card: bg-panel border border-borderSubtle rounded-2xl p-6)
```
#### 3. Formulário de Candidatura
```
max-w-[640px] mx-auto px-6 pb-20
├── h2: "Envie sua candidatura"
└── <form onSubmit={handleSubmit}>
├── name — input text, obrigatório
├── email — input email, obrigatório, validação RFC
├── phone — input tel, opcional
├── role_interest — select (4 opções), obrigatório
├── message — textarea, obrigatório, max 5000 chars, contador de chars
├── file (currículo) — input file, accept=".pdf", max 2MB (validação frontend only)
│ ao selecionar: setFileName(file.name), não envia binário
└── submit button "Enviar Candidatura"
```
**Gerenciamento de estado** (hooks locais, sem Redux/Context):
- `formData` — estado do formulário
- `fileName` — nome do arquivo selecionado (string | null)
- `errors` — Record<string, string> para mensagens por campo
- `submitting` — boolean, desabilita botão durante requisição
- `submitted` — boolean, exibe mensagem de sucesso e reseta form
- `serverError` — string | null, erro de rede/500
**Validação frontend** (antes de chamar `submitApplication`):
- `name`: obrigatório, trim
- `email`: obrigatório, regex RFC simples
- `role_interest`: obrigatório, não pode ser valor vazio/placeholder
- `message`: obrigatório, max 5000 chars
- `file`: se presente, extensão `.pdf` e tamanho ≤ 2 MB; apenas registra `file_name`
**Fluxo de submit**:
1. Validar campos → exibir erros por campo se inválido
2. `setSubmitting(true)`
3. `submitApplication({ name, email, phone, role_interest, message, file_name: fileName ?? undefined })`
4. Sucesso → `setSubmitted(true)`, resetar `formData`, `setFileName(null)`
5. Erro 422 → parsear `details` e mapear para `errors` por campo
6. Erro ≥ 500 ou rede → `setServerError("Erro ao enviar candidatura. Tente novamente.")`
7. `finally``setSubmitting(false)`
**Design tokens** (seguir padrão do projeto):
- Inputs: `w-full bg-panel border border-borderSubtle rounded-lg px-4 py-2.5 text-textPrimary placeholder:text-textQuaternary focus:outline-none focus:ring-2 focus:ring-[#5e6ad2]/50`
- Labels: `text-sm font-medium text-textSecondary mb-1.5`
- Erros: `text-xs text-red-400 mt-1`
- Botão submit: `w-full bg-[#5e6ad2] hover:bg-[#4f5bbf] text-white font-medium py-2.5 rounded-lg transition-colors duration-150 disabled:opacity-60`
- Mensagem de sucesso: `bg-emerald-500/10 border border-emerald-500/20 rounded-xl p-6 text-center`
### Modificação: `frontend/src/App.tsx`
Adicionar a rota da nova página:
```tsx
import JobsPage from './pages/JobsPage'
// ...
<Route path="/trabalhe-conosco" element={<JobsPage />} />
```
### Modificação: `frontend/src/components/Footer.tsx`
Adicionar link na coluna "A Imobiliária" (após "Política de Privacidade"):
```tsx
<FooterLink to="/trabalhe-conosco">Trabalhe Conosco</FooterLink>
```
### Modificação: `frontend/src/pages/AgentsPage.tsx`
Adicionar banner/botão após o grid de corretores e antes do `<Footer />`:
```tsx
{/* CTA Trabalhe Conosco */}
<div className="max-w-[1200px] mx-auto px-6 pb-20">
<div className="bg-panel border border-borderSubtle rounded-2xl p-8 flex flex-col sm:flex-row items-center justify-between gap-4">
<div>
<h2 className="text-lg font-semibold text-textPrimary">Quer fazer parte do time?</h2>
<p className="text-textSecondary text-sm mt-1">Envie sua candidatura e venha crescer conosco.</p>
</div>
<Link
to="/trabalhe-conosco"
className="shrink-0 bg-[#5e6ad2] hover:bg-[#4f5bbf] text-white font-medium px-5 py-2.5 rounded-lg transition-colors duration-150 text-sm"
>
Trabalhe Conosco
</Link>
</div>
</div>
```
---
## Sequência de Implementação
1. **Migration** — criar `i1j2k3l4m5n6_add_job_applications.py` e rodar `flask db upgrade`
2. **Model** — criar `backend/app/models/job_application.py`
3. **Schemas** — criar `backend/app/schemas/job_application.py`
4. **Routes** — criar `backend/app/routes/jobs.py`
5. **Register** — modificar `backend/app/__init__.py` (model import + blueprints)
6. **Types** — criar `frontend/src/types/jobApplication.ts`
7. **Service** — criar `frontend/src/services/jobs.ts`
8. **Page** — criar `frontend/src/pages/JobsPage.tsx`
9. **Route** — modificar `frontend/src/App.tsx`
10. **Footer** — modificar `frontend/src/components/Footer.tsx`
11. **AgentsPage** — modificar `frontend/src/pages/AgentsPage.tsx`
---
## Complexity Tracking
Sem violações de constituição — seção não aplicável.
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|--------------------------------------|
| — | — | — |

View file

@ -0,0 +1,150 @@
# Feature Specification: Página "Trabalhe Conosco"
**Feature Branch**: `028-trabalhe-conosco`
**Created**: 2026-04-21
**Status**: Draft
---
## Contexto
O site imobiliário atualmente não oferece um canal formal para que candidatos manifestem interesse em trabalhar na empresa. Esse contato ocorre de maneira informal — por telefone, e-mail avulso ou presencialmente — sem rastreabilidade e sem uma experiência consistente para o candidato.
Esta spec cobre a criação de uma página pública "/trabalhe-conosco" com formulário de candidatura, armazenamento das submissões em banco de dados e listagem das candidaturas no painel administrativo. A página também deve ser acessível via links no footer e na página de equipe, tornando o recrutamento um ponto de contato organizado e profissional.
---
## User Scenarios & Testing
### User Story 1 — Candidato Envia Formulário de Candidatura (Priority: P1)
Um candidato interessado em trabalhar na imobiliária acessa a página "/trabalhe-conosco", preenche o formulário com seus dados e envia sua candidatura.
**Why this priority**: É o núcleo da feature. Toda a proposta de valor gira em torno dessa ação — sem ela, a página é apenas um conteúdo estático sem utilidade.
**Independent Test**: Acessar `/trabalhe-conosco` sem autenticação, preencher todos os campos obrigatórios (nome, e-mail, telefone, cargo de interesse, mensagem) e submeter. Verificar que uma mensagem de sucesso é exibida e que a candidatura aparece na listagem do painel admin em `GET /api/v1/admin/jobs`.
**Acceptance Scenarios**:
1. **Given** um visitante não autenticado na página `/trabalhe-conosco`, **When** ele preenche todos os campos obrigatórios e clica em "Enviar Candidatura", **Then** a candidatura é registrada no sistema e uma mensagem de confirmação é exibida ao candidato.
2. **Given** o formulário preenchido corretamente, **When** o campo de cargo de interesse é "Corretor(a)", **Then** o valor enviado e armazenado reflete exatamente a opção selecionada.
3. **Given** o formulário preenchido corretamente com um arquivo PDF informado, **When** o candidato submete, **Then** o nome do arquivo é registrado junto com a candidatura, mesmo que o conteúdo do arquivo não seja armazenado nesta versão.
4. **Given** o formulário submetido com sucesso, **When** o candidato tenta submeter novamente sem recarregar a página, **Then** o formulário é limpo/resetado após o sucesso, prevenindo envios duplicados acidentais.
5. **Given** falha de rede durante o envio, **When** a requisição não é completada, **Then** uma mensagem de erro informativa é exibida e o candidato pode tentar novamente sem perder os dados preenchidos.
---
### User Story 2 — Visitante Descobre a Oportunidade Pelo Site (Priority: P1)
Um visitante que navega pelo footer ou pela página de equipe (/corretores) encontra o link "Trabalhe Conosco" e acessa a página de candidatura.
**Why this priority**: Sem pontos de entrada adequados, a página não é encontrada organicamente dentro do site, tornando o canal de recrutamento inacessível na prática.
**Independent Test**: Acessar o footer do site e verificar a presença do link "Trabalhe Conosco" na coluna "A Imobiliária". Acessar `/corretores` e verificar a presença do link/botão "Trabalhe Conosco". Clicar em cada link e confirmar que navega para `/trabalhe-conosco`.
**Acceptance Scenarios**:
1. **Given** um visitante em qualquer página do site, **When** ele visualiza o footer, **Then** o link "Trabalhe Conosco" está visível na coluna "A Imobiliária".
2. **Given** um visitante na página `/corretores`, **When** ele visualiza a página de equipe, **Then** existe um elemento (link ou botão) com o texto "Trabalhe Conosco" que leva a `/trabalhe-conosco`.
3. **Given** um visitante clicando no link "Trabalhe Conosco" a partir do footer, **When** a navegação ocorre, **Then** ele é direcionado para `/trabalhe-conosco` com a página completa carregada.
4. **Given** um visitante em dispositivo móvel, **When** ele visualiza o footer ou a página `/corretores`, **Then** o link "Trabalhe Conosco" é igualmente acessível e funcional.
---
### User Story 3 — Administrador Visualiza as Candidaturas Recebidas (Priority: P2)
O administrador da imobiliária acessa o painel admin e visualiza uma listagem paginada de todas as candidaturas enviadas pelos candidatos.
**Why this priority**: Sem visibilidade das candidaturas, o canal de recrutamento não entrega valor operacional. A listagem é o produto final que o administrador consume para iniciar o processo seletivo.
**Independent Test**: Com candidaturas já enviadas via formulário público, autenticar como administrador e consultar `GET /api/v1/admin/jobs`. Verificar que a resposta inclui os dados dos candidatos (nome, e-mail, cargo, data de envio) com paginação funcional.
**Acceptance Scenarios**:
1. **Given** um administrador autenticado, **When** ele acessa o endpoint de listagem de candidaturas, **Then** a resposta inclui uma lista paginada com nome, e-mail, telefone, cargo de interesse, data de envio e nome do arquivo informado para cada candidatura.
2. **Given** mais de 20 candidaturas no sistema, **When** o administrador consulta a segunda página, **Then** os resultados são diferentes da primeira página e o total de candidaturas é informado na resposta.
3. **Given** um usuário não autenticado tentando acessar o endpoint de listagem, **When** a requisição é enviada, **Then** o sistema retorna erro de acesso não autorizado (HTTP 401).
4. **Given** um token de usuário comum (não administrador), **When** ele tenta acessar o endpoint de listagem, **Then** o sistema retorna erro de permissão insuficiente (HTTP 403).
5. **Given** nenhuma candidatura registrada, **When** o administrador consulta a listagem, **Then** o sistema retorna uma lista vazia com o total zerado, sem erro.
---
### User Story 4 — Candidato Vê a Página com Conteúdo Institucional (Priority: P3)
Um candidato acessa `/trabalhe-conosco` e, além do formulário, encontra uma apresentação institucional da imobiliária como empregadora, com destaque para benefícios de trabalhar na empresa.
**Why this priority**: Enriquece a experiência do candidato e posiciona a imobiliária como empregadora, mas não bloqueia o funcionamento do recrutamento em si.
**Independent Test**: Acessar `/trabalhe-conosco` e verificar que a página contém: uma hero section com título e subtítulo, uma seção "Por que trabalhar conosco?" com exatamente 3 cards de benefícios, e o formulário de candidatura.
**Acceptance Scenarios**:
1. **Given** qualquer visitante acessando `/trabalhe-conosco`, **When** a página carrega, **Then** uma hero section com título principal e subtítulo descritivo é exibida no topo.
2. **Given** a página carregada, **When** o visitante rola a tela, **Then** uma seção "Por que trabalhar conosco?" com 3 cards de benefícios é visível antes do formulário.
3. **Given** a página carregada, **When** o visitante acessa em dispositivo móvel, **Then** hero section, cards de benefícios e formulário se adaptam ao layout vertical sem perda de conteúdo ou sobreposição visual.
---
### Edge Cases
- O que acontece se o candidato enviar o formulário com um e-mail em formato inválido? A validação deve ocorrer no frontend antes do envio, e o backend deve rejeitar com HTTP 422 e mensagem descritiva.
- O que acontece se o campo de mensagem ultrapassar o limite de caracteres? O sistema deve validar e informar o candidato antes de enviar.
- O que acontece se o candidato tentar enviar o mesmo e-mail múltiplas vezes? Por padrão, múltiplas candidaturas do mesmo e-mail são permitidas (sem deduplicação nesta versão).
- O que acontece se o candidato selecionar um arquivo que não seja PDF ou que exceda 2 MB? O frontend deve bloquear o envio e exibir mensagem de erro clara. Nesta versão, apenas o nome do arquivo é registrado — não há upload real de arquivo.
- O que acontece se o backend retornar erro 500 durante o envio? O frontend deve exibir mensagem genérica de erro sem expor detalhes técnicos.
- Como o endpoint público `POST /api/v1/jobs/apply` se comporta em caso de sobrecarga? Por padrão, o endpoint não possui rate limiting nesta versão — isso pode ser adicionado futuramente.
---
## Requirements
### Functional Requirements
- **FR-001**: O sistema DEVE disponibilizar a rota pública `/trabalhe-conosco` no frontend, acessível sem autenticação.
- **FR-002**: A página DEVE conter uma hero section com título e subtítulo configurados estaticamente.
- **FR-003**: A página DEVE conter uma seção "Por que trabalhar conosco?" com 3 cards de benefícios (conteúdo estático).
- **FR-004**: A página DEVE conter um formulário de candidatura com os campos: nome completo, e-mail, telefone, cargo de interesse (select), mensagem/apresentação e seleção de arquivo de currículo.
- **FR-005**: O campo de cargo de interesse DEVE oferecer as opções: Corretor(a), Assistente Administrativo, Estagiário(a), Outro.
- **FR-006**: O formulário DEVE validar campos obrigatórios (nome, e-mail, cargo, mensagem) antes do envio, exibindo mensagens de erro por campo.
- **FR-007**: O campo de e-mail DEVE validar formato de e-mail válido no frontend antes do envio.
- **FR-008**: O campo de arquivo DEVE aceitar apenas arquivos PDF e rejeitar arquivos acima de 2 MB, com mensagem de erro clara — a validação ocorre no frontend; nesta versão, apenas o nome do arquivo é enviado ao backend.
- **FR-009**: Após envio bem-sucedido, o formulário DEVE exibir uma mensagem de confirmação ao candidato e limpar os campos.
- **FR-010**: O sistema DEVE disponibilizar o endpoint público `POST /api/v1/jobs/apply` que receba e persista os dados textuais da candidatura (sem autenticação).
- **FR-011**: O backend DEVE validar os dados recebidos no endpoint de candidatura e retornar HTTP 422 com detalhes para dados inválidos.
- **FR-012**: O sistema DEVE armazenar as candidaturas em uma tabela `job_applications` com os campos: nome completo, e-mail, telefone, cargo de interesse, mensagem, nome do arquivo informado e data/hora do envio.
- **FR-013**: O sistema DEVE disponibilizar o endpoint protegido `GET /api/v1/admin/jobs` que retorne uma listagem paginada das candidaturas, acessível apenas por administradores autenticados.
- **FR-014**: O endpoint de listagem DEVE retornar para cada candidatura: nome, e-mail, telefone, cargo de interesse, mensagem, nome do arquivo e data de envio.
- **FR-015**: O endpoint de listagem DEVE suportar paginação com parâmetros `page` e `per_page`, retornando o total de registros.
- **FR-016**: O footer do site DEVE conter o link "Trabalhe Conosco" na coluna "A Imobiliária", navegando para `/trabalhe-conosco`.
- **FR-017**: A página `/corretores` DEVE conter um link ou botão "Trabalhe Conosco" navegando para `/trabalhe-conosco`.
- **FR-018**: O design da página DEVE seguir os design tokens existentes do projeto (cores, tipografia Inter, estilo de cards limpos).
### Key Entities
- **JobApplication**: Registro de candidatura enviada por um candidato. Atributos principais: identificador único, nome completo do candidato, e-mail, telefone, cargo de interesse selecionado, texto de apresentação/mensagem, nome do arquivo de currículo informado (opcional), data e hora do envio. Sem relacionamento com outras entidades nesta versão.
---
## Success Criteria
### Measurable Outcomes
- **SC-001**: Um candidato consegue localizar e acessar a página "Trabalhe Conosco" a partir do footer ou da página de equipe em no máximo 2 cliques.
- **SC-002**: Um candidato consegue preencher e enviar o formulário de candidatura completo em menos de 3 minutos.
- **SC-003**: 100% das candidaturas enviadas com dados válidos são armazenadas e recuperáveis pelo administrador via painel admin.
- **SC-004**: Tentativas de acesso não autorizado ao endpoint de listagem de candidaturas são bloqueadas em 100% dos casos.
- **SC-005**: O formulário exibe mensagem de erro específica para cada campo inválido sem necessidade de recarregar a página.
- **SC-006**: A página carrega e exibe todo o conteúdo estático (hero, benefícios, formulário) em menos de 2 segundos em conexões de banda larga padrão.
---
## Assumptions
- O upload real do arquivo de currículo (armazenamento binário no servidor ou serviço de storage) está fora do escopo desta versão; apenas o nome do arquivo informado pelo candidato é salvo como texto.
- Não há deduplicação de candidaturas por e-mail nesta versão — múltiplas submissões do mesmo endereço são permitidas.
- Os 3 benefícios exibidos na seção "Por que trabalhar conosco?" são conteúdo estático definido em tempo de desenvolvimento; não há interface de gerenciamento para esse conteúdo.
- Não há envio de e-mail de confirmação ao candidato nem notificação por e-mail ao administrador nesta versão.
- O endpoint público de candidatura não possui rate limiting nesta versão.
- A listagem de candidaturas no painel admin é acessível via API; a interface visual no painel admin (página React de `/admin/jobs`) pode ser entregue em iteração futura, não sendo requisito desta spec.
- O padrão de autenticação de administrador já implementado no projeto (`require_admin`) é suficiente e será reutilizado para proteger o endpoint de listagem.
- O campo de telefone é opcional para o candidato, mas recomendado — a validação de formato não é obrigatória nesta versão.

View file

@ -0,0 +1,217 @@
---
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 1100), 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 | — | T001T006, 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).