# Terceirizados — Rotas e Controllers

> Módulo **existente apenas na `v5`**. Rotas em `v5/routes/basepro.php` (CMS),
> `v5/routes/web.php` (portal) e `v5/routes/breadcrumbs.php`.

---

## 1. Rotas do CMS (`/cms`)

Definidas em `v5/routes/basepro.php`, dentro do grupo
`Route::group(['prefix' => 'cms', 'before' => 'csrf'], ...)` (linha 35). O `Route::resource`
da linha 395 **não** está dentro do subgrupo `Route::middleware(['check.logged'])` que
encerra na linha 316 — a autenticação vem do middleware aplicado no construtor do
controller.

| Método | URI | Nome | Controller@action | Middleware |
|---|---|---|---|---|
| GET | `/cms/terceirizados` | `terceirizados.index` | `Prefecture\OutsourcedController@index` | `check.route.permission` (construtor) |
| GET | `/cms/terceirizados/create` | `terceirizados.create` | `Prefecture\OutsourcedController@create` | idem |
| POST | `/cms/terceirizados` | `terceirizados.store` | `Prefecture\OutsourcedController@store` | idem + `csrf` |
| GET | `/cms/terceirizados/{id}` | `terceirizados.show` | `Prefecture\OutsourcedController@show` | idem |
| GET | `/cms/terceirizados/{id}/edit` | `terceirizados.edit` | `Prefecture\OutsourcedController@edit` | idem |
| PUT/PATCH | `/cms/terceirizados/{id}` | `terceirizados.update` | `Prefecture\OutsourcedController@update` | idem + `csrf` |
| DELETE | `/cms/terceirizados/{id}` | `terceirizados.destroy` | `Prefecture\OutsourcedController@destroy` | idem + `csrf` |
| POST | `/cms/terceirizados/destroyAll` | `terceirizados.destroyAll` | `Prefecture\OutsourcedController@destroyAll` | idem + `csrf` |
| DELETE | `/cms/terceirizados/{id}/arquivo/{index}` | `terceirizados.destroy-arquivo` | `Prefecture\OutsourcedController@destroyArquivo` | idem + `csrf` |
| GET | `/cms/datatable/terceirizados` | `datatable.terceirizados` | `Prefecture\DatatableController@get_terceirizados` | apenas `csrf` (grupo `datatable`) |

Trechos reais:

```php
// v5/routes/basepro.php:395-397
Route::resource('terceirizados', 'Prefecture\OutsourcedController');
Route::post('terceirizados/destroyAll', 'Prefecture\OutsourcedController@destroyAll')->name('terceirizados.destroyAll');
Route::delete('terceirizados/{id}/arquivo/{index}', ['uses' => 'Prefecture\OutsourcedController@destroyArquivo'])->name('terceirizados.destroy-arquivo');
```

```php
// v5/routes/basepro.php:451 (grupo prefix 'datatable', as 'datatable.')
Route::get('/terceirizados', 'Prefecture\DatatableController@get_terceirizados')->name('terceirizados');
```

### Ordem de resolução

`terceirizados/destroyAll` (POST) é declarado **depois** do `Route::resource`. Como o
`store` do resource é `POST /cms/terceirizados` (segmento extra `destroyAll` não colide),
não há conflito. Já `DELETE /cms/terceirizados/{id}` e
`DELETE /cms/terceirizados/{id}/arquivo/{index}` se distinguem pelo número de segmentos.

## 2. Rotas do Portal (público)

Definidas em `v5/routes/web.php`, dentro do grupo
`Route::middleware(['check.site.manutention'])` (linha 5). **Sem autenticação.**

| Método | URI | Nome | Controller@action |
|---|---|---|---|
| GET | `/terceirizados` | `portal.terceirizados.index` | `Portal\OutsourcedController@index` |
| GET | `/terceirizados/{id}` | `portal.terceirizados.show` | `Portal\OutsourcedController@indexShow` |

```php
// v5/routes/web.php:123-125
##########Terceirizados##########################################################
Route::get('terceirizados', 'Portal\OutsourcedController@index')->name('portal.terceirizados.index');
Route::get('terceirizados/{id}', 'Portal\OutsourcedController@indexShow')->name('portal.terceirizados.show');
```

