# Módulo: Trabalhe Conosco

## 1. O que é o módulo

Gerencia a página de vagas e candidaturas (rota `/cms/institucional/trabalhe-conosco`, portal `/trabalhe-conosco`). O REFERENCE (footer link "Trabalhe conosco") indica uma página institucional com vagas abertas e formulário de candidatura com upload de currículo.

O módulo permite:
- CRUD de vagas (abertas/fechadas)
- Recebimento de candidaturas com upload de currículo (PDF/DOCX)
- Área CMS para visualizar candidatos por vaga, baixar currículos, alterar status
- Notificação por e-mail quando nova candidatura chega

## 2. Estrutura de dados

### `v2_job_openings` — Vagas
| Campo | Tipo | Descrição |
|-------|------|-----------|
| id | uuid (string, PK) | UUID primário |
| title | string(255) | Título da vaga |
| slug | string(255) | Slug único para URL |
| type | string(50) | Tipo (CLT, PJ, Estágio, Jovem Aprendiz) |
| workload | string(100) | Carga horária (ex.: "40h semanais") |
| location | string(255) | Local (ex.: "Brasília/DF" ou "Remoto") |
| salary_range | string(100) | Faixa salarial (opcional) |
| description | text | Descrição completa da vaga (HTML ou Markdown) |
| requirements | text | Requisitos |
| benefits | text | Benefícios oferecidos |
| is_active | boolean | Vaga aberta/fechada |
| published_at | timestamp (nullable) | Data de publicação |
| closes_at | timestamp (nullable) | Data de encerramento |
| created_by | uuid (FK → users.id) | Quem criou |
| created_at | timestamp | |
| updated_at | timestamp | |
| deleted_at | timestamp (nullable) | Soft delete |

### `v2_job_applications` — Candidaturas
| Campo | Tipo | Descrição |
|-------|------|-----------|
| id | uuid (string, PK) | UUID primário |
| job_opening_id | uuid (FK → v2_job_openings.id) | Vaga |
| name | string(255) | Nome do candidato |
| email | string(255) | E-mail |
| phone | string(50) | Telefone |
| linkedin_url | string(255) | URL do LinkedIn |
| cover_letter | text | Carta de apresentação |
| resume_path | string(255) | Caminho do arquivo de currículo |
| status | enum('pending','reviewed','interviewed','accepted','rejected') | Status |
| notes | text | Observações internas |
| reviewed_by | uuid (FK → users.id) | Quem avaliou |
| reviewed_at | timestamp (nullable) | |
| created_at | timestamp | |
| updated_at | timestamp | |
| deleted_at | timestamp (nullable) | Soft delete |

## 3. Dependências

- **Core V2** (`V2ServiceProvider`) — auto-descoberta do módulo
- **Cartalyst Sentinel** — autenticação e permissões
- **Flash messages** (`laracasts/flash`) — feedback no CMS
- **Activity Logger Service** (`ActivityLoggerService`) — auditoria
- **UUID genérico** (`v2_uuid()`) — chaves primárias
- **Storage (Laravel Filesystem)** — upload de currículos (disco `local` ou `s3`)
- **Mail (Laravel Mail)** — notificação ao admin sobre nova candidatura
- **Módulo BasicPage** — template de página CMS (padrão)

## 4. Tabelas necessárias

```sql
CREATE TABLE v2_job_openings (
    id            VARCHAR(36) PRIMARY KEY,
    title         VARCHAR(255) NOT NULL,
    slug          VARCHAR(255) NOT NULL UNIQUE,
    type          VARCHAR(50),
    workload      VARCHAR(100),
    location      VARCHAR(255),
    salary_range  VARCHAR(100),
    description   TEXT,
    requirements  TEXT,
    benefits      TEXT,
    is_active     BOOLEAN DEFAULT TRUE,
    published_at  TIMESTAMP NULL,
    closes_at     TIMESTAMP NULL,
    created_by    VARCHAR(36) REFERENCES users(id),
    created_at    TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at    TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    deleted_at    TIMESTAMP NULL
);

CREATE TABLE v2_job_applications (
    id              VARCHAR(36) PRIMARY KEY,
    job_opening_id  VARCHAR(36) NOT NULL REFERENCES v2_job_openings(id),
    name            VARCHAR(255) NOT NULL,
    email           VARCHAR(255) NOT NULL,
    phone           VARCHAR(50),
    linkedin_url    VARCHAR(255),
    cover_letter    TEXT,
    resume_path     VARCHAR(255) NOT NULL,
    status          VARCHAR(20) DEFAULT 'pending',
    notes           TEXT,
    reviewed_by     VARCHAR(36) REFERENCES users(id),
    reviewed_at     TIMESTAMP NULL,
    created_at      TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at      TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    deleted_at      TIMESTAMP NULL
);
```

## 5. Arquivos necessários

