# Concursos e PSS — Views e UX

> Todas as views existem **apenas na v5** (`v5/resources/views/`).
> A v6 não possui nenhuma view deste módulo.

---

## 1. Inventário de views

### 1.1 CMS — `v5/resources/views/prefecture/concursos-pss/`

| Arquivo | Função |
|---|---|
| `index.blade.php` | Listagem em DataTable (AdminLTE). Inclui o JS do módulo. |
| `create.blade.php` | Wrapper do formulário de cadastro (`Form::open` → `concurso-pss.store`). |
| `edit.blade.php` | Wrapper do formulário de edição (`Form::open` PUT → `concurso-pss.update`). |
| `form.blade.php` | Formulário compartilhado entre create e edit (campos + bloco de anexos). |

Todas estendem `admin.layouts.app` (que por sua vez estende `adminlte::page`).
O `adminlte::page` é quem renderiza as mensagens de flash
(`v5/resources/views/vendor/adminlte/page.blade.php:380` → `@include('flash::message')`).

### 1.2 Portal — `v5/resources/views/portal/`

| Caminho | Função |
|---|---|
| `layouts/modelo1/concursos-pss/index.blade.php` | Listagem pública (tema 1). |
| `layouts/modelo1/concursos-pss/filtro.blade.php` | Card de filtros + botões de exportação. |
| `layouts/modelo1/concursos-pss/show.blade.php` | Detalhe do certame. |
| `layouts/modelo2..modelo6/concursos-pss/{index,filtro,show}.blade.php` | **Cópias** dos três arquivos para cada tema. |
| `components/concursos-pss/table.blade.php` | Tabela de resultados — **componente único**, compartilhado por todos os temas. |

**Duplicação:** existem 6 × 3 = **18** views de layout no portal. Comparando com o
`modelo1`, a diferença entre `modelo2`, `modelo3` e `modelo4` é **apenas** o
`@extends` e o namespace do breadcrumb:

```blade
@extends('portal.layouts.modelo1.layout.layout')   →  @extends('portal.layouts.modelo2.layout.layout')
{{ Breadcrumbs::view('portal.layouts.modelo1.layout.breadcrumb', 'concursos-pss') }}
                                                     →  {{ Breadcrumbs::view('portal.layouts.modelo2.layout.breadcrumb', 'concursos-pss') }}
```

`modelo5` e `modelo6` diferem **adicionalmente** no `filtro.blade.php`: trocaram
`float-right` por `d-flex align-items-center justify-content-end` e `text-right`
por `text-end` (ajustes de layout do Bootstrap 4).

O tema ativo é resolvido em tempo de execução por
`getTemplateActived()` (`v5/app/Http/Controllers/Admin/Controller.php:39-49`), que lê
`configurations.layout` e monta o nome da view:

```php
return view(
    'portal.layouts.' . $this->getTemplateActived() . '.concursos-pss.index',
    compact('title', 'result', 'anos', 'tipos', 'anoSelecionado', 'tipoSelecionado', 'busca')
);
```

Se o layout configurado não existir, o método **ativa a página de manutenção** e
lança `abort(404, 'view_inexistente')`.

### 1.3 Exportação

| Arquivo | Função |
|---|---|
| `v5/resources/views/exports/concursos-pss.blade.php` | Template único usado por Excel, CSV **e** PDF (mesma view; a presença de `$pdf` controla o cabeçalho). |

## 2. Tela do CMS — listagem

`v5/resources/views/prefecture/concursos-pss/index.blade.php`:

```blade
@if (has_access(['concurso-pss.create']))
    <a href="{{ route('concurso-pss.create') }}" class="btn btn-success font-weight-bold btn-sm" role="button">
        <i class="fa fa-plus" aria-hidden="true"></i> Cadastrar
    </a>
@endif
...
<table id="tabela_concurso-pss" class="table table-striped table-hover w-100">
    <thead>
        <tr>
            <th scope="col" width="5%">ID</th>
            <th scope="col">Tipo</th>
            <th scope="col">Número</th>
            <th scope="col">Ano</th>
            <th scope="col">Descrição</th>
            <th scope="col">Situação</th>
            <th scope="col" class="text-center" width="15%">Ações</th>
        </tr>
    </thead>
    <tbody>
        {{-- Preenchido via Datatable JS --}}
    </tbody>
</table>
```

