---
id: MOD-PUBLICACOES-TRABALHECONOSCO-CMS
tipo: modulo
onda: 4
status: concluido
unidades: [MOD-PUBLICACOES-TRABALHECONOSCO]
fontes:
  - V2/Modules/Publicacoes/TrabalheConosco/
depende_de: [UI-CMS, SVC-CONTENTRICHEDITOR, SVC-MEDIA, CORE-AUTH, CORE-CMS]
consumidores: [UI-CMS, SVC-MAIL]
criticidade: media
dono: Trabalhe
---

# Publicações/TrabalheConosco — UI CMS

> Anexo do `spec.md`. Quatro telas: listagem de vagas, formulário de vaga (criar/editar),
> listagem de candidaturas e detalhe/gestão da candidatura. Nenhuma usa componente
> próprio: todas estendem `v2-cms::layouts.app` e reutilizam parciais de filtro, ação,
> breadcrumb, paginação e modal. A tela mais rica é o detalhe da candidatura (download de
> currículo + troca de status + remoção). **Com a flag `trabalheconosco` desligada, as
> quatro telas são inalcançáveis (404)** e o item nem aparece no sidebar.

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Dar ao operador do CMS o ciclo completo da vaga (publicar, editar, arquivar) e da candidatura (triar, anotar, baixar currículo, remover). |
| Entrada | `GET /openings`, `GET /openings/novo`, `GET /openings/editar/{id}`, `GET /applications`, `GET /applications/{id}`; query `search`, `status`, `job_opening_id`; formulários POST/PUT/DELETE. |
| Saída | HTML Bootstrap 5 com tabelas, formulário em card, badges de status, modal de confirmação, modal de sucesso, flash. |
| Efeitos colaterais | `confirm()` nativo na remoção de candidatura; modal Bootstrap na de vaga; `created_item` abre modal automaticamente; erros de validação não são exibidos. |
| Particularidade central | A listagem de vagas mostra badge “Ativa/Inativa” apenas por `is_active` (ignora `closes_at`) e testa `is_featured` — atributo inexistente, então o badge “Destaque” nunca aparece. |
| Como se integra | Rotas `v2.trabalhe-conosco.cms.*`; namespace `v2-trabalheconosco`; componentes `v2-service-contentricheditor` e `v2-service-media`; partials `v2-cms::*`; JS global do CMS (`media-input.js`, `content-rich-editor-bundle.js`, `cms-layout.js`). |
| Se removido, quebra | Não há outra UI para vagas/candidaturas; links do sidebar e do e-mail de notificação apontam para essas rotas. |
| Superfície pública | IDs `#confirmModal`, `#creationSuccessModal`, `#media-id-*`, `#media-alt-*`, `#input-description_detailed`; classes `.js-media-input`, `.content-rich-editor-wrapper`, `.filter-card`; atributos `data-confirm-*`, `data-picker="date"`. |

## 1. Efeito

### 1.1 O que o usuário consegue fazer

- **Listar vagas** com total, busca textual e tabela de 6 colunas; clicar no título ou no
  lápis abre a edição; clicar em “N candidatura(s)” abre a listagem filtrada
  (`openings/index.blade.php:25-118`).
- **Criar vaga** em um formulário único, com editor rico, dois seletores de mídia e dois
  date pickers (`openings/create.blade.php:17-31`; `_form.blade.php:1-97`).
- **Editar vaga** no mesmo formulário, já preenchido, com `_method=PUT`
  (`openings/edit.blade.php:17-31`).
- **Arquivar vaga** por ícone de lixeira/arquivo, com modal de confirmação
  (`openings/index.blade.php:89-105`).
- **Listar candidaturas** com total, filtros de vaga/status e tabela de 5 colunas
  (`applications/index.blade.php:23-125`).
- **Gerir candidatura**: ver dados, baixar currículo, abrir LinkedIn e e-mail, alterar
  status/observações e remover (`applications/show.blade.php:19-140`).

### 1.2 O que muda no sistema

| Ação de UI | Efeito | Fonte |
|---|---|---|
| Salvar vaga nova | cria linha + `created_item` na sessão + redireciona | `JobOpeningController.php:107-126` |
| Salvar edição | atualiza linha + flash `Vaga atualizada com sucesso!` | `JobOpeningController.php:184-199` |
| Arquivar | soft delete + flash `Vaga "{título}" removida com sucesso!` | `JobOpeningController.php:208-222` |
| Filtrar candidaturas | query string `status`/`job_opening_id`; paginação preserva (`appends`) | `JobApplicationController.php:41-49`; `applications/index.blade.php:114-118` |
| Atualizar status | grava status/notas/revisor + flash `Status da candidatura atualizado com sucesso!` | `JobApplicationController.php:84-103` |
| Remover candidatura | apaga arquivo + soft delete + flash | `JobApplicationController.php:112-135` |
| Abrir “Nova Vaga” | formulário vazio; nada persiste até salvar | `openings/create.blade.php:20` |

### 1.3 Contrato observável (UI → resultado)

| Interação | Resultado |
|---|---|
| Buscar “CLT” na listagem de vagas | `GET /openings?search=CLT`; resultados cujo título/tipo/local contêm “CLT” (case-insensitive via `ILIKE`). |
| Limpar filtros | link para `url()->current()` sem query (`filter-card.blade.php:81`). |
| Enviar formulário sem título | `302` back; nenhum erro visível (layout não renderiza `$errors`). |
| Criar vaga com sucesso | `302 /openings`; modal “Vaga criado com sucesso!” com botão “Voltar para Listagem”. |
| Desmarcar “Vaga Ativa” na edição | `is_active=false`; badge passa a “Inativa” na listagem. |
| Clicar no ícone de arquivar | abre `#confirmModal` com título “Arquivar Vaga”, corpo `Deseja arquivar a vaga "{título}"?`, botão “Arquivar”. |
| Confirmar arquivamento | `DELETE`; tabela sem a vaga; flash de sucesso. |
| Filtrar por status `accepted` | `GET /applications?status=accepted`; tabela só com “Aceito”. |
| Status inválido na query | filtro ignorado silenciosamente (`JobApplicationController.php:45-47`). |
| Abrir candidatura | detalhe com dados, link de currículo e formulário de status. |
| Remover candidatura | `confirm()` “Tem certeza que deseja remover esta candidatura? O currículo também será removido.”; depois `DELETE`. |