```
V2/Modules/Institucional/TrabalheConosco/
├── Controllers/
│   ├── Cms/
│   │   ├── JobOpeningController.php
│   │   └── JobApplicationController.php
│   └── Portal/
│       └── TrabalheConoscoController.php
├── Models/
│   ├── JobOpening.php
│   └── JobApplication.php
├── Requests/
│   ├── Cms/
│   │   ├── StoreJobOpeningRequest.php
│   │   ├── UpdateJobOpeningRequest.php
│   │   └── UpdateApplicationStatusRequest.php
│   └── Portal/
│       └── StoreApplicationRequest.php
├── Mail/
│   ├── NewJobApplicationMail.php
│   └── ApplicationStatusUpdatedMail.php
├── Services/
│   └── RecruitmentService.php
├── Views/
│   ├── cms/
│   │   ├── openings/
│   │   │   ├── index.blade.php
│   │   │   ├── create.blade.php
│   │   │   └── edit.blade.php
│   │   └── applications/
│   │       ├── index.blade.php
│   │       ├── show.blade.php
│   │       └── edit.blade.php
│   └── portal/
│       ├── index.blade.php
│       ├── show.blade.php
│       └── success.blade.php
├── Migrations/
│   ├── 2025_01_01_000006_create_v2_job_openings_table.php
│   └── 2025_01_01_000007_create_v2_job_applications_table.php
├── Routes/
│   ├── cms.php
│   └── portal.php
└── ModuleProvider.php
```

## 6. Sugestão de implementação

1. **Migration `v2_job_openings`**: safe-rollback, slug unique.

2. **Migration `v2_job_applications`**: FKs para `v2_job_openings` e `users`.

3. **Model `JobOpening`**: UUID PK, soft deletes, fillable, casts (dates), slug auto via `Str::slug()`, relations: `hasMany(JobApplication::class)`.

4. **Model `JobApplication`**: UUID PK, soft deletes, fillable, `resume_path` não fillable (sistema), relations: `belongsTo(JobOpening::class)`, `belongsTo(User::class, 'reviewed_by')`.

5. **Controller CMS (`JobOpeningController`)**: CRUD com datas de publicação e encerramento, toggle ativo/inativo.

6. **Controller CMS (`JobApplicationController`)**: listagem com filtro por vaga e status, visualização com link para download do currículo, atualização de status + notes.

7. **Controller Portal (`TrabalheConoscoController`)**:
   - `index()`: lista vagas ativas com `JobOpening::where('is_active', true)->where(function($q) { $q->whereNull('closes_at')->orWhere('closes_at', '>=', now()); })->orderBy('published_at', 'desc')->get()`.
   - `show($slug)`: exibe detalhes da vaga + formulário de candidatura.
   - `apply($slug)`: recebe o formulário com upload de currículo validado (PDF, DOCX, max 5MB), salva, dispara `NewJobApplicationMail`, retorna view de sucesso.

8. **Requests Portal**: `StoreApplicationRequest` — nome obrigatório, email obrigatório + formato, currículo obrigatório (mimes:pdf,doc,docx, max:5120).

9. **Views CMS**: Bootstrap 5, tabela de vagas com badge ativo/inativo, tabela de candidatos por vaga, modal para atualizar status, botão de download de currículo.

10. **View Portal**: Lista de vagas abertas (cards). Página de detalhe com formulário de candidatura + upload de currículo.

11. **ModuleProvider**: implementa `ModuleProviderInterface`, sidebar com "Vagas" e "Candidaturas".

12. **Permissions**: `TrabalheConosco` (GET→read, POST/PUT/DELETE→write).

## 7. Detalhamento de Implementação V2

### a) ModuleProvider.php

```php
<?php

namespace V2\Modules\Institucional\TrabalheConosco;

use V2\Core\Contracts\ModuleProviderInterface;

class ModuleProvider implements ModuleProviderInterface
{
    public function __construct()
    {
        $app = app();
    }

    public function getConfig(): array
    {
        return [
            'title'          => 'Trabalhe Conosco',
            'description'    => 'Gestão de vagas e candidaturas.',
            'icon'           => 'bi bi-briefcase',
            'position'       => 8,
            'active_pattern' => ['cms/institucional/trabalhe-conosco*'],
            'route'          => 'v2.trabalhe-conosco.cms.openings',

            'subModulesLevel1' => [
                [
                    'title'    => 'Vagas',
                    'icon'     => 'bi bi-card-list',
                    'route'    => 'v2.trabalhe-conosco.cms.openings',
                    'position' => 1,
                ],
                [
                    'title'    => 'Candidaturas',
                    'icon'     => 'bi bi-people',
                    'route'    => 'v2.trabalhe-conosco.cms.applications',
                    'position' => 2,
                ],
            ],
        ];
    }
}
```

### b) Migrations

