- 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)
210 lines
6.5 KiB
Markdown
210 lines
6.5 KiB
Markdown
# 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`.
|