### 1.4 O que estas telas NÃO fazem

- Não exibem `$errors` de validação em nenhum lugar (nem globais, nem por campo).
- Não permitem ordenar por coluna, selecionar várias linhas (bulk), exportar nem
  restaurar registros arquivados.
- Não mostram data de publicação/encerramento na listagem, nem prévia da capa.
- Não permitem editar candidatura (nome/e-mail/carta) nem reenviar currículo.
- Não há tela de configuração/SEO da vaga: `metadata` não é editável.
- Não há paginação configurável (fixa em 20).

## 2. Abordagem

### 2.1 Estratégia em uma página

Um CRUD Blade “clássico” sobre `v2-cms::layouts.app`, com três seções (`page-breadcrumbs`,
`page-actions`, `content`). A listagem de vagas usa `filter-card` + tabela; o formulário
isola os campos em `_form.blade.php` compartilhado por criar/editar; as candidaturas usam
duas telas separadas. Toda a interatividade é Bootstrap 5 + dois componentes de serviço
(editor e mídia). Não há JS próprio do módulo.

```mermaid
flowchart LR
  M[Sidebar: Vagas / Candidaturas] --> L1[Listagem de vagas]
  L1 -->|Nova Vaga| F[Formulário criar]
  L1 -->|título/lápis| E[Formulário editar]
  F -->|POST| S[store]
  E -->|PUT| U[update]
  L1 -->|lixeira| D[destroy]
  L1 -->|N candidaturas| L2[Listagem de candidaturas]
  L2 -->|nome/olho| S2[Detalhe da candidatura]
  S2 -->|PUT status| US[updateStatus]
  S2 -->|DELETE| DC[destroy + arquivo]
```

### 2.2 Mecânica passo a passo

1. **Listagem de vagas**: controller monta query com `search` opcional, `withCount`,
   ordena por `created_at desc` e pagina 20 (`JobOpeningController.php:36-53`); a view
   calcula `$searchTerm` via `request()` e renderiza total, filtro, tabela e paginação
   (`openings/index.blade.php:1-37,114-118`).
2. **Formulário**: `_form.blade.php` resolve `$opening`/`$isEditing`
   (`:1-4`), preenche cada campo com `old()` ou valor do model, inclui editor rico
   (`:36-43`), dois `media-input` (`:45-69`), datas com `data-picker="date"` (`:71-84`),
   switch `is_active` (`:86-92`) e CSRF/method spoofing (`:94-97`).
3. **Criar**: view `create` define `$pageTitle='Nova Vaga'`, breadcrumb manual e form POST
   para `...openings.store` (`create.blade.php:1-31`).
4. **Editar**: view `edit` define título com o título da vaga e action PUT para
   `...openings.update` (`edit.blade.php:1-31`).
5. **Listagem de candidaturas**: a view carrega no topo os arrays estáticos do model e
   **todas** as vagas para o select (`index.blade.php:1-9`); o filtro é um form GET com
   slot para dois selects (`:31-57`); a tabela resolve cor/rótulo do status em PHP
   (`:84-98`).
6. **Detalhe**: view recebe `$application` com `jobOpening`; renderiza `dl` de dados,
   link do currículo via `v2_storage_url`, carta com `nl2br(e())`, form de status e form
   de remoção (`show.blade.php:32-139`).
7. **Retorno**: `flash()` do `laracasts/flash` é renderizado pelo layout; `created_item`
   é renderizado por `creation-success-modal`.

### 2.3 Decisões de design e por quê

- **Um formulário compartilhado** `[C]`: `_form.blade.php` evita duplicação entre criar e
  editar, mas não recebe `$errors` nem define `novalidate`; a validação HTML usa
  `required`/`maxlength`, que não cobrem `mimes`.
- **Editor e mídia por macro-componentes** `[C]`: o módulo não implementa upload; delega
  a `SVC-MEDIA`/`SVC-CONTENTRICHEDITOR`, e o form não tem `enctype` porque os componentes
  enviam por AJAX e gravam um UUID/JSON em hidden (`_form.blade.php:36-69`).
- **Status como select fixo com labels do model** `[C]`: a tela não conhece as regras de
  transição; qualquer valor é oferecido a qualquer momento (`show.blade.php:103-109`).
- **Cores de status hardcoded na view** `[C]`: mapa `pending→warning`, `reviewed→info`,
  `interviewed→primary`, `accepted→success`, `rejected→danger`, duplicado em listagem e
  detalhe (`index.blade.php:84-97`; `show.blade.php:22-28`).
- **Confirmações diferentes por entidade** `[C]`: vaga usa modal acessível reutilizável;
  candidatura usa `confirm()` nativo (`show.blade.php:132-135`).
- **Paginação preservando query** `[C]`: `appends(request()->except('page'))` mantém
  filtros ao navegar (`index.blade.php:116`; `openings/index.blade.php:116`).

### 2.4 Trechos de código pivô

