# Servidor — Rotas e Controllers

> **Módulo:** `servidor` (tabela `modulos`, id 35)

---

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

Arquivo: `v6/routes/basepro.php`, grupo `Route::group(['prefix' => 'cms', 'before' => 'csrf'], function () {`
aberto na linha 35, com middleware `web` (via
`v6/app/Providers/RouteServiceProvider.php:80-85`).

### 1.1 Resource

`v6/routes/basepro.php:204`:

```php
Route::resource("servidor", "Prefecture\ServidorController");
```

Sem `only`/`except`, o Laravel 5.5 registra as **sete** rotas padrão
(`v6/vendor/laravel/framework/src/Illuminate/Routing/ResourceRegistrar.php:21`):

| Método | URI | Nome | Controller@ação |
|---|---|---|---|
| GET | `/cms/servidor` | `servidor.index` | `Prefecture\ServidorController@index` |
| GET | `/cms/servidor/create` | `servidor.create` | `Prefecture\ServidorController@create` |
| POST | `/cms/servidor` | `servidor.store` | `Prefecture\ServidorController@store` |
| GET | `/cms/servidor/{servidor}` | `servidor.show` | `Prefecture\ServidorController@show` — **método inexistente** |
| GET | `/cms/servidor/{servidor}/edit` | `servidor.edit` | `Prefecture\ServidorController@edit` |
| PUT | `/cms/servidor/{servidor}` | `servidor.update` | `Prefecture\ServidorController@update` |
| DELETE | `/cms/servidor/{servidor}` | `servidor.destroy` | `Prefecture\ServidorController@destroy` |

> **Bug:** a rota `servidor.show` aponta para um método que não existe no controller.
> Uma requisição a `GET /cms/servidor/{id}` resulta em `BadMethodCallException`.

### 1.2 Exclusão em lote (rota duplicada)

| Método | URI | Nome | Controller@ação | Linha |
|---|---|---|---|---|
| POST | `/cms/servidor/destroyAll` | `servidor.destroyAll` | `Prefecture\ServidorController@destroyAll` | `v6/routes/basepro.php:247` |
| POST | `/cms/servidor/destroyAll` | `servidor.destroyAll` | `Prefecture\ServidorController@destroyAll` | `v6/routes/basepro.php:253` |

As duas linhas são **idênticas** (mesmo verbo, URI, controller e nome). A segunda
sobrescreve a primeira no registro de nomes do Laravel; funcionalmente o resultado é o
mesmo, mas é duplicação sem efeito (ver `08-GAPS-E-DIVIDAS.md`).

### 1.3 DataTable

| Método | URI | Nome | Controller@ação | Linha |
|---|---|---|---|---|
| GET | `/cms/datatable/servidor` | `datatable.servidor` | `Prefecture\DatatableController@get_servidor` | `v6/routes/basepro.php:439` |

Dentro do grupo `Route::group(['prefix' => 'datatable', 'as' => 'datatable.'], function () {`
(`v6/routes/basepro.php:396`).

### 1.4 Rotas públicas que consomem o módulo

| Método | URI | Nome | Controller@ação | Linha |
|---|---|---|---|---|
| ANY | `/requerimento-de-diarias` | `requerimento-diarias-portal.index` | `Portal\BuscaDiariasController@index` | `v6/routes/web.php:211` |
| GET | `/requerimento-de-diarias/{id}` | `requerimento-diarias-portal.show` | `Portal\BuscaDiariasController@show` | `v6/routes/web.php:212` |

Ambas vivem no grupo `Route::middleware(['check.site.manutention'])->group(function () {`
(`v6/routes/web.php:7`) — são **públicas**.

## 2. Ações do controller

`v6/app/Http/Controllers/Prefecture/ServidorController.php`