**Migration 1 — `v2_job_openings`:**

```php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class CreateV2JobOpeningsTable extends Migration
{
    public function up()
    {
        DB::beginTransaction();
        DB::statement("SET lock_timeout = '3s';");

        try {
            Schema::create('v2_job_openings', function (Blueprint $table) {
                $table->uuid('id')->primary();
                $table->string('title', 255);
                $table->string('slug', 255)->unique();
                $table->string('type', 50)->nullable();
                $table->string('workload', 100)->nullable();
                $table->string('location', 255)->nullable();
                $table->string('salary_range', 100)->nullable();
                $table->text('description')->nullable();
                $table->text('requirements')->nullable();
                $table->text('benefits')->nullable();
                $table->boolean('is_active')->default(true);
                $table->timestamp('published_at')->nullable();
                $table->timestamp('closes_at')->nullable();
                $table->string('created_by', 36)->nullable();
                $table->timestamps();
                $table->softDeletes();

                $table->index('slug', 'idx_v2_job_openings_slug');
                $table->index('is_active', 'idx_v2_job_openings_active');
                $table->index('published_at', 'idx_v2_job_openings_published');
            });

            DB::commit();
            Log::info("[Migration] Tabela 'v2_job_openings' criada com sucesso.");
        } catch (\Exception $e) {
            DB::rollBack();
            Log::error("[Migration] ERRO AO CRIAR v2_job_openings: " . $e->getMessage());
            throw $e;
        }
    }

    public function down()
    {
        DB::beginTransaction();
        DB::statement("SET lock_timeout = '3s';");

        try {
            if (Schema::hasTable('v2_job_openings')) {
                $backupTable = 'backup_v2_job_openings_' . date('Y_m_d_His');
                Schema::rename('v2_job_openings', $backupTable);
                Log::alert("[Migration] Tabela 'v2_job_openings' preservada como '{$backupTable}'.");
            }
            DB::commit();
        } catch (\Exception $e) {
            DB::rollBack();
            Log::error("[Migration] ERRO AO REVERTER v2_job_openings: " . $e->getMessage());
            throw $e;
        }
    }
}
```

**Migration 2 — `v2_job_applications`:**

```php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class CreateV2JobApplicationsTable extends Migration
{
    public function up()
    {
        DB::beginTransaction();
        DB::statement("SET lock_timeout = '3s';");

        try {
            Schema::create('v2_job_applications', function (Blueprint $table) {
                $table->uuid('id')->primary();
                $table->uuid('job_opening_id');
                $table->string('name', 255);
                $table->string('email', 255);
                $table->string('phone', 50)->nullable();
                $table->string('linkedin_url', 255)->nullable();
                $table->text('cover_letter')->nullable();
                $table->string('resume_path', 255);
                $table->string('status', 20)->default('pending');
                $table->text('notes')->nullable();
                $table->string('reviewed_by', 36)->nullable();
                $table->timestamp('reviewed_at')->nullable();
                $table->timestamps();
                $table->softDeletes();

                $table->foreign('job_opening_id')->references('id')->on('v2_job_openings');
                $table->index('job_opening_id', 'idx_v2_job_applications_opening');
                $table->index('status', 'idx_v2_job_applications_status');
                $table->index('email', 'idx_v2_job_applications_email');
            });

            DB::commit();
            Log::info("[Migration] Tabela 'v2_job_applications' criada com sucesso.");
        } catch (\Exception $e) {
            DB::rollBack();
            Log::error("[Migration] ERRO AO CRIAR v2_job_applications: " . $e->getMessage());
            throw $e;
        }
    }

    public function down()
    {
        DB::beginTransaction();
        DB::statement("SET lock_timeout = '3s';");

        try {
            if (Schema::hasTable('v2_job_applications')) {
                Schema::table('v2_job_applications', function (Blueprint $table) {
                    $table->dropForeign(['job_opening_id']);
                });
                $backupTable = 'backup_v2_job_applications_' . date('Y_m_d_His');
                Schema::rename('v2_job_applications', $backupTable);
                Log::alert("[Migration] Tabela 'v2_job_applications' preservada como '{$backupTable}'.");
            }
            DB::commit();
        } catch (\Exception $e) {
            DB::rollBack();
            Log::error("[Migration] ERRO AO REVERTER v2_job_applications: " . $e->getMessage());
            throw $e;
        }
    }
}
```

### c) Models

**Model `JobOpening`:**

