# Terceirizados — Views e UX

> Módulo **existente apenas na `v5`**.

---

## 1. Inventário de telas

### CMS (AdminLTE 1.22, layout `admin.layouts.app`)

| Arquivo | Função |
|---|---|
| `v5/resources/views/prefecture/outsourceds/index.blade.php` | Listagem (DataTable AJAX), botão "Cadastrar Terceirizado", botão de exclusão em lote, colunas ID/Ano/Mês/Nome/Empresa/Função/Ações. |
| `v5/resources/views/prefecture/outsourceds/create.blade.php` | Wrapper do formulário de cadastro (`Form::open` → `terceirizados.store`, `files => true`). |
| `v5/resources/views/prefecture/outsourceds/edit.blade.php` | Wrapper do formulário de edição (`Form::open` → `terceirizados.update`, `method => PUT`). |
| `v5/resources/views/prefecture/outsourceds/form.blade.php` | Formulário compartilhado (Ano, Mês, Nome, Empresa, Função + bloco de anexos) + JS de upload/remoção. |

Componentes reutilizados do CMS:

| Componente | Uso |
|---|---|
| `v5/resources/views/prefecture/components/deleteByLoteSubmitButton.blade.php` | Botão "Excluir Registro(s) (0)" que chama `submitDeleteAll(route('terceirizados.destroyAll'))`. |
| `v5/resources/views/prefecture/components/deleteByLoteMasterCheckbox.blade.php` | Checkbox mestre `#checkAll` da seleção em lote. |

### Portal (Bootstrap 4, templates `modelo1`..`modelo6`)

| Arquivo | Função |
|---|---|
| `v5/resources/views/portal/layouts/modelo1/terceirizados/index.blade.php` | Página de listagem pública (inclui filtro e tabela). |
| `v5/resources/views/portal/layouts/modelo1/terceirizados/filtro.blade.php` | Card de filtros + botões de exportação. |
| `v5/resources/views/portal/layouts/modelo1/terceirizados/show.blade.php` | Ficha do terceirizado + card de downloads. |
| `v5/resources/views/portal/components/terceirizados/table.blade.php` | Tabela da listagem + paginação (**compartilhada por todos os templates**). |

O mesmo conjunto de três telas existe em `modelo2`, `modelo3`, `modelo4`, `modelo5` e
`modelo6` (18 arquivos Blade no total). As diferenças entre os templates são apenas de
casca:

| Diferença | Templates |
|---|---|
| `@extends('portal.layouts.modeloN.layout.layout')` | todos |
| `Breadcrumbs::view('portal.layouts.modeloN.layout.breadcrumb', ...)` | todos (em `modelo3`/`modelo4` o segundo argumento do breadcrumb não é passado) |
| `data-toggle` → `data-bs-toggle` (Bootstrap 5) | `modelo5`, `modelo6` (no `show.blade.php`) |
| `form-control` → `form-select` nos `<select>` | `modelo5`, `modelo6` (no `filtro.blade.php`) |
| `col-md-3` → `col-sm-3` | `modelo4` (no `filtro.blade.php`) |
| Botões "Limpar campos"/"Consultar" reordenados | `modelo5`, `modelo6` |

> **Os layouts `default` e `newsbit` NÃO possuem as views `terceirizados`.** Como a rota
> pública resolve a view por `portal.layouts.<layout>.terceirizados.index`, se
> `configurations.key = layout` apontar para `default`, `newsbit`, `barra-privacidade`,
> `busca-darias`, `calendar-events`, `contratos`, `diario-oficial`, `dynamics-forms` ou
> `surveys`, a página pública **quebra com erro 404**. Ver `08-GAPS-E-DIVIDAS.md`.

### Exportação

| Arquivo | Função |
|---|---|
| `v5/resources/views/exports/outsourceds.blade.php` | Tabela HTML usada **tanto** pelo Excel (`FromView`) quanto pelo PDF (DomPDF). |

## 2. O que o operador vê (CMS)

### 2.1 Listagem

Estrutura (AdminLTE "box box-success"), com cabeçalho:

```blade
<h3 class="box-title align-middle">Lista de Terceirizados</h3>
<div class="float-right">
    @if (has_access(['terceirizados.create']))
        <a href="{{ route('terceirizados.create') }}" class="btn btn-success font-weight-bold btn-sm"
            role="button"><i class="fa fa-plus"></i> Cadastrar Terceirizado</a>
    @endif
</div>
@if (has_access(['terceirizados.delete']))
    <div class="float-right pr-1">
        @component('prefecture.components.deleteByLoteSubmitButton', ['route' => 'terceirizados.destroyAll'])
        @endcomponent
    </div>
@endif
```