```blade
{{-- V2/Modules/Publicacoes/TrabalheConosco/Views/cms/openings/index.blade.php:54-82
     — tabela, badge de status e o contador que dispara N+1. --}}
@foreach($openings as $opening)
    <tr>
        <td>
            <a href="{{ route('v2.trabalhe-conosco.cms.openings.edit', ['id' => $opening->id]) }}">
                {{ $opening->title }}
            </a>
            @if($opening->is_featured)
                <span class="badge bg-warning text-dark ms-1">Destaque</span>
            @endif
        </td>
        <td>{{ $opening->type ?? '-' }}</td>
        <td>{{ $opening->location ?? '-' }}</td>
        <td>
            @if($opening->is_active)
                <span class="badge bg-success">Ativa</span>
            @else
                <span class="badge bg-secondary">Inativa</span>
            @endif
        </td>
        <td>
            @php $appCount = $opening->applications()->count(); @endphp
            <a href="{{ route('v2.trabalhe-conosco.cms.applications', ['job_opening_id' => $opening->id]) }}">
                {{ $appCount }} candidatura(s)
            </a>
        </td>
```

`is_featured` não existe no model/tabela: a condição é sempre falsa. `applications()->count()`
roda por linha, apesar do `withCount('applications')` feito no controller. O link de
contagem leva à listagem de candidaturas já filtrada.

```blade
{{-- .../Views/cms/openings/_form.blade.php:71-84 — datas e o grid quebrado. --}}
<div class="row mb-3">
    <div class="col-md-6">
        <label for="published_at" class="form-label">Data de Publicação</label>
        <input type="text" id="published_at" name="published_at" class="form-control"
               data-picker="date"
               value="{{ old('published_at', $isEditing && $opening->published_at ? $opening->published_at->format('Y-m-d') : '') }}">

        <div class="col-md-6">
        <label for="closes_at" class="form-label">Data de Encerramento</label>
        <input type="text" id="closes_at" name="closes_at" class="form-control"
               data-picker="date"
               value="{{ old('closes_at', $isEditing && $opening->closes_at ? $opening->closes_at->format('Y-m-d') : '') }}">
    </div>
</div>
```

O segundo `col-md-6` está aninhado no primeiro e o fechamento de `div` não corresponde à
abertura: o campo “Data de Encerramento” fica dentro da coluna de publicação, e o grid
seguinte herda o desalinhamento. Funciona visualmente em telas largas, mas quebra em
`md`. Os inputs usam Flatpickr (`data-picker="date"`, formato `Y-m-d`, exibição `d/m/Y`)
inicializado por `cms-layout.js:65`.

```blade
{{-- .../Views/cms/openings/index.blade.php:89-105 — confirmação por modal. --}}
@php $formId = 'destroy-opening-' . $opening->id; @endphp
<form action="{{ route('v2.trabalhe-conosco.cms.openings.destroy', ['id' => $opening->id]) }}"
      method="POST" class="d-inline" id="{{ $formId }}"
      onsubmit="return confirm('Tem certeza que deseja realizar esta ação?')">
    {{ csrf_field() }}
    {{ method_field('DELETE') }}
    <button type="button" class="btn btn-sm btn-outline-danger"
            data-confirm-modal
            data-confirm-title="Arquivar Vaga"
            data-confirm-body="Deseja arquivar a vaga &quot;{{ $opening->title }}&quot;?"
            data-confirm-btn-text="Arquivar"
            data-confirm-btn-class="btn-danger"
            data-confirm-form-id="{{ $formId }}"
            title="Arquivar">
        <i class="bi bi-archive"></i>
    </button>
</form>
```

Há **duas** confirmações: o `onsubmit="return confirm(...)"` e o modal. Como o botão é
`type="button"`, o clique não dispara o submit; o modal submete o form por
`data-confirm-form-id` (`_confirm-modal.blade.php`), e aí o `confirm()` nativo também
aparece — o usuário confirma duas vezes. O texto do botão/action diz “Arquivar”, mas a
operação é soft delete.

```blade
{{-- .../Views/cms/applications/index.blade.php:84-98 — status e cor. --}}
@php
    $statusColor = [
        'pending' => 'warning',
        'reviewed' => 'info',
        'interviewed' => 'primary',
        'accepted' => 'success',
        'rejected' => 'danger',
    ];
    $color = isset($statusColor[$application->status]) ? $statusColor[$application->status] : 'secondary';
@endphp
<span class="badge bg-{{ $color }}">
    {{ isset($statusLabels[$application->status]) ? $statusLabels[$application->status] : $application->status }}
</span>
```

Os rótulos vêm de `JobApplication::$statusLabels` (`Pendente`, `Em Análise`,
`Entrevistado`, `Aceito`, `Rejeitado`), carregados no topo da view. Status desconhecido
cai em `secondary` e mostra o valor cru.

```blade
{{-- .../Views/cms/applications/show.blade.php:61-71 — link do currículo. --}}
<dt class="col-sm-4">Currículo</dt>
<dd class="col-sm-8">
    @if($application->resume_path)
        <a href="{{ v2_storage_url($application->resume_path) }}" target="_blank"
           class="btn btn-sm btn-outline-success">
            <i class="bi bi-download me-1" aria-hidden="true"></i> Baixar Currículo
        </a>
    @else
        -
    @endif
</dd>
```

O caminho foi gravado no disco `local` (`storage/app/resumes`), mas `v2_storage_url`
resolve o disco padrão `public` (`/storage/...`) — o link legado não corresponde ao
arquivo. **Não** corrija isso com symlink/espelhamento para `public/storage/resumes`:
isso exporia nome, e-mail, telefone, LinkedIn e currículo sem autenticação (LGPD). O
requisito de reimplementação é uma rota autenticada
(`CmsAuthenticate` + permissão `TrabalheConosco`, autorização por candidatura) que
responde `Storage::disk('local')->download($application->resume_path)` com
`Content-Disposition: attachment`, `X-Content-Type-Options: nosniff` e
`Cache-Control: private, no-store`; alternativa aceitável é URL assinada de curta
duração e uso único. Ver `spec.md` §3.4 e `LG-TRABALHECONOSCO-02`. O link legado também
abre em nova aba sem `rel="noopener"`.

### 2.5 Algoritmos, consultas e custos