```php
<?php

namespace V2\Modules\Institucional\TrabalheConosco\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class JobOpening extends Model
{
    use SoftDeletes;

    protected $table = 'v2_job_openings';

    public $incrementing = false;

    protected $keyType = 'string';

    protected $fillable = [
        'id', 'title', 'slug', 'type', 'workload', 'location',
        'salary_range', 'description', 'requirements', 'benefits',
        'is_active', 'published_at', 'closes_at', 'created_by',
    ];

    protected $casts = [
        'is_active'    => 'boolean',
        'published_at' => 'datetime',
        'closes_at'    => 'datetime',
        'metadata'     => 'array',
    ];

    protected $dates = [
        'created_at', 'updated_at', 'deleted_at',
        'published_at', 'closes_at',
    ];

    protected static function boot()
    {
        parent::boot();

        static::creating(function ($model) {
            if (empty($model->id)) {
                $model->id = v2_uuid();
            }
            if (empty($model->slug) && !empty($model->title)) {
                $model->slug = str_slug($model->title);
            }
        });

        static::saving(function ($model) {
            // Garantir unicidade do slug
            $baseSlug = $model->slug ?: str_slug($model->title);
            $slug = $baseSlug;
            $counter = 1;
            if (!empty($model->id)) {
                while (static::where('slug', $slug)->where('id', '!=', $model->id)->exists()) {
                    $slug = $baseSlug . '-' . $counter;
                    $counter++;
                }
            } else {
                while (static::where('slug', $slug)->exists()) {
                    $slug = $baseSlug . '-' . $counter;
                    $counter++;
                }
            }
            $model->slug = $slug;
        });

        static::saved(function ($model) {
            try {
                app(\V2\Services\Search\Services\PublicIndexerService::class)->index(
                    new \V2\Services\Search\DTOs\PublicSearchDTO(
                        $model->title ?? $model->name,
                        $model->slug,
                        'TrabalheConosco',
                        'job_opening',
                        $model->id,
                        $model->description ?? '',
                        null,
                        ['tags' => []]
                    )
                );
            } catch (\Exception $e) {
                \Log::error('[Module] Falha ao sincronizar busca: ' . $e->getMessage());
            }
        });

        static::deleted(function ($model) {
            try {
                app(\V2\Services\Search\Services\PublicIndexerService::class)->remove('job_opening', $model->id);
            } catch (\Exception $e) {
                \Log::error('[Module] Falha ao remover da busca: ' . $e->getMessage());
            }
        });
    }

    public function applications()
    {
        return $this->hasMany(JobApplication::class, 'job_opening_id', 'id');
    }

    public function creator()
    {
        return $this->belongsTo(\App\Admin\User::class, 'created_by', 'id');
    }

    public function scopeActive($query)
    {
        return $query->where('is_active', true);
    }

    public function scopeOpen($query)
    {
        return $query->where('is_active', true)
            ->where(function ($q) {
                $q->whereNull('closes_at')
                  ->orWhere('closes_at', '>=', now());
            });
    }
}
```

**Model `JobApplication`:**

```php
<?php

namespace V2\Modules\Institucional\TrabalheConosco\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class JobApplication extends Model
{
    use SoftDeletes;

    protected $table = 'v2_job_applications';

    public $incrementing = false;

    protected $keyType = 'string';

    protected $fillable = [
        'id', 'job_opening_id', 'name', 'email', 'phone',
        'linkedin_url', 'cover_letter', 'status', 'notes',
        'reviewed_by', 'reviewed_at',
    ];

    protected $casts = [
        'reviewed_at' => 'datetime',
        'metadata'    => 'array',
    ];

    protected $dates = [
        'created_at', 'updated_at', 'deleted_at', 'reviewed_at',
    ];

    const STATUS_PENDING     = 'pending';
    const STATUS_REVIEWED    = 'reviewed';
    const STATUS_INTERVIEWED = 'interviewed';
    const STATUS_ACCEPTED    = 'accepted';
    const STATUS_REJECTED    = 'rejected';

    public static $validStatuses = [
        self::STATUS_PENDING,
        self::STATUS_REVIEWED,
        self::STATUS_INTERVIEWED,
        self::STATUS_ACCEPTED,
        self::STATUS_REJECTED,
    ];

    public static $statusLabels = [
        self::STATUS_PENDING     => 'Pendente',
        self::STATUS_REVIEWED    => 'Em Análise',
        self::STATUS_INTERVIEWED => 'Entrevistado',
        self::STATUS_ACCEPTED    => 'Aceito',
        self::STATUS_REJECTED    => 'Rejeitado',
    ];

    protected static function boot()
    {
        parent::boot();

        static::creating(function ($model) {
            if (empty($model->id)) {
                $model->id = v2_uuid();
            }
        });
    }

    public function jobOpening()
    {
        return $this->belongsTo(JobOpening::class, 'job_opening_id', 'id');
    }

    public function reviewer()
    {
        return $this->belongsTo(\App\Admin\User::class, 'reviewed_by', 'id');
    }

    public function scopeByStatus($query, $status)
    {
        return $query->where('status', $status);
    }

    public function scopePending($query)
    {
        return $query->where('status', self::STATUS_PENDING);
    }
}
```