O `<tbody>` é preenchido por AJAX; a busca e a paginação são **client-side**
(DataTables), não server-side. Consequência: todos os registros são baixados do
servidor em uma única resposta JSON.

### 2.1 JavaScript do DataTable

`v5/public/assets/js/custom/config/concursos-pss/datatable.js`:

```js
let collums = [
    { data: 'tipo',      render: function (data) { return `<div class='vertical-middle' style='padding:8px'>${data ?? '-'}</div>`; } },
    { data: 'numero',    render: ... },
    { data: 'ano',       render: ... },
    { data: 'descricao', render: function (data) {
        if (!data) return '-';
        const truncated = data.length > 80 ? data.substring(0, 80) + '...' : data;
        return `<div class='vertical-middle' style='padding:8px'>${truncated}</div>`;
    }},
    { data: 'situacao',  render: ... },
    { data: null, orderable: false, className: 'text-center', render: function (data) {
        return `<div class='flex justify-center'>`
            + createBTN(data.id, `concursos-pss/editar/${data.id}`, data.edit, data.delete, `concursos-pss/excluir/${data.id}`)
            + `</div>`;
    }}
];

runDatatables('concurso-pss', collums);
```

| Item | Valor |
|---|---|
| Helper | `runDatatables(url, columns, ordenavel, type_order, callback, deleteAll, customButtons)` — `v5/public/assets/js/custom/utilities/table.js:3` |
| URL do AJAX | `/cms/datatable/concurso-pss` (montada pelo helper) |
| Coluna "ID" | inserida pelo helper via `data: 'value'` (o `row_number()` da view) |
| Ordenação | coluna 0 (`value`) em ordem **ASC** — por padrão do helper |
| Exportações do CMS | Excel / PDF / CSV / TXT built-in do helper (`extend: 'excel'`, `'pdf'`, `'csv'` + botão TXT) |
| Busca | campo "Pesquisar" do DataTables, client-side |
| Exclusão | `addDelete()` (`table.js:357`) faz `myFetch(url, 'DELETE')` e recarrega o DataTable |
| Exclusão em lote | `deleteAll` **não** é usado neste módulo |

**URLs relativas nos botões:** `concursos-pss/editar/{id}` e
`concursos-pss/excluir/{id}` são resolvidas contra a URL atual
`/cms/concursos-pss` → `/cms/concursos-pss/editar/{id}`. Funciona, mas quebra se
a rota do CMS mudar deprofundidade ou ganhar barra final.

**XSS:** os valores são interpolados direto nos templates literais do JS
(`${data}`), sem escape. O Blade escapa na view, mas o JSON do DataTable é
renderizado por JS — um `descricao` com `<script>` seria interpretado.
Mitigação atual: o `descricao` é truncado em 80 chars, não sanitizado.

## 3. Tela do CMS — formulário

`v5/resources/views/prefecture/concursos-pss/form.blade.php` — campos:

| Campo | Componente | Observações |
|---|---|---|
| Tipo | `components.inputs.input` (`size` 5, `maxlen` 60) | Texto livre, placeholder "Ex: Concurso Público". |
| Número | `components.inputs.input` (`size` 3, `maxlen` 20) | Placeholder "Ex: 001". |
| Ano | `components.inputs.input` (`size` 2, `maxlen` 4) | Máscara JS `0000`. |
| Situação | `components.inputs.input` (`size` 4, `maxlen` 60) | Texto livre, placeholder "Ex: Inscrições Abertas". |
| Vagas Ofertadas | `components.inputs.input` (`size` 2) | Filtro JS que remove não-dígitos. |
| Vagas Preenchidas | `components.inputs.input` (`size` 2) | Idem. |
| Validade | `components.inputs.date` (`mask` `br-data`, `date_format` `d/m/Y`) | Valor exibido em `d/m/Y`. |
| Descrição | `<textarea rows="4">` | Campo HTML puro (não usa o componente `components.inputs.textarea`). |
| Arquivos | Bloco dinâmico | `input[type=file][name="arquivo[]"]` + `input[name="arquivo_desc[]"]`. |

Comportamento do bloco de anexos (JS inline na própria view):

