# Servidor — Regras e Portabilidade

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

---

## 1. Regras de negócio não óbvias

| Regra | Onde está implementada | Consequência prática |
|---|---|---|
| Obrigatórios: nome, CPF, identidade, matrícula e cargo | `ServidorRequest::rules()` | o cadastro só é aceito com os cinco campos preenchidos |
| O CPF é validado por dígito verificador | `App\Rules\Validate_cpf` + `AppTrait::validar_cpf()` | CPFs como `111.111.111-11` são recusados ("CPF inválido, verifique e tente novamente.") |
| `min:11` conta caracteres, não dígitos | `ServidorRequest` | o CPF mascarado (`123.456.789-09`, 14 caracteres) passa; um CPF com 10 dígitos falha |
| A "secretaria" é um conteúdo, não uma tabela própria | `contents` com `category_contents.slug = 'secretarias'` | apagar ou renomear a categoria de secretarias muda o significado do campo |
| Valores vazios viram `NULL` | `ServidorRepository::structureUpInsert` → `AppTrait::filter_request()` | o option "Selecione" (`"0"`) vira `secretaria = NULL`, evitando violar a FK |
| Exclusão é lógica | `SoftDeletes` | a linha permanece; a view `servidores` já a oculta |
| A listagem do CMS lê a **view**, não a tabela | `DatatableController@get_servidor` | cargo, secretaria e dados bancários não aparecem na listagem |
| O contador `clicks` do menu é incrementado a cada visita | `menuClicks('servidor')` no construtor | o módulo é um dos poucos que alimenta essa métrica |
| A exclusão em lote é sempre reportada como sucesso | `Repository::destroyAll` devolve sempre um `JsonResponse` | o operador recebe "Servidores deletados com sucesso." mesmo se nada foi apagado |
| Um servidor com requerimentos de diárias não pode ser excluído fisicamente | FK `requerimento_diarias.id_servidor → servidor(id)` | o soft delete funciona; o `DELETE` físico é bloqueado pelo banco |

## 2. Acoplamentos

### Dependências do módulo

| Dependência | Forma | Observação |
|---|---|---|
| `contents` (categoria `secretarias`) | FK `servidor.secretaria → contents.id` | o select do formulário depende de o conteúdo estar na categoria certa |
| `modulos` (id 35) | `menu_orders.modulo_id`, `resources.modulo_id` | habilitação do menu e criação das permissões |
| `resources` (id 47) | `resources.slug = 'servidor'` | sem essa linha, as permissões `servidor.*` não podem ser concedidas na tela de Perfis |
| `MenuOrder` / `menuClicks()` | helper global | métrica de acessos |
| `GetInformationsPrefectureTrait::getContentByCategoryWith()` | trait | carrega as secretarias **no construtor** (uma query por requisição, mesmo em rotas que não precisam delas) |
| `App\Repositories\Repository` (base) | herança | `upInsert`, `destroy`, `destroyAll` — compartilhados com todos os módulos |
| `AppTrait::filter_request()` | helper | normalização de campos vazios |

### Dependências **do** módulo (quem consome `servidor`)

| Consumidor | Uso | Risco |
|---|---|---|
| `v6/app/Http/Controllers/Prefecture/RequerimentoDiariasController.php` | `$this->servidor = app(Servidor::class)`; `'servidores' => $this->servidor->all()` nos formulários de create e edit | carrega **todos** os servidores sem paginação nem filtro de secretaria |
| `v6/database/migrations/2023_01_13_093828_create_requerimento_diarias_table.php` | FK `id_servidor` | integridade referencial |
| `v6/app/Http/Model/Prefecture/RequerimentoDiarias.php` | relação `servidor` (`hasOne`) | usada no `with('servidor')` da busca pública |
| `v6/app/Http/Controllers/Portal/BuscaDiariasController.php` | `Servidor::whereRaw` com concatenação da entrada sobre `name` e `cpf` | **SQL injection** (ver `08-GAPS-E-DIVIDAS.md`) |
| `v6/database/migrations/2023_04_10_100557_create_view_to_requerimentos_diarias.php` e `2023_05_17_091233_update_requerimento_diarias.php` | `join servidor s on s.id = rd.id_servidor` nas views de requerimentos | as views SQL do módulo de diárias dependem da tabela `servidor` |