- Listagem de vagas: 2 queries + N (20) contagens por linha; total exibido é o do
  paginador (`$openings->total()`).
- Listagem de candidaturas: 2 queries + `JobOpening::all()` na view (sem limite).
- Detalhe: 2 queries (candidatura + vaga).
- Nenhuma tela faz chamada AJAX própria; o editor pode buscar mídia
  (`/cms/v2/editor/search`) e upload (`/cms/v2/editor/upload`) por conta do serviço.
- Formulário: o editor serializa o AST no submit; nenhuma transformação no módulo.

## 3. Particularidades

### 3.1 O que só existe aqui

- **Duas confirmações no arquivamento de vaga** (modal + `confirm()`).
- **Badge “Destaque” morto** por atributo inexistente.
- **Formulário compartilhado sem exibição de erros** e com grid de datas quebrado.
- **Detalhe da candidatura como tela de decisão**: único ponto do CMS que mostra todos
  os dados pessoais e o currículo.
- **Select de vaga carregado com o catálogo inteiro** na listagem de candidaturas.

### 3.2 Invariantes e regras não óbvias

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| `RN-TRABALHECONOSCO-C01` | Nenhuma tela exibe `$errors`; validação falha silenciosamente na UI. | Usuário vê a mesma tela após erro. | `openings/_form.blade.php:1-97`; layout sem `$errors` |
| `RN-TRABALHECONOSCO-C02` | Badge de status da vaga usa só `is_active`. | Vaga vencida aparece “Ativa”. | `openings/index.blade.php:67-73` |
| `RN-TRABALHECONOSCO-C03` | `is_featured` é testado sem existir. | “Destaque” nunca renderiza. | `openings/index.blade.php:61-63` |
| `RN-TRABALHECONOSCO-C04` | Contagem de candidaturas é feita por linha, ignorando `applications_count`. | N+1 de até 20 queries. | `openings/index.blade.php:75-77`; `JobOpeningController.php:49` |
| `RN-TRABALHECONOSCO-C05` | Filtro de candidaturas ignora `search` (não há campo `search` no controller). | O campo “Buscar...” do card não filtra nada; o placeholder é “Buscar...”. | `applications/index.blade.php:31-36`; `JobApplicationController.php:37-51` |
| `RN-TRABALHECONOSCO-C06` | Status inválido na query é descartado sem erro. | Listagem mostra todos. | `JobApplicationController.php:45-47` |
| `RN-TRABALHECONOSCO-C07` | O select de status oferece todos os valores, sem transição. | É possível voltar de `accepted` para `pending`. | `applications/show.blade.php:103-109` |
| `RN-TRABALHECONOSCO-C08` | Notas são um textarea sem histórico; o valor atual é sempre sobrescrito. | Perde-se a anotação anterior. | `JobApplicationController.php:85-86`; `show.blade.php:112-115` |
| `RN-TRABALHECONOSCO-C09` | Datas são exibidas `d/m/Y H:i` (listagem) e `d/m/Y H:i:s` (detalhe). | Formatos diferentes entre telas. | `applications/index.blade.php:100`; `show.blade.php:74,78` |
| `RN-TRABALHECONOSCO-C10` | O botão de remover candidatura usa `confirm()` nativo, não o modal do CMS. | Experiência inconsistente e sem foco gerenciado. | `show.blade.php:132-135` |

### 3.3 Casos-limite e comportamentos surpreendentes

- **Vaga sem candidaturas**: coluna mostra “0 candidatura(s)” e continua linkando para a
  listagem (que exibirá o vazio “Nenhum candidato se inscreveu até o momento.”).
- **Listagem de candidaturas vazia**: dois textos diferentes — “Nenhuma candidatura
  encontrada” + “Nenhum candidato se inscreveu até o momento.”, mesmo quando há filtro
  ativo (`applications/index.blade.php:119-124`). Não há botão “Limpar filtros” nesse
  vazio (diferente da listagem de vagas).
- **Busca sem resultados em vagas**: mostra “Tente ajustar os filtros ou limpar a busca.”
  + botão “Limpar Filtros”; sem busca, “Nenhuma vaga cadastrada ainda.”
  (`openings/index.blade.php:119-131`).
- **Formulário de edição de vaga inexistente**: `findOrFail` → 404 antes de renderizar.
- **Candidatura inexistente**: idem.
- **Data de publicação vazia na edição**: o input fica vazio; ao salvar, o controller grava
  `null` (a vaga perde a data).
- **Switch “Vaga Ativa”**: no create vem marcado (`old(..., true)`); no edit reflete o
  banco. Desmarcado, o campo não é enviado e o controller assume `false` no update.
- **Editor rico vazio**: o input hidden recebe o default `{"type":"doc","content":[]}` do
  componente (`content-rich-editor.blade.php:1-6`), não string vazia.
- **Mídia atual**: se `coverImage`/`attachmentPdf` não resolverem (UUID órfão), o
  componente mostra vazio e salvar mantém o UUID antigo se o input hidden não for limpo.
- **Nome do arquivo**: o detalhe não mostra o nome original do currículo; só o botão.

### 3.4 Armadilhas de reimplementação

- **Copiar `onsubmit="confirm(...)"` + modal**: gera dupla confirmação. Escolha um
  mecanismo (o modal é o padrão do CMS).
- **Renderizar `$errors` só no formulário de vaga**: o layout atual não tem bloco global;
  adicionar melhora a UX, mas é mudança de contrato visual — registre como decisão.
- **Trocar o select de vagas por AJAX**: hoje ele carrega todas as vagas; mudar o
  comportamento altera o filtro por `job_opening_id`.
- **Usar `$opening->applications_count`**: é o correto para performance, mas o texto da
  coluna deve continuar “N candidatura(s)”.
- **Corrigir o grid de datas**: o markup atual é inválido; uma reimplementação deve
  produzir duas colunas irmãs, mantendo ids/nomes `published_at`/`closes_at`.