### d) CMS Controllers

**`JobOpeningController`:**

```php
<?php

namespace V2\Modules\Institucional\TrabalheConosco\Controllers;

use V2\Core\Controllers\BaseController;
use Illuminate\Http\Request;
use V2\Modules\Institucional\TrabalheConosco\Models\JobOpening;
use V2\Services\ActivityLogger\ActivityLoggerService;
use V2\Services\SEO\Services\SEOService;
use Sentinel;

class JobOpeningController extends BaseController
{
    /** @var ActivityLoggerService */
    protected $activityLogger;

    /** @var SEOService */
    protected $seo;

    public function __construct(ActivityLoggerService $activityLogger, SEOService $seo)
    {
        $this->activityLogger = $activityLogger;
        $this->seo = $seo;
    }

    public function index(Request $request)
    {
        $query = JobOpening::query();

        if ($request->has('search')) {
            $searchTerm = $request->input('search');
            $query->where(function ($q) use ($searchTerm) {
                $q->where('title', 'ilike', '%' . $searchTerm . '%')
                  ->orWhere('type', 'ilike', '%' . $searchTerm . '%')
                  ->orWhere('location', 'ilike', '%' . $searchTerm . '%');
            });
        }

        $items = $query->orderBy('created_at', 'desc')->paginate(15);
        return view('v2-trabalhe-conosco::cms.openings.index', compact('items'));
    }

    public function create()
    {
        return view('v2-trabalhe-conosco::cms.openings.create');
    }

    public function store(Request $request)
    {
        $this->validate($request, [
            'title'       => 'required|string|max:255',
            'slug'        => 'nullable|string|max:255|unique:v2_job_openings,slug',
            'type'        => 'nullable|string|max:50',
            'workload'    => 'nullable|string|max:100',
            'location'    => 'nullable|string|max:255',
            'salary_range' => 'nullable|string|max:100',
            'description' => 'nullable|string',
            'requirements' => 'nullable|string',
            'benefits'    => 'nullable|string',
            'is_active'   => 'boolean',
            'published_at' => 'nullable|date',
            'closes_at'   => 'nullable|date|after_or_equal:published_at',
        ]);

        $model = new JobOpening();
        $model->id = v2_uuid();
        $model->fill($request->all());
        $model->created_by = Sentinel::getUser()->id ?? null;
        $model->save();

        $this->activityLogger->log($model, 'created', ['type' => 'job_opening_create']);

        flash('Vaga criada com sucesso!')->success();
        return redirect()->route('v2.trabalhe-conosco.cms.openings.edit', ['id' => $model->id]);
    }

    public function edit($id)
    {
        $model = JobOpening::findOrFail($id);
        return view('v2-trabalhe-conosco::cms.openings.edit', compact('model'));
    }

    public function update(Request $request, $id)
    {
        $model = JobOpening::findOrFail($id);

        $this->validate($request, [
            'title'       => 'required|string|max:255',
            'slug'        => 'nullable|string|max:255|unique:v2_job_openings,slug,' . $id . ',id',
            'type'        => 'nullable|string|max:50',
            'workload'    => 'nullable|string|max:100',
            'location'    => 'nullable|string|max:255',
            'salary_range' => 'nullable|string|max:100',
            'description' => 'nullable|string',
            'requirements' => 'nullable|string',
            'benefits'    => 'nullable|string',
            'is_active'   => 'boolean',
            'published_at' => 'nullable|date',
            'closes_at'   => 'nullable|date|after_or_equal:published_at',
        ]);

        $model->update($request->all());

        $this->activityLogger->log($model, 'updated', ['type' => 'job_opening_update']);

        flash('Vaga atualizada com sucesso!')->success();
        return redirect()->route('v2.trabalhe-conosco.cms.openings.edit', ['id' => $model->id]);
    }

    public function destroy($id)
    {
        $model = JobOpening::findOrFail($id);
        $title = $model->title;
        $model->delete();

        $this->activityLogger->log($model, 'archived', ['type' => 'job_opening_archive']);

        flash('Vaga "' . $title . '" arquivada com sucesso!')->success();
        return redirect()->route('v2.trabalhe-conosco.cms.openings');
    }
}
```

**`JobApplicationController`:**