Parâmetros aceitos na rota do portal (query string): `ano`, `mes`, `funcao`, `busca`
(filtros) e `type` (`xls` | `pdf` | `csv` | `txt`, dispara a exportação).

## 3. Breadcrumbs

`v5/routes/breadcrumbs.php:213-223`:

```php
// Home > Terceirizados
Breadcrumbs::register('terceirizados', function ($breadcrumbs) {
    $breadcrumbs->parent('home');
    $breadcrumbs->push('Terceirizados', route('portal.terceirizados.index'));
});

// Home > Terceirizados > [Nome]
Breadcrumbs::register('terceirizados-show', function ($breadcrumbs, $name) {
    $breadcrumbs->parent('terceirizados');
    $breadcrumbs->push($name);
});
```

## 4. Controller do CMS — `Prefecture\OutsourcedController`

`v5/app/Http/Controllers/Prefecture/OutsourcedController.php`

| Método | Comportamento | Retorno |
|---|---|---|
| `__construct(IOutsourcedRepository)` | Aplica `check.route.permission` e injeta o repositório. | — |
| `index()` | Log de atividade "Listou Terceirizados" + `menuClicks('terceirizados')`. | `view('prefecture.outsourceds.index')` |
| `create()` | Instancia `new Outsourced()` (apenas para o formulário). | `view('prefecture.outsourceds.create')` |
| `store(OutsourcedRequest)` | Valida, salva anexos, grava `file_path` JSON, `is_show = true`. | redirect `terceirizados.index` + flash |
| `show($id)` | **Não mostra nada**: redireciona para a rota pública. | `redirect()->route('portal.terceirizados.show', $id)` |
| `edit($id)` | Busca pelo repositório; se não achar, flash de erro e volta à listagem. | `view('prefecture.outsourceds.edit')` |
| `update(OutsourcedRequest, $id)` | Mesma montagem do `store`, porém **soma** aos anexos existentes. | redirect + flash |
| `destroyArquivo($id, $index)` | Remove o item do JSON de `file_path`. | JSON `{success}` 200 / `{erro}` 404 / `{erro}` 500 |
| `destroy($id)` | *Soft delete* via repositório. | JSON `{success}` 200 / `{erro}` 500 |
| `destroyAll(Request)` | `return $this->outsourcedRepository->destroyAll($request->ids);` | JSON `true`/`false` |

Trechos críticos:

```php
// store()
$data = $request->only(['year', 'month', 'employee_name', 'company', 'role']);
$data['is_show'] = true;

if ($request->hasFile('arquivo')) {
    $items = [];
    $descs = $request->input('arquivo_desc', []);
    foreach ($request->file('arquivo') as $i => $file) {
        $path = $this->saveUpload($file, 'terceirizados', null, null, false);
        $items[] = ['path' => "terceirizados/{$path}", 'description' => $descs[$i] ?? ''];
    }
    $data['file_path'] = json_encode($items);
}

$this->outsourcedRepository->upInsert($data);
```

```php
// update() — preserva os anexos anteriores
$existing = $outsourced->file_path
    ? (json_decode($outsourced->file_path, true) ?: [['path' => $outsourced->file_path, 'description' => '']])
    : [];

$existing = array_map(function ($item) {
    return is_array($item) ? $item : ['path' => $item, 'description' => ''];
}, $existing);
```

```php
// destroyArquivo()
$arquivos = json_decode($outsourced->file_path, true) ?: [['path' => $outsourced->file_path, 'description' => '']];
if (!isset($arquivos[$index])) {
    return response()->json(['erro' => 'Arquivo não encontrado.'], 404);
}
array_splice($arquivos, $index, 1);
$outsourced->file_path = !empty($arquivos) ? json_encode(array_values($arquivos)) : null;
$outsourced->save();
```

### Auditoria

`index()` grava a atividade com o pacote `spatie/laravel-activitylog`:

```php
activity()->performedOn(new Outsourced())
    ->causedBy(Sentinel::getUser())
    ->withProperty('Detalhe', 'Listado por: ' . Sentinel::getUser()->name)
    ->log('Listou Terceirizados');
```

O model, por sua vez, registra automaticamente created/updated/deleted com a descrição
`"O Terceirizado {employee_name} foi {eventName}"`.

## 5. Controller do Portal — `Portal\OutsourcedController`

`v5/app/Http/Controllers/Portal/OutsourcedController.php`

| Método | Comportamento |
|---|---|
| `__construct(Outsourced $outsourced)` | Injeta o model (usado apenas como dependência do container). |
| `index(Request, FilterOutsourceds)` | Aplica filtros, ordena, materializa a collection, exporta se `type`, senão pagina (15) e renderiza a view do template ativo. |
| `private export($outsourceds, $type)` | Despacha para XLSX / PDF / CSV / TXT. |
| `private gerarTxt($outsourceds)` | Monta o TXT alinhado em pipes. |
| `private exportTxt($outsourceds, $filename)` | Devolve o TXT como download (`Content-Type: plain/txt`). |
| `indexShow($id)` | `Outsourced::where('id', $id)->where('is_show', true)->firstOrFail()` → ficha. |

```php
private function export($outsourceds, $type)
{
    if ($type == 'xls') {
        return Excel::download(new OutsourcedsExport($outsourceds), 'terceirizados.xlsx');
    } elseif ($type == 'pdf') {
        $pdf = PDF::loadView('exports.outsourceds', compact('outsourceds', 'pdf'));
        return $pdf->download('terceirizados.pdf');
    } elseif ($type == 'csv') {
        return Excel::download(new OutsourcedsExport($outsourceds), 'terceirizados.csv');
    } else {
        return $this->exportTxt($outsourceds, 'terceirizados.txt');
    }
}
```

O texto gerado por `gerarTxt()` (cabeçalho real; linha de dados ilustrativa):

```
Ano  | Mês        | Nome do Empregado                    | Empresa                              | Função
-----|------------|--------------------------------------|--------------------------------------|-------
2026 | Março      | Maria de Souza                        | Contratada Exemplo LTDA              | Auxiliar
```

## 6. Form Request — `OutsourcedRequest`

`v5/app/Http/Requests/OutsourcedRequest.php`

```php
public function authorize()
{
    return true;
}

public function rules()
{
    return [
        'year'          => 'required|integer|min:2000|max:'.(date('Y') + 1),
        'month'         => 'required|integer|min:1|max:12',
        'employee_name' => 'required|string|max:255',
        'company'       => 'required|string|max:255',
        'role'          => 'required|string|max:255',
        'arquivo.*'     => 'nullable|mimes:pdf',
    ];
}
```

| Campo | Regra | Efeito observável |
|---|---|---|
| `year` | `required\|integer\|min:2000\|max:{ano+1}` | Bloqueia anos fora da faixa. O limite superior é calculado **no momento em que a classe é carregada** (`date('Y') + 1`). |
| `month` | `required\|integer\|min:1\|max:12` | Mês numérico entre 1 e 12. |
| `employee_name` | `required\|string\|max:255` | Campo de texto obrigatório. |
| `company` | `required\|string\|max:255` | Idem. |
| `role` | `required\|string\|max:255` | Idem. |
| `arquivo.*` | `nullable\|mimes:pdf` | Cada anexo deve ser PDF. `nullable` aqui é inócuo (a regra se aplica aos itens, não ao array). |
| `arquivo_desc` | **sem regra** | Descrição livre dos anexos — não é validada nem sanitizada. |
| `is_show` | **sem regra** | Nunca chega do formulário; é forçado a `true` no controller. |

Nomes amigáveis (`attributes()`): Ano, Mês, Nome do Empregado, Empresa, Função,
Anexo / Arquivo.

> `authorize()` retorna `true`: a autorização real vem do middleware
> `check.route.permission`, não do Form Request. Na v7 isso deve virar uma Policy.

## 7. Repositório

Contrato `v5/app/Repositories/Contracts/IOutsourcedRepository.php`:

```php
interface IOutsourcedRepository
{
    public function all();
    public function find(int $id);
    public function upInsert($request, int $id = null);
    public function delete(int $id);
}
```

Implementação `v5/app/Repositories/Eloquent/Prefecture/Outsourced/OutsourcedRepositoryDefaultImpl.php`:

```php
protected function structureUpInsert($request): array
{
    return $request;
}
```

Ou seja: a "estrutura" é a própria request — nenhum filtro de campos. O `Repository`
base (`v5/app/Repositories/Repository.php`) aplica `dataTrait()` → `traitRequest()`, que
sanitiza apenas chaves especiais (`birth`, `currency`, `url`, `email`, `id`, …) e **deixa
intactas** `year`, `month`, `employee_name`, `company`, `role`, `file_path` e `is_show`.

```php
// Repository::upInsert()
$data = DB::transaction(function () use ($request, $id) {
    return $this->structureUpInsert($this->dataTrait($request));
});
return $this->getModel()->updateOrCreate(['id' => $id], $data);
```

> `updateOrCreate(['id' => null], $data)` no `store` equivale a um `INSERT`; no `update`
> o `$id` é usado como chave de busca.

## 8. Filtros do portal — `FilterOutsourceds`

`v5/app/Http/Filter/FilterOutsourceds.php`

| Parâmetro | Efeito |
|---|---|
| `ano` | `where('year', $request->ano)` |
| `mes` | `where('month', $request->mes)` |
| `busca` | `where( employee_name ilike %termo% or company ilike %termo% )` |
| `funcao` | `where('role', 'ilike', '%termo%')` |

Base: `Outsourced::where('is_show', true)` — o portal **nunca** mostra registros ocultos.

## 9. API Resources / Presenters

**Não existem.** O módulo não expõe API JSON nem usa `laracasts/presenter`. A única saída
JSON é o Yajra DataTables (`/cms/datatable/terceirizados`) e as respostas dos endpoints
de exclusão (`{success}` / `{erro}`).

## 10. Permissões exigidas

| Recurso | Permissão (Sentinel) | Onde é verificada |
|---|---|---|
| Ver a listagem | `terceirizados.index` | `check.route.permission` (nome da rota) + botão "Cadastrar" (`has_access(['terceirizados.create'])`). |
| Criar | `terceirizados.create` | `v5/resources/views/prefecture/outsourceds/index.blade.php` |
| Editar | `terceirizados.edit` | `struct_views()` → flag `edit` no JSON do DataTable. |
| Excluir | `terceirizados.delete` | `index.blade.php` (checkbox de lote) + `struct_views()`. |
| Acessar rota | derivação do nome da rota em `check.route.permission` | `v5/app/Http/Middleware/CheckRoutePermission.php` |

`has_access()` (`v5/app/Library/Helper/appHelpers.php:22`) retorna `true` para
`super-admin`; `resolve_permission_aliases()` **não** possui alias para `terceirizados`
(mapa atual: `obra-paralisada`, `obras-paralisadas`, `categorias-obras` → `obras`).

## 11. Inventário de rotas por nome

| Nome | Uso |
|---|---|
| `terceirizados.index` | redirect após store/update; link "Voltar" no formulário. |
| `terceirizados.create` | botão "Cadastrar Terceirizado". |
| `terceirizados.store` | `Form::open` do `create.blade.php`. |
| `terceirizados.edit` | botão de edição do DataTable (`terceirizados/{id}/edit`). |
| `terceirizados.update` | `Form::open` do `edit.blade.php`. |
| `terceirizados.destroy` | botão de exclusão do DataTable (`terceirizados/{id}`). |
| `terceirizados.destroyAll` | botão de exclusão em lote (`deleteByLoteSubmitButton`). |
| `terceirizados.destroy-arquivo` | botão de lixeira do anexo no `form.blade.php`. |
| `datatable.terceirizados` | fonte AJAX do DataTable do CMS. |
| `portal.terceirizados.index` | página pública de listagem + filtro + export. |
| `portal.terceirizados.show` | ficha pública e destino do `show()` do CMS. |