- **Preservar `data-picker="date"`**: sem o atributo, o Flatpickr do CMS não inicializa e
  o campo vira texto livre, quebrando a conversão `' 00:00:00'`.

### 3.5 Divergências documentação × código e dívidas

- **Campo de busca de candidaturas é decorativo**: `filter-card` renderiza `search`, mas o
  controller não lê `search` (`applications/index.blade.php:31-36` vs
  `JobApplicationController.php:37-51`).
- **Textos “Arquivar” vs soft delete** (`openings/index.blade.php:97-103`).
- **`is_featured`** sem schema.
- **`$validStatuses` carregado em `show.blade.php:4` e não usado** (só `$statusLabels`).
- **Breadcrumbs inconsistentes**: listagem usa `v2-cms::partials.breadcrumb`; create/edit
  usam HTML manual com `›` (`openings/create.blade.php:9-15`; `edit.blade.php:9-15`).
- **`title` de página**: create/edit definem `$pageTitle` e passam em `['title' => ...]`;
  a listagem também, mas não usa `page-actions` na de candidaturas (não há “Nova
  Candidatura”, o que é intencional).

## 4. Integrabilidade

### 4.1 Superfície pública

| Símbolo | Tipo | Assinatura/atributos | Precondição | Consumidores |
|---|---|---|---|---|
| `v2-trabalheconosco::cms.openings.index` | view | recebe `$openings` paginado | rota + permissão | `openings` route |
| `v2-trabalheconosco::cms.openings.create` | view | sem dados | — | `openings.create` |
| `v2-trabalheconosco::cms.openings.edit` | view | recebe `$opening` | vaga existente | `openings.edit` |
| `v2-trabalheconosco::cms.openings._form` | parcial | `$opening` opcional; usa `old()` | dentro de form | create/edit |
| `v2-trabalheconosco::cms.applications.index` | view | recebe `$applications` paginado | — | `applications` |
| `v2-trabalheconosco::cms.applications.show` | view | recebe `$application` com `jobOpening` | candidatura existente | `applications.show` |
| `#confirmModal` | DOM | `data-confirm-title/body/btn-text/btn-class/form-id` | parcial incluída na listagem | qualquer botão de confirmação |
| `#creationSuccessModal` | DOM | depende de `session('created_item')` | pós-create | layout do CMS |

### 4.2 Contrato de UI/JS (quando aplicável)

- **Editor rico**: `#wrapper-description_detailed`, `#input-description_detailed`
  (hidden), toolbar com `data-command`; bundle
  `public/assets/js/content-rich-editor-bundle.js`; upload em `/cms/v2/editor/upload`,
  busca em `/cms/v2/editor/search` (do serviço, não do módulo).
- **Mídia**: `.js-media-input` com `data-name`, `data-owner`, `data-accept`,
  `data-no-picker`; hidden `#media-id-{name}` e `#media-alt-{name}`; dropzone
  `#drop-zone-{name}`; JS `public/assets/js/media-input.js` (jQuery + Gallery modal).
- **Flatpickr**: `[data-picker="date"]` inicializado por `cms-layout.js:65`, formato de
  envio `Y-m-d`, exibição `d/m/Y`.
- **Confirmação**: `[data-confirm-modal]` lido por script do CMS/partial; botão
  `type="button"`, submit via `data-confirm-form-id`.
- **Flash**: `laracasts/flash` (`flash('...')->success()`), renderizado pelo layout;
  classe/estrutura do alerta é do layout, não do módulo.

### 4.3 Ordem e ciclo de vida

1. Layout carrega Bootstrap 5, ícones Bootstrap Icons, jQuery e os bundles globais.
2. `media-input.js` e `content-rich-editor-bundle.js` inicializam no
   `DOMContentLoaded`/`ready` ao encontrar seus wrappers; múltiplas instâncias são
   suportadas por `data-name` (o form usa duas de mídia e uma de editor).
3. `cms-layout.js` inicializa Flatpickr para os campos de data.
4. Modal de sucesso é instanciado no `DOMContentLoaded` se `#creationSuccessModal`
   existir (`creation-success-modal.blade.php:20-30`).
5. Nenhum estado JS é persistido em localStorage; o estado do form vive nos inputs
   hidden.

### 4.4 Receitas de uso

**1. Adicionar uma coluna à listagem de vagas sem quebrar o layout:**

```blade
{{-- em openings/index.blade.php, após <th>Local</th> --}}
<th scope="col">Publicada em</th>
{{-- e na linha, após a célula de local --}}
<td>{{ $opening->published_at ? $opening->published_at->format('d/m/Y') : '-' }}</td>
```

**2. Reutilizar o formulário em outra tela:**

```blade
<form action="{{ route('v2.trabalhe-conosco.cms.openings.store') }}" method="POST">
    @include('v2-trabalheconosco::cms.openings._form')
    <button type="submit" class="btn btn-primary">Salvar Vaga</button>
</form>
{{-- o parcial já inclui csrf_field() e method_field('PUT') quando $opening->exists --}}
```

**3. Disparar o modal de confirmação para outro form:**

```blade
<button type="button" class="btn btn-danger"
        data-confirm-modal
        data-confirm-title="Remover"
        data-confirm-body="Confirma?"
        data-confirm-btn-text="Remover"
        data-confirm-btn-class="btn-danger"
        data-confirm-form-id="meu-form">Remover</button>
```

**4. Link para candidaturas já filtradas por vaga:**

```blade
<a href="{{ route('v2.trabalhe-conosco.cms.applications', ['job_opening_id' => $opening->id]) }}">
    {{ $opening->applications()->count() }} candidatura(s)
</a>
```

### 4.5 Anti-uso

- **Usar `@include('..._form')` fora de um `<form>`**: o parcial emite `csrf_field()` e
  `method_field('PUT')` soltos, gerando inputs órfãos no DOM.