```php
<?php

namespace V2\Modules\Institucional\TrabalheConosco\Controllers;

use V2\Core\Controllers\BaseController;
use Illuminate\Http\Request;
use V2\Modules\Institucional\TrabalheConosco\Models\JobApplication;
use V2\Modules\Institucional\TrabalheConosco\Models\JobOpening;
use V2\Services\ActivityLogger\ActivityLoggerService;
use V2\Services\SEO\Services\SEOService;
use Sentinel;
use Storage;

class JobApplicationController extends BaseController
{
    /** @var ActivityLoggerService */
    protected $activityLogger;

    /** @var SEOService */
    protected $seo;

    public function __construct(ActivityLoggerService $activityLogger, SEOService $seo)
    {
        $this->activityLogger = $activityLogger;
        $this->seo = $seo;
    }

    public function index(Request $request)
    {
        $query = JobApplication::with('jobOpening');

        if ($request->has('job_opening_id')) {
            $query->where('job_opening_id', $request->input('job_opening_id'));
        }

        if ($request->has('status') && in_array($request->input('status'), JobApplication::$validStatuses)) {
            $query->where('status', $request->input('status'));
        }

        $items = $query->orderBy('created_at', 'desc')->paginate(15);
        $openings = JobOpening::all();

        return view('v2-trabalhe-conosco::cms.applications.index', compact('items', 'openings'));
    }

    public function show($id)
    {
        $model = JobApplication::with('jobOpening')->findOrFail($id);
        return view('v2-trabalhe-conosco::cms.applications.show', compact('model'));
    }

    public function updateStatus(Request $request, $id)
    {
        $model = JobApplication::findOrFail($id);

        $this->validate($request, [
            'status' => 'required|string|in:' . implode(',', JobApplication::$validStatuses),
            'notes'  => 'nullable|string',
        ]);

        $oldStatus = $model->status;
        $model->status = $request->input('status');

        if ($request->has('notes')) {
            $model->notes = $request->input('notes');
        }

        if ($oldStatus === JobApplication::STATUS_PENDING && $model->status !== $oldStatus) {
            $model->reviewed_by = Sentinel::getUser()->id ?? null;
            $model->reviewed_at = now();
        }

        $model->save();

        $this->activityLogger->log($model, 'updated', [
            'type'       => 'application_status_update',
            'old_status' => $oldStatus,
            'new_status' => $model->status,
        ]);

        flash('Status da candidatura atualizado com sucesso!')->success();
        return redirect()->route('v2.trabalhe-conosco.cms.applications.show', ['id' => $model->id]);
    }

    public function downloadResume($id)
    {
        $model = JobApplication::findOrFail($id);

        if (!$model->resume_path || !Storage::disk('local')->exists($model->resume_path)) {
            flash('Currículo não encontrado.')->error();
            return redirect()->route('v2.trabalhe-conosco.cms.applications.show', ['id' => $id]);
        }

        return Storage::disk('local')->download($model->resume_path);
    }

    public function destroy($id)
    {
        $model = JobApplication::findOrFail($id);
        $name = $model->name;

        if ($model->resume_path) {
            Storage::disk('local')->delete($model->resume_path);
        }

        $model->delete();

        $this->activityLogger->log($model, 'archived', ['type' => 'application_archive']);

        flash('Candidatura de "' . $name . '" arquivada com sucesso!')->success();
        return redirect()->route('v2.trabalhe-conosco.cms.applications');
    }
}
```

### e) Portal Controller

```php
<?php

namespace V2\Modules\Institucional\TrabalheConosco\Controllers;

use V2\Core\Controllers\BaseController;
use Illuminate\Http\Request;
use V2\Modules\Institucional\TrabalheConosco\Models\JobOpening;
use V2\Modules\Institucional\TrabalheConosco\Models\JobApplication;
use V2\Services\SEO\Services\SEOService;
use Mail;
use Storage;

class TrabalheConoscoController extends BaseController
{
    /** @var SEOService */
    protected $seo;

    public function __construct(SEOService $seo)
    {
        $this->seo = $seo;
    }

    public function index()
    {
        $openings = JobOpening::open()
            ->orderBy('published_at', 'desc')
            ->get();

        $this->seo->setTitle('Trabalhe Conosco — ' . config('app.name'));
        $this->seo->setDescription('Confira nossas vagas abertas e candidate-se.');

        return view('v2-trabalhe-conosco::portal.index', compact('openings'));
    }

    public function show($slug)
    {
        $opening = JobOpening::where('slug', $slug)->open()->firstOrFail();

        $this->seo->setTitle($opening->title . ' — Trabalhe Conosco');
        $this->seo->setDescription(Str::limit(strip_tags($opening->description), 160));
        $this->seo->bindModel($opening);

        return view('v2-trabalhe-conosco::portal.show', compact('opening'));
    }

    public function apply(Request $request, $slug)
    {
        $opening = JobOpening::where('slug', $slug)->open()->firstOrFail();

        $this->validate($request, [
            'name'         => 'required|string|max:255',
            'email'        => 'required|email|max:255',
            'phone'        => 'nullable|string|max:50',
            'linkedin_url' => 'nullable|url|max:255',
            'cover_letter' => 'nullable|string|max:5000',
            'resume'       => 'required|file|mimes:pdf,doc,docx|max:5120',
        ]);

        $application = new JobApplication();
        $application->id = v2_uuid();
        $application->job_opening_id = $opening->id;
        $application->name = $request->input('name');
        $application->email = $request->input('email');
        $application->phone = $request->input('phone');
        $application->linkedin_url = $request->input('linkedin_url');
        $application->cover_letter = $request->input('cover_letter');
        $application->status = JobApplication::STATUS_PENDING;

        $path = $request->file('resume')->store('resumes', 'local');
        $application->resume_path = $path;

        $application->save();

        // Notifica admin
        try {
            Mail::send('v2-trabalhe-conosco::emails.new-application', [
                'application' => $application,
                'opening'     => $opening,
            ], function ($m) {
                $m->to(config('v2.support.email'))
                  ->subject('Nova candidatura — ' . config('app.name'));
            });
        } catch (\Exception $e) {
            \Log::error('[TrabalheConosco] Falha ao enviar notificação: ' . $e->getMessage());
        }

        return view('v2-trabalhe-conosco::portal.success', compact('opening'));
    }
}
```