A tabela `#tabela_terceirizados` é preenchida por AJAX. As colunas do DataTable
(`v5/public/assets/js/custom/contents/terceirizados/datatables.js`):

```js
let collums = [
    { data: 'year', name: 'year', "width": "10%" },
    { data: 'mes_extenso', name: 'mes_extenso', "width": "15%" },
    { data: 'employee_name', name: 'employee_name', "width": "25%" },
    { data: 'company', name: 'company', "width": "20%" },
    { data: 'role', name: 'role', "width": "20%" },
    {
        data: null,
        orderable: false,
        searchable: false,
        render: function (data, type, row) {
            return `<div class="flex justify-center">`
                + createBTN(data.id, `terceirizados/${data.id}/edit`, data.edit, data.delete, `terceirizados/${data.id}`)
                + `</div>`;
        }
    },
];

runDatatables('terceirizados', collums, true, 'DESC', null, true);
```

Pontos observáveis:

- `mes_extenso` é uma coluna **calculada no servidor** em
  `v5/app/Http/Controllers/Prefecture/DatatableController.php:133`:
  `$item->mes_extenso = $months[$item->month] ?? $item->month;`
- A coluna `value` (numeração sequencial) é injetada por `runDatatables` a partir do
  `data: 'value'` que o controller preenche.
- `createBTN(...)` gera o botão de **editar** (`terceirizados/{id}/edit`) e o de
  **excluir** (`terceirizados/{id}`), ambos condicionados às flags `edit`/`delete`
  devolvidas por `struct_views()`.
- O último parâmetro `true` de `runDatatables` habilita a coluna de seleção para
  exclusão em lote.
- A barra de ferramentas do DataTable traz **Excel, PDF, CSV e TXT** (exportação
  client-side das colunas visíveis, sem passar pelo controller) — ver
  `v5/public/assets/js/custom/utilities/table.js`.
- Scripts carregados pela própria view:
  `v5/public/assets/js/custom/utilities/deleteAll.js` e
  `v5/public/assets/js/custom/contents/terceirizados/datatables.js`.

### 2.2 Formulário (create/edit)

Campos (todos com `old()` e exibição de erro por campo):

| Campo | Widget | Observações |
|---|---|---|
| `year` | `Form::number` | `min=2000`, `max=date('Y')+1`; default `date('Y')` no cadastro. |
| `month` | `Form::select` | Opções 1–12 com nomes em português; placeholder "Selecione o mês...". |
| `employee_name` | `Form::text` | `maxlength=255`. |
| `company` | `Form::text` | `maxlength=255`. |
| `role` | `Form::text` | `maxlength=255`. |
| `arquivo[]` | `<input type="file" accept=".pdf">` | Criado dinamicamente pelo botão "Adicionar Arquivo". |
| `arquivo_desc[]` | `<input type="text">` | Descrição livre do anexo (placeholder: "Informe a descrição (ex: Contrato, Aditivo)"). |

### 2.3 Upload de arquivo

O bloco de upload é **100 % client-side**: o botão "#btn-adicionar" clona um template
`<div class="data-itens">` e o insere após o último `.data-itens`; o botão
`.js-remove-itens` remove a linha do DOM. Nenhum upload AJAX — o formulário é enviado
inteiro, com `files => true`.

```js
$('#btn-adicionar').click(function () {
    var novoInput = $('<div class="data-itens" style="margin-top: 10px;">' +
        '<div class="row">' +
        '<div class="col-md-4 col-sm-12 mb-2">' +
        '<input type="file" name="arquivo[]" class="form-control" accept=".pdf">' +
        '</div>' +
        '<div class="col-md-4 col-sm-12 mb-2">' +
        '<input type="text" name="arquivo_desc[]" class="form-control" placeholder="Informe a descrição (ex: Contrato, Aditivo)">' +
        '</div>' +
        '<button type="button" class="js-remove-itens btn btn-sm btn-danger mb-2" title="Remover Linha" style="margin-left: 15px; height: 35px;">' +
        '<i class="fa fa-trash"></i>' +
        '</button>' +
        '</div>' +
        '</div>');

    novoInput.hide();
    $('.itens-wrapper .data-itens:last').after(novoInput);
    novoInput.fadeIn('fast');
    $('.description').css('display', 'block');
    return false;
});
```