- **Contar candidaturas com `$opening->applications()->count()` em laço grande**: repete
  o N+1; use `withCount`/`applications_count` (o controller já o fornece).
- **Assumir que o `confirm()` do `onsubmit` é a única confirmação**: o modal também
  submete; remover um sem o outro muda o fluxo.

### 4.6 Custos e limites da integração

- 20 linhas por página em ambas as listagens; sem ordenação alternativa.
- Select de vagas sem limite (cresce com o catálogo).
- Editor e mídia adicionam requisições próprias; o form não envia arquivos.
- Nenhuma tela funciona com a flag inativa; não há modo “somente leitura” visual além do
  RBAC `read` (que esconde ações só pelo bloqueio do request, não por ocultar botões).

## 5. Rotas e contratos HTTP

Não se aplica em detalhe: ver `rotas.md` §5.2. Resumo das telas:

| Tela | Rota | Métodos | Permissão |
|---|---|---|---|
| Listagem de vagas | `v2.trabalhe-conosco.cms.openings` | GET | read |
| Nova vaga | `...cms.openings.create` / `.store` | GET / POST | read / write |
| Editar vaga | `...cms.openings.edit` / `.update` | GET / PUT-POST | read / write |
| Arquivar vaga | `...cms.openings.destroy` | DELETE | write |
| Listagem de candidaturas | `...cms.applications` | GET | read |
| Detalhe da candidatura | `...cms.applications.show` | GET | read |
| Status | `...cms.applications.status` | PUT-POST | write |
| Remover candidatura | `...cms.applications.destroy` | DELETE | write |

## 6. Dados e modelo de domínio

Não se aplica: ver `dados.md`. A UI lê `JobOpening` (todos os campos do form + relações de
mídia) e `JobApplication` (todos os campos, incluindo `status`, `notes`, `reviewed_at`).

## 7. Fluxos e estados

```mermaid
stateDiagram-v2
  [*] --> ListaVagas
  ListaVagas --> FormCriar: Nova Vaga
  ListaVagas --> FormEditar: título/lápis
  FormCriar --> ListaVagas: salvar (302 + modal)
  FormEditar --> ListaVagas: salvar (302 + flash)
  ListaVagas --> ListaVagas: busca/limpar/paginar
  ListaVagas --> ListaCandidaturas: N candidaturas
  ListaVagas --> ListaVagas: arquivar (modal)
  ListaCandidaturas --> Detalhe: nome/olho
  ListaCandidaturas --> ListaCandidaturas: filtros/paginar
  Detalhe --> Detalhe: atualizar status (302 back)
  Detalhe --> ListaCandidaturas: remover (confirm)
```

## 8. UI CMS

### 8.1 Listagem de vagas — `GET /openings`

- **Layout**: `v2-cms::layouts.app`, título “Vagas”; breadcrumb Publicações › Trabalhe
  Conosco › Vagas; ação “Nova Vaga”.
- **Resumo**: `Gerencie as vagas de emprego do portal. Total: N vaga(s).`
- **Filtro** (`filter-card`): busca `search` com placeholder `Buscar por título, tipo ou
  local...`; ação para a própria rota; sem faixa de datas; sem filtros avançados.
- **Tabela** (`aria-label="Vagas"`): Título (link para edição; badge “Destaque” morto),
  Tipo (`-` quando nulo), Local (`-`), Status (badge `Ativa` verde / `Inativa` cinza),
  Candidaturas (link “N candidatura(s)”), Ações (lápis “Editar”; lixeira/arquivo com
  modal). Sem checkbox, sem bulk.
- **Ordenação**: fixa `created_at desc`; sem controle na UI.
- **Paginação**: `v2-cms::partials.pagination`, preservando `search`.
- **Vazio**: com busca → ícone, “Nenhuma vaga encontrada”, “Tente ajustar os filtros ou
  limpar a busca.”, botão “Limpar Filtros”; sem busca → “Nenhuma vaga cadastrada ainda.”.
- **Mensagens**: flash do layout; modal `Vaga criado com sucesso!`.
- **a11y**: `aria-label` na tabela e nos botões de ícone via `title`; ícones de editar/arquivar
  sem `aria-hidden` (`openings/index.blade.php:85-104`); o modal tem `aria-labelledby`,
  `aria-hidden` e botão fechar com `aria-label="Fechar"`.

### 8.2 Formulário de vaga — `GET /openings/novo` e `GET /openings/editar/{id}`

Campos (todos no card, em ordem):

| Campo | Tipo | Obrigatório | Default | Limite/validação | Ajuda/placeholder |
|---|---|---|---|---|---|
| `title` | text | sim | vazio / valor atual | `required|string|max:255`; HTML `required maxlength=255` | — |
| `type` | text | não | vazio | `max:50`; HTML `maxlength=50` | `Ex: CLT, PJ, Estágio` |
| `workload` | text | não | vazio | `max:100` | `Ex: 40h/semana` |
| `location` | text | não | vazio | `max:255` | `Ex: São Paulo, SP` |
| `salary_range` | text | não | vazio | `max:100` | `Ex: R$ 3.000 - R$ 5.000` |
| `description_detailed` | editor rico (hidden AST) | não | `{"type":"doc","content":[]}` | `nullable|string` | label “Descrição Detalhada” |
| `cover_image_id` | media-input | não | UUID atual | `nullable|string|max:36` | capa, `accept=image/*` |
| `attachment_pdf_id` | media-input | não | UUID atual | `nullable|string|max:36` | PDF, `accept=application/pdf` |
| `published_at` | date picker | não | hoje/atual | `nullable|date`; enviado `Y-m-d` | exibição `d/m/Y` |
| `closes_at` | date picker | não | vazio/atual | `nullable|date|after_or_equal:published_at` | — |
| `is_active` | switch | não | marcado no create / valor atual | `boolean` | “Vaga Ativa” |