### f) Rotas CMS (`Routes/cms.php`)

```php
<?php

Route::group([
    'middleware' => [\V2\Core\Middleware\CmsAuthenticate::class],
    'prefix'     => 'cms/institucional/trabalhe-conosco',
], function () {

    // Vagas
    Route::get('/openings', [
        'as'   => 'v2.trabalhe-conosco.cms.openings',
        'uses' => 'JobOpeningController@index',
    ]);

    Route::get('/openings/novo', [
        'as'   => 'v2.trabalhe-conosco.cms.openings.create',
        'uses' => 'JobOpeningController@create',
    ]);

    Route::post('/openings/novo', [
        'as'   => 'v2.trabalhe-conosco.cms.openings.store',
        'uses' => 'JobOpeningController@store',
    ]);

    Route::get('/openings/editar/{id}', [
        'as'   => 'v2.trabalhe-conosco.cms.openings.edit',
        'uses' => 'JobOpeningController@edit',
    ]);

    Route::match(['PUT', 'POST'], '/openings/editar/{id}', [
        'as'   => 'v2.trabalhe-conosco.cms.openings.update',
        'uses' => 'JobOpeningController@update',
    ]);

    Route::delete('/openings/{id}', [
        'as'   => 'v2.trabalhe-conosco.cms.openings.destroy',
        'uses' => 'JobOpeningController@destroy',
    ]);

    // Candidaturas
    Route::get('/applications', [
        'as'   => 'v2.trabalhe-conosco.cms.applications',
        'uses' => 'JobApplicationController@index',
    ]);

    Route::get('/applications/{id}', [
        'as'   => 'v2.trabalhe-conosco.cms.applications.show',
        'uses' => 'JobApplicationController@show',
    ]);

    Route::match(['PUT', 'POST'], '/applications/{id}/status', [
        'as'   => 'v2.trabalhe-conosco.cms.applications.status',
        'uses' => 'JobApplicationController@updateStatus',
    ]);

    Route::get('/applications/{id}/download', [
        'as'   => 'v2.trabalhe-conosco.cms.applications.download',
        'uses' => 'JobApplicationController@downloadResume',
    ]);

    Route::delete('/applications/{id}', [
        'as'   => 'v2.trabalhe-conosco.cms.applications.destroy',
        'uses' => 'JobApplicationController@destroy',
    ]);
});
```

### g) Rotas Portal (`Routes/web.php`)

```php
<?php

Route::group(['middleware' => ['web']], function () {
    Route::get('/trabalhe-conosco', [
        'as'   => 'v2.trabalhe-conosco.portal.index',
        'uses' => 'TrabalheConoscoController@index',
    ]);

    Route::get('/trabalhe-conosco/{slug}', [
        'as'   => 'v2.trabalhe-conosco.portal.show',
        'uses' => 'TrabalheConoscoController@show',
    ]);

    Route::post('/trabalhe-conosco/{slug}/apply', [
        'as'   => 'v2.trabalhe-conosco.portal.apply',
        'uses' => 'TrabalheConoscoController@apply',
    ]);
});
```

### h) View CMS — openings/index.blade.php

