# Estagiários — Rotas, Controladores e Validação

> Versão analisada: `v5`. O módulo não existe na `v6`.

---

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

Definidas em `v5/routes/basepro.php:329-336`, dentro do grupo
`Route::group(['prefix' => 'cms', 'before' => 'csrf'], ...)`
(`v5/routes/basepro.php:35`) e carregadas com o middleware `web`
(`v5/app/Providers/RouteServiceProvider.php`, método `mapBaseproRoutes`).

| Método | URI | Nome da rota | Controller@action | Middleware declarado na rota |
|---|---|---|---|---|
| GET | `/cms/estagiarios` | `estagiario.index` | `Prefecture\EstagiarioController@index` | `check.logged` |
| GET | `/cms/estagiarios/cadastrar` | `estagiario.create` | `Prefecture\EstagiarioController@create` | `check.logged` |
| GET | `/cms/estagiarios/editar/{id}` | `estagiario.edit` | `Prefecture\EstagiarioController@edit` | `check.logged` |
| PUT | `/cms/estagiarios/atualizar/{id}` | `estagiario.update` | `Prefecture\EstagiarioController@update` | — |
| POST | `/cms/estagiarios` | `estagiario.store` | `Prefecture\EstagiarioController@store` | — |
| DELETE | `/cms/estagiarios/excluir/{id}` | `estagiario.destroy` | `Prefecture\EstagiarioController@destroy` | — |
| DELETE | `/cms/estagiarios/{id}/arquivo/{index}` | `estagiario.destroy-arquivo` | `Prefecture\EstagiarioController@destroyArquivo` | — |

Além dessas, existe a rota de dados do DataTable, dentro do grupo
`prefix => 'datatable', as => 'datatable.'` (`v5/routes/basepro.php:409`):

| Método | URI | Nome da rota | Controller@action |
|---|---|---|---|
| GET | `/cms/datatable/estagiario` | `datatable.estagiario` | `Prefecture\DatatableController@get_estagiario` |

Observações:

- O prefixo real é `/cms` (o grupo usa `'before' => 'csrf'`, sintaxe do Laravel
  4 que é **ignorada** no 5.5; o CSRF vem do middleware group `web`).
- `check.logged` só aparece nas três rotas GET. As rotas de escrita herdam
  `check.route.permission` do construtor de `Prefecture\Controller`
  (`v5/app/Http/Controllers/Prefecture/Controller.php:19-23`), que redireciona
  para `login` quando não há sessão.
- `estagiario.destroy-arquivo` é a única rota DELETE cujo nome **não** cai na
  lista de ações liberadas pelo middleware (ver seção 4).

## 2. Rotas do portal (públicas)

Definidas em `v5/routes/web.php:153-155`, dentro do grupo
`Route::middleware(['check.site.manutention'])` (`v5/routes/web.php:4`).

| Método | URI | Nome da rota | Controller@action | Middleware |
|---|---|---|---|---|
| ANY | `/estagiarios` | `estagiarios.index` | `Portal\EstagiariosController@index` | `web` + `check.site.manutention` |
| GET | `/estagiario/{slug}` | `estagiarios.show` | `Portal\EstagiariosController@show` | `web` + `check.site.manutention` |

- `Route::any` aceita GET e POST; o formulário de filtro usa GET.
- O parâmetro `type` na mesma URL (`?type=xls|pdf|csv|txt`) dispara a
  exportação; não existe rota separada de exportação.
- Não há rota de API pública para o módulo.

### Breadcrumbs

`v5/routes/breadcrumbs.php:168-177` (carregado automaticamente pelo pacote
`diglactic/laravel-breadcrumbs`, que aponta para `base_path('routes/breadcrumbs.php')`):

```php
Breadcrumbs::register('estagiarios', function ($breadcrumbs) {
    $breadcrumbs->parent('home');
    $breadcrumbs->push('Estagiários', route('estagiarios.index'));
});

Breadcrumbs::register('estagiarios.show', function ($breadcrumbs, $estagiario) {
    $breadcrumbs->parent('estagiarios');
    $breadcrumbs->push($estagiario->nome);
});
```

## 3. Controlador do CMS — `Prefecture\EstagiarioController`

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