- **Ações**: “Salvar Vaga” (create) / “Atualizar Vaga” (edit) e “Cancelar” (volta à
  listagem). Breadcrumb manual com link para a listagem.
- **CSRF/método**: `csrf_field()` sempre; `method_field('PUT')` quando `$opening->exists`.
- **Erros**: nenhum bloco de erro; `old()` preserva valores.
- **Estados**: em edição, datas formatadas `Y-m-d`, mídia atual com URL via
  `v2_storage_url`; formulário não tem estado “carregando”.
- **a11y**: todos os campos têm `<label for>`; o editor recebe `aria-labelledby`; o
  media-input usa `role="button"`, `tabindex=0` e instruções; o switch tem label.

### 8.3 Listagem de candidaturas — `GET /applications`

- **Layout**: breadcrumb parcial; sem `page-actions` (não há criação manual).
- **Resumo**: `Gerencie as candidaturas recebidas. Total: N candidatura(s).`
- **Filtros**: card com campo `search` (não consumido pelo controller — decorativo),
  select “Vaga” (`job_opening_id`, todas as vagas, sem escopo) e select “Status” (5
  rótulos), ambos com `aria-label`; `showDateRange=false`.
- **Tabela** (`aria-label="Candidaturas"`): Nome (link para detalhe), Email (texto puro),
  Vaga (título ou `-`), Status (badge colorido + rótulo), Data (`d/m/Y H:i`), ação “olho”
  (visualizar). Sem bulk.
- **Ordenação**: fixa `created_at desc`.
- **Paginação**: preserva filtros via `appends`.
- **Vazio**: “Nenhuma candidatura encontrada” + “Nenhum candidato se inscreveu até o
  momento.” (sempre os dois, mesmo com filtro).
- **a11y**: `aria-label` nos selects e na tabela; ícone do olho com `aria-hidden`.

### 8.4 Detalhe da candidatura — `GET /applications/{id}`

- **Cabeçalho**: badge grande de status (`fs-6`), cor conforme mapa.
- **Coluna esquerda (7/12)**: card “Dados do Candidato” com `dl`: Nome, Email (link
  `mailto:`), Telefone (`-` quando vazio), LinkedIn (quando presente, link em nova aba
  com `rel="noopener noreferrer"`), Vaga, Currículo (botão “Baixar Currículo”, que na
  reimplementação MUST apontar para rota autenticada de download, nunca `/storage`),
  Recebido em (`d/m/Y H:i:s`), Revisado em (quando presente) e, se houver, Carta de
  Apresentação com `nl2br(e(...))`.
- **Coluna direita (5/12)**: card “Atualizar Status” com select dos 5 status (atual
  selecionado) e textarea “Observações” (`rows=4`, valor atual), botão “Atualizar”;
  card “Remover Candidatura” com botão outline-danger e `confirm()`.
- **Mensagens**: flash `Status da candidatura atualizado com sucesso!` / `Candidatura de
  "{nome}" removida com sucesso!`.
- **a11y**: labels em todos os campos; ícones com `aria-hidden`; botão de remover sem
  `aria-label`, texto visível.

### 8.5 Mensagens exatas (inventário)

| Contexto | Texto | Fonte |
|---|---|---|
| Flash update vaga | `Vaga atualizada com sucesso!` | `JobOpeningController.php:197` |
| Flash delete vaga | `Vaga "{título}" removida com sucesso!` | `JobOpeningController.php:220` |
| Flash erro create | `Erro ao criar vaga. Tente novamente.` | `JobOpeningController.php:111` |
| Flash erro update | `Erro ao atualizar vaga. Tente novamente.` | `JobOpeningController.php:188` |
| Flash status | `Status da candidatura atualizado com sucesso!` | `JobApplicationController.php:101` |
| Flash delete candidatura | `Candidatura de "{nome}" removida com sucesso!` | `JobApplicationController.php:133` |
| Modal de sucesso | `{tipo} criado com sucesso!` / `{título}` / `Voltar para Listagem` | `creation-success-modal.blade.php:11-14` |
| Confirmação de vaga | `Arquivar Vaga` / `Deseja arquivar a vaga "{título}"?` / `Arquivar` | `openings/index.blade.php:97-101` |
| Confirmação nativa de vaga | `Tem certeza que deseja realizar esta ação?` | `openings/index.blade.php:92` |
| Confirmação de candidatura | `Tem certeza que deseja remover esta candidatura? O currículo também será removido.` | `applications/show.blade.php:133` |
| Vazio de vagas (busca) | `Nenhuma vaga encontrada` / `Tente ajustar os filtros ou limpar a busca.` / `Limpar Filtros` | `openings/index.blade.php:122-127` |
| Vazio de vagas (sem busca) | `Nenhuma vaga cadastrada ainda.` | `openings/index.blade.php:129` |
| Vazio de candidaturas | `Nenhuma candidatura encontrada` / `Nenhum candidato se inscreveu até o momento.` | `applications/index.blade.php:122-123` |
| Título das páginas | `Vagas`, `Nova Vaga`, `Editar Vaga: {título}`, `Candidaturas`, `Candidatura: {nome}` | views |

## 9. UI Portal

Não se aplica a este anexo (ver `portal.md`).

## 10. Configuração, permissões e flags

- Menu: item “Trabalhe Conosco” (posição 8, ícone `bi bi-briefcase`) com subitens “Vagas”
  e “Candidaturas”; `active_pattern` ilumina o item nas URIs do prefixo.
- RBAC: `read` esconde funcionalmente as escritas (requests bloqueados), mas os botões
  continuam visíveis — não há `@can`/condicional na UI.
- Flag: com `trabalheconosco.active=false`, o item some do sidebar e as rotas 404; não há
  aviso na tela (o usuário nunca chega a ela).
- Não há configuração de itens por página nem de tema específico.

## 11. Observabilidade, erros e limites