```blade
@extends('v2-cms::layouts.app')

@section('title', 'Vagas')
@section('header-title', 'Vagas')
@section('header-description', 'Gerenciamento de vagas abertas.')

@section('content')
    <div class="d-flex justify-content-between align-items-center mb-3">
        <form method="GET" class="d-flex gap-2">
            <input type="text" name="search" class="form-control" placeholder="Buscar vagas..." value="{{ request('search') }}">
            <button class="btn btn-outline-secondary"><i class="bi bi-search"></i></button>
        </form>
        @v2can('TrabalheConosco', 'write')
            <a href="{{ route('v2.trabalhe-conosco.cms.openings.create') }}" class="btn btn-primary">
                <i class="bi bi-plus-circle"></i> Nova Vaga
            </a>
        @endv2can
    </div>

    <div class="card">
        <div class="card-body">
            @if(count($items) > 0)
                <table class="table">
                    <thead>
                        <tr>
                            <th>Título</th>
                            <th>Tipo</th>
                            <th>Local</th>
                            <th>Candidaturas</th>
                            <th>Status</th>
                            <th>Publicação</th>
                            <th>Ações</th>
                        </tr>
                    </thead>
                    <tbody>
                        @foreach($items as $item)
                        <tr>
                            <td>{{ $item->title }}</td>
                            <td>{{ $item->type ?? '-' }}</td>
                            <td>{{ $item->location ?? '-' }}</td>
                            <td>{{ $item->applications()->count() }}</td>
                            <td>
                                <span class="badge bg-{{ $item->is_active ? 'success' : 'secondary' }}">
                                    {{ $item->is_active ? 'Aberta' : 'Fechada' }}
                                </span>
                            </td>
                            <td>{{ $item->published_at ? $item->published_at->format('d/m/Y') : '-' }}</td>
                            <td>
                                <a href="{{ route('v2.trabalhe-conosco.cms.openings.edit', ['id' => $item->id]) }}"
                                   class="btn btn-sm btn-outline-primary">
                                    <i class="bi bi-pencil"></i>
                                </a>
                            </td>
                        </tr>
                        @endforeach
                    </tbody>
                </table>
                {{ $items->links() }}
            @else
                <div class="text-center py-4 text-muted">
                    <i class="bi bi-inbox fs-1"></i>
                    <p class="mt-2">Nenhuma vaga encontrada.</p>
                </div>
            @endif
        </div>
    </div>
@endsection
```

### i) View Portal — index.blade.php

```blade
@extends('v2-portal::layouts.app')

@section('seo')
    @include('v2-portal::partials.seo')
@endsection

@section('content')
    <section class="jobs-section section-padding fix">
        <div class="container">
            <div class="text-center mb-5">
                <h2>Trabalhe Conosco</h2>
                <p class="text-muted">Confira nossas vagas abertas e faça parte da equipe SUCESU.</p>
            </div>
            <div class="row g-4">
                @forelse($openings as $opening)
                <div class="col-lg-6 wow fadeInUp">
                    <div class="job-card card">
                        <div class="card-body">
                            <div class="d-flex justify-content-between align-items-start">
                                <div>
                                    <h4>{{ $opening->title }}</h4>
                                    <p class="text-muted mb-2">
                                        <i class="bi bi-geo-alt"></i> {{ $opening->location ?? 'Não informado' }}
                                        &nbsp;|&nbsp; <i class="bi bi-clock"></i> {{ $opening->type ?? '-' }}
                                        &nbsp;|&nbsp; {{ $opening->workload ?? '-' }}
                                    </p>
                                </div>
                                @if($opening->salary_range)
                                    <span class="badge bg-primary">{{ $opening->salary_range }}</span>
                                @endif
                            </div>
                            <p class="mb-3">{{ Str::limit(strip_tags($opening->description), 200) }}</p>
                            <a href="{{ route('v2.trabalhe-conosco.portal.show', ['slug' => $opening->slug]) }}"
                               class="btn btn-primary">
                                Ver Detalhes e Candidatar-se
                            </a>
                        </div>
                    </div>
                </div>
                @empty
                <div class="col-12 text-center py-5">
                    <i class="bi bi-briefcase fs-1 text-muted"></i>
                    <p class="mt-3">Nenhuma vaga aberta no momento.</p>
                </div>
                @endforelse
            </div>
        </div>
    </section>
@endsection
```

### j) Dependências e Integrações

| Serviço | Como usar |
|---------|-----------|
| **ActivityLoggerService** | Injetado via construtor nos controllers CMS. Log em create, update, destroy. |
| **SEOService** | Injetado no PortalController. `setTitle()`, `setDescription()` nas páginas públicas. |
| **Sentinel** | `Sentinel::getUser()` para `created_by`, `reviewed_by` e activity log. |
| **Storage (local)** | Upload de currículos para `resumes/`, download via `Storage::disk('local')->download()`. |
| **Storage (public)** | Upload de logos se necessário. |
| **Mail (Laravel)** | Notificação por email ao admin quando nova candidatura chega. |
| **v2_uuid()** | Geração de UUIDs nos models (boot) e controllers. |
| **flash()** | Mensagens de sucesso/erro em todas as operações de escrita. |
| **v2_can()** | Controle de botões de ação nas views CMS. |
| **V2PermissionMiddleware** | Automático: GET → read, POST/PUT/DELETE → write para permissão `TrabalheConosco`. |
| **Super Admin** | Email em `config('v2.support.email')` ou role `super-admin` bypassa tudo. |
| **config('v2.support.email')** | Destinatário das notificações de nova candidatura. |