## 3. Armadilhas

| Armadilha | Evidência | Impacto |
|---|---|---|
| **Tabela `servidor` × view `servidores`** | model com `protected $table = 'servidor'`; `DatatableController@get_servidor` usa `DB::table('servidores')` | dois objetos com nomes quase idênticos; consultas escritas à mão confundem um pelo outro e a listagem mostra menos campos que o formulário |
| **Rota `servidor.show` sem método** | `Route::resource` gera 7 rotas; o controller não implementa `show` | `GET /cms/servidor/{id}` → `BadMethodCallException` (HTTP 500) |
| **Rota duplicada** | `v6/routes/basepro.php:247` e `:253` registram `POST /cms/servidor/destroyAll` com o mesmo nome | ruído no arquivo de rotas; a segunda sobrescreve a primeira no registro de nomes |
| **`destroyAll` sempre reporta sucesso** | `Repository::destroyAll` retorna `response()->json(true|false)` (objeto truthy) | o controller responde `success` mesmo quando nada foi apagado; o operador não percebe a falha |
| **`dd($th)` no repository base** | `v6/app/Repositories/Repository.php:66-72` | qualquer exceção no `upInsert` vira um dump de debug na tela do operador (e o `catch (Exception)` do controller nunca roda) |
| **SQL injection na busca pública** | `BuscaDiariasController` concatena `$request->req_name` e `$request->cpf` em `whereRaw` | rota pública (`v6/routes/web.php:211`) permite injeção de SQL sobre a tabela `servidor`; `req_name` não tem validação nenhuma e `cpf` só tem `nullable|min:14` |
| **Dados bancários em claro** | colunas `banco`, `conta`, `agencia` sem cifragem, exibidas no formulário | exposição de dado financeiro de servidores a qualquer operador com `servidor.edit` |
| **CPF sem unicidade** | não há índice único em `servidor.cpf` | o mesmo servidor pode ser cadastrado várias vezes, com requerimentos de diárias duplicados |
| **Relação `secretarias()` com semântica errada** | `hasOne(Content::class, 'id', 'secretaria')` | deveria ser `belongsTo`; a relação **não é usada** em lugar nenhum |
| **Contrato vazio** | `IServidorRepository` sem métodos | o controller depende de métodos herdados de `Repository` que o contrato não declara; trocar a implementação quebraria em tempo de execução |
| **Regra órfã no Form Request** | `new Validate_cpf` solto no array de regras (chave numérica) | regra inerte; sinal de código esquecido |
| **`filter_request` sem allowlist** | `$fields = array_keys($request)` | todo campo enviado é copiado; a segurança depende inteiramente do Form Request |
| **Coluna "ID" mostra `row_number()`** | view `servidores` (`value`) + `runDatatables` | a coluna "ID" da listagem não é o `servidor.id`; a ordenação exibida é por data de criação |
| **`index()` carrega a tabela inteira** | `$this->table->all()` passado para a view e não usado | leitura completa da tabela a cada visita ao CMS |
| **Secretarias carregadas no construtor** | `$this->secretarias = getContentByCategoryWith('secretarias', ['id', 'title as name'])` no construtor | uma query com `with('linksUnique')` em **todas** as rotas do controller, inclusive nas que não usam a lista |
| **`Servidor::all()` no Requerimento de Diárias** | `RequerimentoDiariasController@create`/`@edit` | todos os servidores são carregados e materializados em memória a cada abertura do formulário |
| **Rotas `store`, `update` e `destroy` liberadas** | `CheckRoutePermission` abre qualquer rota cujo segmento seja `store`, `update` ou `destroy` | qualquer usuário autenticado que descubra a URL pode criar, alterar ou excluir servidores |
| **Permissão `servidor.destroyAll` não gerável** | as permissões são `recurso.slug` + `permission.slug` genérico | a exclusão em lote fica restrita ao `super-admin` na prática |
| **Summernote carregado sem uso** | `form.blade.php` inclui os scripts do Summernote | peso desnecessário no formulário |