```js
$('#btn-adicionar').click(function () {
    var novoInput = $('<div class="data-itens" ...>' +
        '<input type="file" name="arquivo[]" class="form-control" accept=".pdf">' +
        '<input type="text" name="arquivo_desc[]" class="form-control" placeholder="Informe a descrição (ex: Edital, Retificação)">' +
        ...
    novoInput.hide();
    $('.itens-wrapper .data-itens:last').after(novoInput);
    novoInput.fadeIn('fast');
    $('.description').css('display', 'block');
    return false;
});
```

- O botão **não** é `type="submit"` — usa `return false` para não submeter.
- A linha "Descrição" (cabeçalho do par arquivo/descrição) fica oculta
  (`display: none`) até o primeiro clique em "Adicionar Arquivo".
- Arquivos já enviados aparecem em `#files-done`, com download
  (`url('storage/' . path)`), visualização e botão de lixeira:

```js
function deleteConcursoArquivo(btn) {
    if (!confirm('Deseja realmente remover este arquivo?')) return;
    var $btn = $(btn);
    var url  = $btn.data('url');
    var row  = $btn.closest('.ajax-file-upload-statusbar');
    $.ajax({
        url: url,
        type: 'DELETE',
        data: { _token: $('meta[name="csrf-token"]').attr('content') },
        success: function () { row.fadeOut(300, function () { $(this).remove(); }); },
        error: function () { alert('Erro ao remover o arquivo. Tente novamente.'); }
    });
}
```

A URL vem de `route('concurso-pss.destroy-arquivo', [$concurso->id, $arqIndex])` —
o índice **posicional** do arquivo no array JSON.

### 3.1 Erros de validação

O form exibe `$errors->all()` no topo, cada um como
`<li class="font-weight-bold bg-red text-light px-2 my-1">{{ $error }}</li>`.

## 4. Portal — listagem

`v5/resources/views/portal/layouts/modelo1/concursos-pss/index.blade.php`:

```blade
@include('portal.layouts.modelo1.concursos-pss.filtro')
<br>
@include('portal.components.concursos-pss.table')

@if ($result->isEmpty())
    <div class="text-center mt-3">
        <p>Nenhuma informação encontrada!</p>
    </div>
@endif
```

### 4.1 Card de filtros (`filtro.blade.php`)

Formulário `GET` para `route('concursos-pss.index')` com três campos:

| Campo | Nome | Origem dos valores | Comportamento |
|---|---|---|---|
| Ano | `ano` | `$anos` — `ConcursoPss::selectRaw('ano')->distinct()->orderBy('ano','desc')->pluck('ano')` | `<select>` com "Todos" + anos existentes; mantém o selecionado via `$anoSelecionado`. |
| Tipo | `tipo` | `$tipos` — **lista fixa no controller** | `<select>` com "Todos" + 3 tipos fixos. |
| Busca | `busca` | texto livre | Filtra **apenas a descrição**. |

```php
$tipos = [
    'Concurso Público',
    'Processo Seletivo Simplificado',
    'Teste Seletivo',
];
```

> A lista de tipos é **hardcoded** no controller, enquanto o formulário do CMS
> aceita qualquer texto em `tipo`. Um certame cadastrado com tipo "Seleção de
> Residentes" aparece na listagem, mas **não** pode ser filtrado por tipo.

Botões: "Consultar" (submit) e "Limpar campos" (JS que zera os três campos sem
recarregar):

```js
function limparFiltrosConcursos() {
    $('select[name="ano"]').val('');
    $('select[name="tipo"]').val('');
    $('input[name="busca"]').val('');
}
```

### 4.2 Botões de exportação

```js
function exportarConcursos(type) {
    var url = '{{ route('concursos-pss.index') }}';
    var params = new URLSearchParams(window.location.search);
    params.set('type', type);
    window.location.href = url + '?' + params.toString();
}
```

O botão só aparece quando `$result->count() > 0`. O `type` substitui/insere o
parâmetro na query string atual, preservando os filtros ativos.

### 4.3 Tabela de resultados (`portal/components/concursos-pss/table.blade.php`)

| Coluna | Conteúdo |
|---|---|
| Tipo | `$concurso->tipo` |
| Número | `$concurso->numero` |
| Ano | `$concurso->ano` |
| Descrição | `$concurso->descricao` (texto completo, sem truncamento) |
| Situação | `$concurso->situacao` |
| Vagas | `ofertadas / preenchidas` (o `preenchidas` só aparece se não for nulo) |
| Validade | `d/m/Y` ou `-` |
| Detalhes | link para `route('concursos-pss.show', $concurso->slug)` |