- Erros de validação não aparecem; erros de persistência mostram flash apenas em
  `store`/`update` de vaga.
- `findOrFail` gera 404 padrão do Laravel.
- Nenhum log é emitido por ações de UI bem-sucedidas além da auditoria.
- Limites: 20 linhas/página; uploads de mídia do serviço; `notes` ≤ 2000.

## 12. Seeds e dados iniciais

A UI não depende de seeds, mas o ambiente demo mostra 6 vagas (uma inativa/vencida) e 5
candidaturas, incluindo currículos apontando para arquivos inexistentes
(`DemoContentSeeder.php:1155-1195`). Na listagem, a vaga inativa aparece com badge
“Inativa” e a vencida com “Ativa”.

## 13. Lacunas

- `LG-TRABALHECONOSCO-C01` — Não há evidência de como o layout do CMS renderiza `$errors`
  (não há bloco); confirmar se um partial global é injetado por outro provider.
- `LG-TRABALHECONOSCO-C02` — Comportamento do modal `#confirmModal` com múltiplos forms na
  mesma página: o script não foi lido em detalhe; a implementação depende de
  `data-confirm-form-id`.
- `LG-TRABALHECONOSCO-C03` — Mensagens de validação em pt-BR: ausentes (ver
  `LG-TRABALHECONOSCO-01`).
- `LG-TRABALHECONOSCO-C04` — Contraste/cores dos badges dependem do tema Bootstrap
  customizado do CMS; não foi medido.

## 14. Checklist de paridade

- [ ] `PC-TRABALHECONOSCO-45` — Listagem de vagas mostra total, busca, tabela de 6 colunas, paginação preservando `search` e os dois vazios. Fonte: `openings/index.blade.php:25-132`.
- [ ] `PC-TRABALHECONOSCO-46` — Coluna Candidaturas conta por vaga e linka para `applications?job_opening_id={id}`. Fonte: `openings/index.blade.php:74-81`.
- [ ] `PC-TRABALHECONOSCO-47` — Badge “Destaque” não aparece (atributo inexistente) e status reflete só `is_active`. Fonte: `openings/index.blade.php:61-73`.
- [ ] `PC-TRABALHECONOSCO-48` — Formulário contém os 11 campos com os limites e placeholders da §8.2; create envia POST e edit envia POST+`_method=PUT`. Fonte: `_form.blade.php:6-97`.
- [ ] `PC-TRABALHECONOSCO-49` — Datas usam `data-picker="date"` e são enviadas `Y-m-d`; o controller acrescenta `00:00:00`. Fonte: `_form.blade.php:71-84`; `JobOpeningController.php:99-104`.
- [ ] `PC-TRABALHECONOSCO-50` — Arquivar abre modal “Arquivar Vaga” e submete `DELETE`; flash com o título. Fonte: `openings/index.blade.php:89-105`; `JobOpeningController.php:208-222`.
- [ ] `PC-TRABALHECONOSCO-51` — Listagem de candidaturas filtra por vaga/status e pagina preservando filtros. Fonte: `applications/index.blade.php:31-118`; `JobApplicationController.php:41-49`.
- [ ] `PC-TRABALHECONOSCO-52` — Campo “Buscar...” de candidaturas não altera o resultado (paridade com o legado). Fonte: `applications/index.blade.php:31-36`; `JobApplicationController.php:37-51`.
- [ ] `PC-TRABALHECONOSCO-53` — Detalhe exibe todos os campos, formata datas `d/m/Y H:i:s`, mostra carta com quebras e botão de currículo. Fonte: `applications/show.blade.php:32-89`.
- [ ] `PC-TRABALHECONOSCO-54` — Alterar status mantém `reviewed_at` quando o status anterior não era `pending`. Fonte: `JobApplicationController.php:88-91`.
- [ ] `PC-TRABALHECONOSCO-55` — Remover candidatura pede confirmação nativa com o texto exato e apaga o arquivo. Fonte: `applications/show.blade.php:129-136`.
- [ ] `PC-TRABALHECONOSCO-56` — Nenhuma tela renderiza erros de validação (paridade atual). Fonte: layout; `_form.blade.php`.
- [ ] `PC-TRABALHECONOSCO-57` — Com flag inativa, as quatro telas e o item de menu não são acessíveis. Fonte: `ModuleAccessMiddleware.php:46-62`; `SidebarComposer.php:24-26`.

## 15. Evidências consultadas

- `V2/Modules/Publicacoes/TrabalheConosco/Views/cms/openings/index.blade.php:1-135`
- `V2/Modules/Publicacoes/TrabalheConosco/Views/cms/openings/_form.blade.php:1-97`
- `V2/Modules/Publicacoes/TrabalheConosco/Views/cms/openings/create.blade.php:1-32`
- `V2/Modules/Publicacoes/TrabalheConosco/Views/cms/openings/edit.blade.php:1-32`
- `V2/Modules/Publicacoes/TrabalheConosco/Views/cms/applications/index.blade.php:1-126`
- `V2/Modules/Publicacoes/TrabalheConosco/Views/cms/applications/show.blade.php:1-141`
- `V2/Modules/Publicacoes/TrabalheConosco/Controllers/{JobOpeningController.php,JobApplicationController.php}`
- `V2/Modules/Publicacoes/TrabalheConosco/Models/JobApplication.php:54-80`
- `V2/layouts/cms/partials/{filter-card.blade.php:1-140,_confirm-modal.blade.php:1-30,creation-success-modal.blade.php:1-30}`
- `V2/Services/Media/Views/components/media-input.blade.php:1-60`
- `V2/Services/ContentRichEditor/Views/components/forms/content-rich-editor.blade.php:1-25`
- `public/assets/js/{media-input.js:1-40,cms-layout.js:60-70}`
- `V2/Services/Storage/StorageService.php:130-133`; `V2/Services/Storage/Adapters/LocalAdapter.php:85-96`