Os arquivos **já enviados** são listados abaixo do formulário (bloco `#files-done`), com
nome (`basename`), descrição e três botões: download, visualizar (ambos apontando para
`url('storage/' . $item['path'])`) e **deletar** (AJAX `DELETE`).

### 2.4 Remoção de anexo (AJAX)

```js
function deleteTerceirizadoArquivo(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('terceirizados.destroy-arquivo', [$outsourced->id, $arqIndex])` —
o índice é a **posição** do anexo no array JSON (não um id estável): remover o primeiro
item renumera os demais.

### 2.5 Exclusão em lote

`v5/public/assets/js/custom/utilities/deleteAll.js` coleta os valores marcados em
`input[name="toDeleteAll[]"]` e envia:

```js
$.ajax({
    url: routeDelete,
    method: 'POST',
    dataType: 'json',
    data: {
        _token: $('meta[name="csrf-token"]').attr('content'),
        toDelete: toDelete
    },
    ...
});
```

> **Incompatibilidade real:** o controller lê `$request->ids`
> (`v5/app/Http/Controllers/Prefecture/OutsourcedController.php:157`), não `toDelete`.
> Todos os outros módulos do CMS (`ContentController`, `DiarioController`,
> `FilaEsperaSusController`, …) leem `$request->toDelete`. Ver `08-GAPS-E-DIVIDAS.md`.

### 2.6 Feedback ao operador

`flash()` (pacote `laracasts/flash`) com três desfechos:

| Situação | Mensagem |
|---|---|
| Cadastro OK | `Terceirizado cadastrado com sucesso.` (`->success()`) |
| Edição OK | `Terceirizado atualizado com sucesso.` (`->success()`) |
| Exceção no store/update | `Erro ao cadastrar terceirizado: {mensagem}` / `Erro ao atualizar terceirizado: {mensagem}` (`->error()`) + `redirect()->back()->withInput()` |
| Registro inexistente na edição | `Terceirizado não encontrado.` (`->error()`) |

## 3. O que o cidadão vê (Portal)

### 3.1 Listagem (`/terceirizados`)

Estrutura do `index.blade.php` (modelo1):

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

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

A tabela (`v5/resources/views/portal/components/terceirizados/table.blade.php`) mostra
Ano, Mês (nome extenso), Nome do Empregado, Empresa, Função e a ação "Detalhes" (ícone de
lupa) que leva a `route('portal.terceirizados.show', $item->id)`. O rodapé traz a
paginação (`$outsourceds->appends(request()->query())->links()`) e o resumo
"Exibindo X a Y de Z registros".

### 3.2 Filtros (`filtro.blade.php`)

Formulário `GET` para `portal.terceirizados.index` com quatro campos:

| Campo | Tipo | Origem dos valores |
|---|---|---|
| `ano` | `<select>` | `$years` = `Outsourced::where('is_show', true)->distinct()->orderBy('year','desc')->pluck('year','year')` |
| `mes` | `<select>` | Array fixo 1–12 com nomes em pt-BR (no controller). |
| `funcao` | `<select>` | `$roles` = `Outsourced::where('is_show', true)->distinct()->orderBy('role','asc')->pluck('role','role')` |
| `busca` | `<input type="text">` | Texto livre (nome do empregado **ou** empresa). |

Botões: "Consultar" (submit) e "Limpar campos" (JS que zera os quatro campos).

### 3.3 Exportação pelo portal

Aparece **apenas quando há registros** (`@if ($outsourceds->count() > 0)`):

```html
<small class="text-secondary">Salvar como:</small>
<button type="button" onclick="exportarTerceirizados('xls')" title="Exportar Excel">xls</button>
<button type="button" onclick="exportarTerceirizados('pdf')" title="Exportar PDF">pdf</button>
<button type="button" onclick="exportarTerceirizados('csv')" title="Exportar CSV">csv</button>
<button type="button" onclick="exportarTerceirizados('txt')" title="Exportar TXT">txt</button>
```

O JS **preserva os filtros atuais** e acrescenta o formato:

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

### 3.4 Ficha (`show.blade.php`)