| Ação | Comportamento |
|---|---|
| `__construct(IServidorRepository $servidorRepository)` | `menuClicks('servidor')`; `$this->title = 'Servidor'`; `$this->repository`; `$this->table = app(Servidor::class)`; `$this->middleware('check.route.permission')`; `$this->secretarias = $this->getContentByCategoryWith('secretarias', ['id', 'title as name'])`. |
| `index()` | `view('prefecture.contents.servidor.index', ['title' => $this->title, 'content' => $this->table->all()])`. A collection é carregada e **não usada** pela view. |
| `create()` | `view('prefecture.contents.servidor.create', ['title' => $this->title, 'secretarias' => $this->secretarias])`. |
| `store(ServidorRequest $request)` | `$this->repository->upInsert($request)`; em sucesso, `flash('Registro cadastrado com sucesso.')->success()` e `redirect()->route('servidor.index')`; em exceção, `flash($exception->getMessage())->error()`. |
| `edit(int $id)` | `view('prefecture.contents.servidor.create', ['title' => $this->title, 'data' => $this->table->where('id', $id)->first(), 'secretarias' => $this->secretarias])` — **reutiliza a view de cadastro**. |
| `update(ServidorRequest $request, int $id)` | `$this->repository->upInsert($request, $id)`. |
| `destroy(int $id)` | `$this->table->destroy($id)` → JSON `{success}` ou 500 `{erro}`; `PDOException` tem tratamento próprio. |
| `destroyAll(Request $request)` | `$this->repository->destroyAll($request->toDelete)` → JSON `{success}` ou 500 `{erro}`. |

### Código do construtor (fonte)

```php
public function __construct(IServidorRepository $servidorRepository)
{
    menuClicks('servidor');
    $this->title = 'Servidor';
    $this->repository = $servidorRepository;
    $this->table = app(Servidor::class);
    $this->middleware('check.route.permission');
    $this->secretarias = $this->getContentByCategoryWith('secretarias', ['id', 'title as name']);
}
```

### Código da exclusão em lote (fonte)

```php
public function destroyAll(Request $request)
{
    try {
        if ($this->repository->destroyAll($request->toDelete))
            return response()->json(['success' => 'Servidores deletados com sucesso.'], 200);

        return response()->json(['erro' => 'Ocorreu um erro! Por favor entre em contato com o suporte técnico.'], 500);
    } catch (PDOException $exception) {
        return response()->json(['erro' => 'Ocorreu um erro ao tentar salvar no banco de dados.'], 500);
    } catch (Exception $e) {
        return response()->json(['erro' => 'Ocorreu um erro! Por favor entre em contato com o suporte técnico.'], 500);
    }
}
```

> `Repository::destroyAll()` **sempre** devolve um `JsonResponse` (true ou false), que é
> sempre um objeto truthy em PHP. Logo a condição `if ($this->repository->destroyAll($request->toDelete))` é sempre verdadeira e o
> controller **sempre responde sucesso**, mesmo quando nada foi apagado
> (ver `08-GAPS-E-DIVIDAS.md`).

## 3. Repository e Form Request

### `IServidorRepository` (contrato)

`v6/app/Repositories/Contracts/IServidorRepository.php` — **interface vazia**:

```php
namespace App\Repositories\Contracts;

interface IServidorRepository
{

}
```

O contrato é injetado pelo container (`ServidorController::__construct(IServidorRepository)`)
e resolvido para `App\Repositories\Eloquent\Prefecture\Servidor\ServidorRepository`.

### `ServidorRepository`

```php
class ServidorRepository extends Repository implements IServidorRepository
{
    public function __construct()
    {
        $this->model = new Servidor();
    }

    protected function structureUpInsert($request): array
    {
        $fields = array_keys($request);
        return $this->filter_request($request, $fields);
    }
}
```

`filter_request()` (`v6/app/Http/Traits/AppTrait.php:523-535`) percorre o request e
substitui **todo valor falsy por `null`**:

```php
if (!$valor)
    $filter[$campo] = null;
else
    $filter[$campo] = $valor;
```

Consequências:

- Strings vazias (`""`) viram `NULL` — é isso que permite gravar `secretaria = NULL`
  quando o select do formulário envia `"0"` (option "Selecione"), sem violar a FK.
- A string `"0"` também é falsy e vira `NULL`.
- Como `$fields = array_keys($request)`, **todos** os campos enviados são aceitos:
  o repository não tem uma allowlist própria — a proteção é a validação do
  Form Request. Se um campo extra for enviado (por exemplo `_token` já é removido,
  mas qualquer outro), ele será gravado na tabela (ou ignorado, se não for fillable).

### `ServidorRequest`

`v6/app/Http/Requests/ServidorRequest.php`:

| Campo | Regras | Mensagem |
|---|---|---|
| `name` | `required` | "O campo de Nome é obrigatório." |
| `cpf` | `required`, `min:11`, `Validate_cpf` | "O campo de CPF é obrigatório." / "CPF inválido, verifique e tente novamente." |
| `identidade` | `required` | "O campo de Identidade é obrigatório." |
| `matricula` | `required` | "O campo de Matrícula é obrigatório." |
| `cargo` | `required` | "O campo de Cargo é obrigatório." |
| — | `new Validate_cpf` solto (chave numérica `4`) | — |
| `secretaria`, `banco`, `conta`, `agencia` | **sem regras** | — |

Pontos de atenção:

- A entrada `new Validate_cpf` no array de regras cria uma regra órfã com chave `4`:
  o validador passa a considerar um atributo chamado `"4"`, que não existe no request,
  e a regra não é implícita, portanto **não executa**. É um resíduo de código.
- `min:11` conta **caracteres**: o CPF com máscara (`123.456.789-09`) tem 14 e passa;
  um CPF com 11 dígitos também passa. O `Validate_cpf` normaliza com `onlyNumbers()`
  antes de calcular os dígitos verificadores.
- `authorize()` retorna `true` unconditionally: a autorização real vem do middleware.
- `secretaria` não é validada: um id inexistente violaria a FK e cairia no `dd()` do
  repository base.

## 4. Permissões exigidas

O `CheckRoutePermission` resolve a permissão pelo **nome da rota**:

| Nome da rota | Segmento | Situação |
|---|---|---|
| `servidor.index` | `index` | exige `servidor.index` (chave gerável na tela de Perfis) |
| `servidor.create` | `create` | exige `servidor.create` |
| `servidor.store` | `store` | **aberto** — `store` está na lista de valores de `$resourceRoutes` do middleware |
| `servidor.show` | `show` | exigiria `servidor.show`, mas o método não existe |
| `servidor.edit` | `edit` | exige `servidor.edit` |
| `servidor.update` | `update` | **aberto** — `update` está na lista de valores de `$resourceRoutes` |
| `servidor.destroy` | `destroy` | **aberto** — `destroy` está na lista de valores de `$resourceRoutes` |
| `servidor.destroyAll` | `destroyAll` | exige `servidor.destroyAll` — chave **não gerável** pela tela de Perfis (as permissões são `recurso.slug` + `permission.slug` genérico: index, create, edit, delete, inactive, inactivate, restore, show, publish) |
| `datatable.servidor` | `datatable` | **aberto** — `datatable` está na lista `openRoutes` |

Regras gerais:

- `super-admin` passa direto.
- Usuário não logado → `redirect()->route('login')`.
- Usuário logado sem a permissão → `flash('Você não tem autorização para acessar esta funcionalidade!')->warning()` e `redirect()->back()`.

Além do middleware, a view usa `has_access()`:

| Verificação | Efeito |
|---|---|
| `has_access(['servidor.create'])` | exibe o botão "Cadastrar Servidor" |
| `has_access(['servidor.delete'])` | exibe o botão de exclusão em lote e o checkbox mestre |

E o `DatatableController::struct_views()` injeta, por linha:

| Campo | Permissão |
|---|---|
| `edit` | `has_access('servidor.edit')` |
| `delete` | `has_access('servidor.delete')` |
| `active` | `has_access('servidor.restore')` |
| `inactivate` | `has_access('servidor.inactivate')` |

## 5. API Resources / presenters

Não existem API Resources, transformers ou presenters para o módulo. As saídas são:

| Endpoint | Formato |
|---|---|
| `/cms/datatable/servidor` | JSON do Yajra DataTables (campos da view + `edit`, `delete`, `active`, `inactivate`) |
| `DELETE /cms/servidor/{id}` | JSON `{success}` ou `{erro}` (HTTP 500) |
| `POST /cms/servidor/destroyAll` | JSON `{success}` ou `{erro}` (HTTP 500) |
| `POST`/`PUT` do resource | redirect (302) para `servidor.index` com flash |

## 6. Item de menu

`v6/app/Providers/MenuServiceProvider.php`, `case 'servidor'`:

```php
case 'servidor':
    $event->menu->add(['text' => 'Servidores', 'url' => '/cms/servidor', 'icon' => 'users']);
    break;
```

Exibido somente quando `has_access("servidor.index")` **e** `$menu->modulo->status`
(`modulos.id = 35`) são verdadeiros. O `clicks` da linha `menu_orders` (id 29) é
incrementado pelo próprio controller a cada visita.