## 4. Código duplicado ou copiado

| Trecho | Onde | Observação |
|---|---|---|
| Estrutura de CRUD (controller + repository + Form Request) | idêntica a dezenas de outros módulos (`RequerimentoDiarias`, `Enquete`, `DiarioOficial`) | o padrão é `Repository::upInsert` + `filter_request` |
| `DatatableController@get_servidor` | idêntico a `get_newsletter`, `get_noticias` e companhia | `DB::table('<view>')->get()` + `struct_views()` |
| JS de DataTable | `assets/js/custom/contents/servidor/datatable.js` | mesma estrutura do `assets/js/custom/contents/newsletter/datatables.js` (inclusive com nomes de arquivo diferentes) |
| Trilho de exclusão em lote | `prefecture/components/deleteByLoteSubmitButton` + `deleteAll.js` | compartilhado por todos os módulos |
| Componentes de formulário | `components/inputs/input.blade.php`, `components/inputs/select.blade.php` | compartilhados |

## 5. Divergências v5 → v6

O módulo é **byte a byte idêntico** entre `v5` e `v6`. Diff de todos os arquivos:

| Arquivo | Resultado do diff |
|---|---|
| `v5/app/Http/Controllers/Prefecture/ServidorController.php` × `v6/app/Http/Controllers/Prefecture/ServidorController.php` | idêntico |
| `v5/app/Http/Model/Prefecture/Servidor.php` × `v6/app/Http/Model/Prefecture/Servidor.php` | idêntico |
| `v5/app/Http/Requests/ServidorRequest.php` × `v6/app/Http/Requests/ServidorRequest.php` | idêntico |
| `v5/app/Repositories/Eloquent/Prefecture/Servidor/ServidorRepository.php` × `v6/app/Repositories/Eloquent/Prefecture/Servidor/ServidorRepository.php` | idêntico |
| `v5/resources/views/prefecture/contents/servidor/index.blade.php` × `v6/resources/views/prefecture/contents/servidor/index.blade.php` | idêntico |
| `v5/resources/views/prefecture/contents/servidor/create.blade.php` × `v6/resources/views/prefecture/contents/servidor/create.blade.php` | idêntico |
| `v5/resources/views/prefecture/contents/servidor/form.blade.php` × `v6/resources/views/prefecture/contents/servidor/form.blade.php` | idêntico |
| `v5/public/assets/js/custom/contents/servidor/datatable.js` × `v6/public/assets/js/custom/contents/servidor/datatable.js` | idêntico |
| `v5/database/migrations/2023_01_12_093921_create_servidors_table.php` × `v6/database/migrations/2023_01_12_093921_create_servidors_table.php` | idêntico |
| `v5/database/migrations/2023_03_27_154732_create_view_to_servidores.php` × `v6/database/migrations/2023_03_27_154732_create_view_to_servidores.php` | idêntico |
| `v5/routes/basepro.php` × `v6/routes/basepro.php` | `Route::resource("servidor", "Prefecture\ServidorController")` e as **duas** linhas `servidor/destroyAll` já existiam na v5 (linhas 185, 219 e 225) |

### Novidade da v6 (fora do módulo, mas com efeito sobre ele)

- A tabela `modulos` e a migration `2024_11_01_150206_create_modulos_table.php` **não
  existem na v5**. Na v6 o módulo ganhou `modulos.id = 35` (`slug = 'servidor'`) e
  `menu_orders.modulo_id = 35`.
- `resources.id = 47` (`slug = 'servidor'`, `modulo_id = 35`) é o que habilita o módulo
  na tela de Perfis. Na v5 não havia essa integração.

**Conclusão:** nenhuma funcionalidade do módulo `servidor` foi alterada, removida ou
acrescentada entre `v5` e `v6`. Todos os problemas mapeados em `08-GAPS-E-DIVIDAS.md`
são herdados do legado.