Tabela vertical com Ano, Mês, Nome do Empregado, Empresa e Função. Se `file_path` existir,
um card "Arquivos para Download" lista cada anexo com ícone `far fa-file-pdf`, o texto da
descrição (ou o `basename` do arquivo, se a descrição estiver vazia) e link para
`url('storage/' . $arquivo['path'])` em `target="_blank"`. No rodapé, um link "Voltar"
para `portal.terceirizados.index`.

## 4. Caminho e URL dos arquivos anexados

| Etapa | Valor |
|---|---|
| Gravação | `FileTrait::saveUpload($file, 'terceirizados', null, null, false)` → `$file->storeAs('terceirizados', $nameFile)` |
| Diretório físico | `storage/app/public/terceirizados/` |
| Nome do arquivo | `uniqid('') . str_random(5) . '.' . $extensão`, em minúsculas (`FileTrait::getFileName`) |
| Caminho salvo no banco | `"terceirizados/{$nameFile}"` (prefixo incluso) |
| URL pública | `url('storage/' . $path)` → depende do symlink `public/storage` → `storage/app/public`. **O symlink não está versionado no repositório** (`v5/public/` não contém `storage`): ele precisa ser criado por `php artisan storage:link` no deploy, sob pena de 404 nos downloads. |
| Compressão | **desativada** (`$compress = false`), pois o módulo aceita apenas PDF e `FileTrait::compress` só processa imagens |

## 5. Exportação Excel/PDF/CSV/TXT

| Formato | Implementação | Arquivo gerado |
|---|---|---|
| XLSX | `maatwebsite/excel` com `OutsourcedsExport` (`FromView` + `ShouldAutoSize`) sobre `exports/outsourceds.blade.php` | `terceirizados.xlsx` |
| CSV | Mesma classe de exportação, extensão `.csv` | `terceirizados.csv` |
| PDF | `barryvdh/laravel-dompdf` com `PDF::loadView('exports.outsourceds', compact('outsourceds', 'pdf'))` | `terceirizados.pdf` |
| TXT | Montagem manual em `gerarTxt()` + `Response::make` com `Content-Type: plain/txt` | `terceirizados.txt` |

A view de exportação (`v5/resources/views/exports/outsourceds.blade.php`) mostra o
cabeçalho "Terceirizados - {nome da prefeitura}" e a data da pesquisa **somente** quando
a variável `$pdf` está definida (marcador usado pelo DomPDF); a versão Excel não tem
cabeçalho. As linhas são alternadas com fundo cinza (`#f2f2f2` / `#ffffff`) e, quando
não há dados, a tabela exibe "Nenhum dado encontrado.".

Detalhe importante: a exportação usa a **collection completa** (`$outsourceds_all`),
materializada antes da paginação — o arquivo gerado contém todos os registros que
casam com o filtro, não apenas a página atual.

## 6. Assets e JS

| Arquivo | Papel |
|---|---|
| `v5/public/assets/js/custom/contents/terceirizados/datatables.js` | Definição de colunas e chamada de `runDatatables`. |
| `v5/public/assets/js/custom/utilities/table.js` | `runDatatables()`, `createBTN()`, botões de exportação do DataTable. |
| `v5/public/assets/js/custom/utilities/deleteAll.js` | Exclusão em lote ( SweetAlert2 + AJAX). |
| `v5/resources/views/portal/components/terceirizados/table.blade.php` | Tabela compartilhada por todos os templates do portal. |

Não há polling, SSE nem WebSocket no módulo. Não há requisição AJAX no portal — os
filtros usam `GET` comum e as exportações navegações.

## 7. Acessibilidade e pontos de UI que merecem atenção

| Ponto | Situação |
|---|---|
| Confirmação de exclusão de anexo | `confirm()` nativo do navegador (não SweetAlert2, ao contrário do bulk delete). |
| Remoção de anexo por posição | A URL usa o índice; se dois anexos forem removidos em sequência sem recarregar, o segundo índice pode apontar para outro arquivo. |
| Tabela do CMS | `#tabela_terceirizados` não tem `aria-*` nem legenda; sem ordenação acessível. |
| Filtro do portal | Os `<select>` não têm `aria-label`; o rótulo é `<label for="ano">`etc. (associado corretamente). |
| Exportação do portal | Botões só ficam visíveis se houver registros — o usuário não descobre a funcionalidade antes de filtrar. |
| Responsividade | `modelo4` usa `col-sm-3` nos filtros; `modelo5`/`modelo6` usam `form-select` e `data-bs-*` (Bootstrap 5) enquanto o restante do portal é Bootstrap 4. |