| Método | Comportamento observável |
|---|---|
| `index()` | Devolve `view('prefecture.estagiarios.index')`. Os dados chegam por AJAX. |
| `create()` | Carrega as secretarias (`getSecretarias()`) e devolve `prefecture.estagiarios.create`. |
| `store(EstagiarioRequest)` | Valida; converte datas; grava os arquivos enviados; `Estagiario::create($data)`; `flash('Estagiário cadastrado com sucesso.')->success()`; redireciona para `estagiario.index`. Em exceção: `flash($e->getMessage())->error()` e depois uma segunda mensagem genérica de erro, com `redirect()->back()`. |
| `edit($id)` | `Estagiario::findOrFail($id)` (404 se inexistente) + secretarias → `prefecture.estagiarios.edit`. |
| `update(EstagiarioRequest, $id)` |Igual ao `store`, mas **concatena** os novos arquivos aos já existentes em vez de substituí-los. |
| `destroy($id)` | `Estagiario::findOrFail($id)` + `delete()` (soft delete) → JSON `{"success": "Estagiário deletado com sucesso."}` com HTTP 200; em erro, JSON `{"erro": "..."}` com HTTP 500. |
| `destroyArquivo($id, $index)` | Decodifica o JSON de `arquivo`, remove o item na posição `$index` (`array_splice`), regrava o JSON ou `null`; 404 se o índice não existir. |
| `prepareData($request)` *(privado)* | Pega apenas `nome`, `periodo_inicio`, `periodo_fim`, `lotacao`, `situacao` e normaliza as datas. |
| `getSecretarias()` *(privado)* | `DB::table('secretarias')->orderBy('titulo')->get(['id', 'titulo as name'])`. |

Trecho real do `store` (upload de anexos):

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

E o `prepareData`:

```php
$data = $request->only(['nome', 'periodo_inicio', 'periodo_fim', 'lotacao', 'situacao']);

if (!empty($data['periodo_inicio'])) {
    $data['periodo_inicio'] = date('Y-m-d', strtotime(str_replace('/', '-', $data['periodo_inicio'])));
}

if (!empty($data['periodo_fim'])) {
    $data['periodo_fim'] = date('Y-m-d', strtotime(str_replace('/', '-', $data['periodo_fim'])));
}
```

## 4. Controlador do portal — `Portal\EstagiariosController`

`v5/app/Http/Controllers/Portal/EstagiariosController.php`
(herda de `App\Http\Controllers\Admin\Controller`, que fornece
`getTemplateActived()`).

| Método | Comportamento observável |
|---|---|
| `index(Request)` | Monta a query com ordenação por `nome`, aplica os filtros `busca`, `lotacao` e `situacao`; se `type` vier preenchido, exporta; senão pagina em 15 e devolve `portal.layouts.{layout}.estagiarios.index`. |
| `show($slug)` | `Estagiario::where('slug', $slug)->firstOrFail()` → `portal.layouts.{layout}.estagiarios.show`. |
| `export($estagiarios, $type)` *(privado)* | `xls` → `Excel::download(new EstagiariosExport(...), 'estagiarios.xlsx')`; `pdf` → `PDF::loadView('exports.estagiarios', ...)`; `csv` → `Excel::download(..., 'estagiarios.csv')`; qualquer outro valor → `exportTxt()`. |
| `exportTxt($estagiarios, $filename)` *(privado)* | Monta um texto alinhado com `str_repeat('-', 110)` e devolve com `Content-Type: plain/txt`. |

Filtro de busca (SQL concatenado, sem binding):

```php
$query->when($request->busca, function ($q) use ($request) {
    $busca = $request->busca;
    $q->whereRaw("translate(lower(nome), ' ''àáâãäéèëêíìïîóòõöôúùüûçÇ', '--aaaaaeeeeiiiiooooouuuucc') ilike '%" . clean_slug($busca) . "%'");
});
```

Filtros estruturados (com binding, seguros):

```php
$query->when($request->lotacao, function ($q) use ($request) {
    $q->where('lotacao', $request->lotacao);
});

$query->when($request->situacao, function ($q) use ($request) {
    $q->where('situacao', $request->situacao);
});
```

## 5. Form Request — `EstagiarioRequest`

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