Paginação (rodapé da tabela):

```blade
<span class="text-muted" style="font-size: 12px;">
    Exibindo {{ $result->firstItem() }} a {{ $result->lastItem() }} de {{ $result->total() }} registros
</span>
...
{{ $result->appends(request()->query())->links() }}
```

> `appends(request()->query())` é redundante: o `LengthAwarePaginator` já recebe
> `['query' => request()->query()]` na construção
> (`v5/app/Http/Controllers/Portal/ConcursosPssController.php:114-127`).

## 5. Portal — detalhe

`v5/resources/views/portal/layouts/modelo1/concursos-pss/show.blade.php`:

- Tabela com Tipo, Número, Ano, Descrição, Situação.
- Bloco "Vagas Ofertadas"/"Vagas Preenchidas" **apenas se** alguma das duas não for
  nula (`@if ($concurso->vagas_ofertadas !== null || $concurso->vagas_preenchidas !== null)`).
- Bloco "Validade" apenas se houver data.
- Card "Arquivos para Download" apenas se houver `arquivo`, listando
  `description` (ou o `basename` do path) com link `url('storage/' . $item['path'])`
  em `target="_blank"`.
- Breadcrumb: `"{tipo} nº {numero}/{ano}"`.
- Rodapé com link "Voltar" para `concursos-pss.index`.

## 6. Exportação — view `exports/concursos-pss.blade.php`

Usada por **três** motores (XLSX, CSV e PDF). Estrutura:

| Bloco | Comportamento |
|---|---|
| Cabeçalho | Só aparece quando `$pdf` está definido (`@if(isset($pdf))`) — título "Concursos e PSS - {app.name}" + data da pesquisa. **XLSX/CSV saem sem título.** |
| Tabela | 8 colunas: Tipo, Número, Ano, Descrição, Situação, Vagas Ofertadas, Vagas Preenchidas, Validade. |
| Corpo | Um `<tbody>` **por linha** (`@foreach` dentro do `<tbody>`) — HTML inválido. |
| Vazio | `<td colspan="5">Nenhum registro encontrado.</td>` — `colspan` errado (são 8 colunas). |

```blade
{{ $concurso->validade ? \Carbon\Carbon::parse($concurso->validade)->format('d/m/Y') : '-' }}
```

A classe `v5/app/Exports/ConcursosPssExport.php` implementa **apenas** `FromView`
(sem `ShouldAutoSize`, sem `WithHeadings`, sem `WithColumnFormatting`):

```php
class ConcursosPssExport implements FromView
{
    protected $concursos;

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

    public function view(): View
    {
        return view('exports.concursos-pss', ['concursos' => $this->concursos]);
    }
}
```

## 7. Exportação TXT

`gerarTxt()` monta um texto com separador `|` e cabeçalho fixo de 8 colunas; os
campos são concatenados **sem escape** (uma descrição com `|` quebra o alinhamento).
`exportTxt()` responde com `Content-Type: plain/txt` (tipo MIME não registrado) e
`Content-Length` calculado por `strlen()`.

## 8. Dependências de assets

| Recurso | Onde |
|---|---|
| AdminLTE 1.22 + Bootstrap 3 | CMS (`admin.layouts.app`). |
| Bootstrap 4 + Font Awesome 5 | Portal (`portal.layouts.modelo1..6`). |
| jQuery + jQuery Mask + DataTables (+ Buttons, pdfmake, JSZip) | `v5/resources/views/admin/layouts/app.blade.php`. |
| `toastr` | Notificações do `addDelete()` no JS do DataTable. |
| `breadcrumbs` (`tabuna/breadcrumbs` ou similar) | `v5/routes/breadcrumbs.php`. |

## 9. O que o cidadão vê vs. o que o operador vê

| | Cidadão | Operador |
|---|---|---|
| Telas | `/concursos-pss` e `/concurso-pss/{slug}` | `/cms/concursos-pss`, `.../cadastrar`, `.../editar/{id}` |
| Filtros | Ano, Tipo, busca por descrição | Busca e ordenação client-side do DataTable |
| Ações | Exportar (4 formatos), abrir detalhe | Criar, editar, excluir, anexar/remover PDFs |
| Download | PDFs do certame via `public/storage` | Idem + upload |
| Mensagens | — | Flash `laracasts/flash` (modal AdminLTE) + `toastr` no DataTable |