| Campo | Regra | Efeito |
|---|---|---|
| `nome` | `required|string|max:255` | obrigatório |
| `periodo_inicio` | `required` | obrigatório (sem validação de formato) |
| `periodo_fim` | `required` | obrigatório (sem validação de formato) |
| `lotacao` | `required|string|max:255` | obrigatório |
| `situacao` | `required|string` | obrigatório (sem `in:`) |
| `arquivo` | `nullable|array` | lista de arquivos opcional |
| `arquivo.*` | `nullable|file|mimes:pdf|max:20480` | PDF, até 20 MB |
| `arquivo_desc` | `nullable|array` | descrições opcionais |
| `arquivo_desc.*` | `nullable|string|max:255` | descrição de até 255 caracteres |

`authorize()` retorna `true` — **não há verificação de permissão no Request**;
a autorização depende exclusivamente do middleware.

Mensagens customizadas:

```php
return [
    'nome.required'           => 'O nome do estagiário é obrigatório.',
    'periodo_inicio.required' => 'O início do período é obrigatório.',
    'periodo_fim.required'    => 'O fim do período é obrigatório.',
    'lotacao.required'        => 'A lotação é obrigatória.',
    'situacao.required'       => 'A situação é obrigatória.',
    'arquivo.required'        => 'Pelo menos um arquivo é obrigatório.',
    'arquivo.*.mimes'         => 'Os arquivos devem ser PDFs.',
];
```

A mensagem de `arquivo.required` é **inalcançável**: a regra é `nullable|array`,
logo um conjunto vazio passa sem erro.

## 6. Permissões exigidas

| Recurso/ação | Onde é verificado | Efeito quando falta |
|---|---|---|
| `estagiario.index` | menu do AdminLTE (`v5/app/Providers/MenuServiceProvider.php`, bloco `has_access("$menu->slug.index")`) | Item "Estagiários" não aparece no menu |
| `estagiario.index` / `estagiarios.index` | `check.route.permission` (nome da rota + segmento da URL) | `flash('Você não tem autorização para acessar esta funcionalidade!')->warning()` + `redirect()->back()` |
| `estagiario.create` | botão "Cadastrar" (`@if (has_access(['estagiario.create']))`) | Botão some |
| `estagiario.edit` / `estagiario.delete` | `struct_views()` (`v5/app/Http/Controllers/Prefecture/DatatableController.php:508-525`) injeta `edit`/`delete` em cada linha | Botões de editar/excluir somem da listagem |

`has_access()` (`v5/app/Library/Helper/appHelpers.php:22`) retorna `true`
incondicionalmente para o papel `super-admin`.

Regras do middleware `check.route.permission`
(`v5/app/Http/Middleware/CheckRoutePermission.php`) aplicáveis a este módulo:

- Ações liberadas sem checagem de permissão: `store`, `update`, `destroy`
  (a lista `resourceRoutes` mapeia `create→store`, `edit→update`, `delete→destroy`).
- Rotas cujo nome contém `datatable` estão na lista `openRoutes` → qualquer
  usuário logado lê o JSON completo de estagiários.
- `estagiario.destroy-arquivo` **não** está nas listas → exige a permissão
  `estagiario.destroy-arquivo`, que não existe cadastrada.

## 7. Formato das respostas

| Endpoint | Formato |
|---|---|
| `GET /cms/estagiarios` | HTML (AdminLTE) |
| `GET /cms/datatable/estagiario` | JSON Yajra DataTables (com `data`, `recordsTotal`, `recordsFiltered`, `draw`) + campos extras `edit`, `delete`, `active`, `inactivate` por linha |
| `POST/PUT /cms/estagiarios*` | Redirect 302 para `estagiario.index` (ou `back()` em erro) |
| `DELETE /cms/estagiarios/excluir/{id}` | JSON `{"success": "..."}` (200) / `{"erro": "..."}` (500) |
| `DELETE /cms/estagiarios/{id}/arquivo/{index}` | JSON `{"success": "..."}` (200) / `{"erro": "..."}` (404/500) |
| `GET /estagiarios` | HTML do portal (Bootstrap 4, template ativo) |
| `GET /estagiarios?type=xls` | XLSX (`estagiarios.xlsx`) |
| `GET /estagiarios?type=pdf` | PDF (`estagiarios.pdf`) |
| `GET /estagiarios?type=csv` | arquivo `estagiarios.csv` (conteúdo HTML — ver `04-VIEWS-E-UX.md`) |
| `GET /estagiarios?type=txt` | texto puro (`plain/txt`) |
| `GET /estagiario/{slug}` | HTML do portal; 404 se o slug não existir |

Não há API Resource / Presenter: o JSON é montado à mão pelo
`yajra/laravel-datatables`.
