# Estagiários — Regras de Negócio, Armadilhas e Portabilidade

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

---

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

| # | Regra | Onde está | Consequência prática |
|---|---|---|---|
| R1 | O `slug` é `str_slug(nome-id)`, recalculado em todo `update` | `v5/app/Http/Model/Prefecture/Estagiario.php` (`boot()`) | Renomear o estagiário **quebra** links públicos `/estagiario/{slug}` antigos |
| R2 | `lotacao` armazena o **título** da secretaria, não o id | `prepareData()` em `v5/app/Http/Controllers/Prefecture/EstagiarioController.php` | Se o nome da secretaria mudar no CMS, o estagiário fica com lotação órfã (não aparece mais no filtro) |
| R3 | `situacao` é texto livre no banco (`varchar(50)`, default `Ativo`) | migration `2026_05_26_200000` | O Request aceita qualquer string; só o `<select>` restringe a `Ativo`/`Encerrado`/`Rescindido` |
| R4 | Anexos são um **JSON** em `text`, não uma tabela relacionada | `store`/`update`/`destroyArquivo` | Não há integridade referencial, ordenação, tamanho máximo ou contagem de arquivos |
| R5 | Exclusão é soft delete; a view já filtra `deleted_at IS NULL` | model + `estagiarios_view` | O portal (que usa o model) e o CMS (que usa a view) deixam de exibir o registro excluído |
| R6 | A exportação ignora a paginação | `Portal\EstagiariosController@index` (`$query->get()`) | Exportar 50 mil estagiários carrega tudo em memória |
| R7 | O filtro do portal é aplicado **antes** da paginação | mesmo método | O `whereRaw` roda sobre a tabela inteira a cada requisição |
| R8 | A listagem do CMS não tem filtros | `prefecture/estagiarios/index.blade.php` | Só busca client-side do DataTable |
| R9 | O `row_number()` da view é a "coluna ID" exibida | `estagiarios_view` + `runDatatables` | O número exibido é a ordem de criação (decrescente), não o `estagiarios.id` |

## 2. Acoplamentos

### 2.1 Dependências do módulo

| Depende de | Tipo | Onde |
|---|---|---|
| `secretarias` (view sobre `contents`/`category_contents`) | dado | `getSecretarias()` no CMS; `pluck('titulo')` no portal |
| `menu_orders` | dado | item de menu do AdminLTE |
| `resources` | dado | recurso de permissão (`estagiario`, id 15) |
| Sentinel (`cartalyst/sentinel`) | permissão | `has_access()`, `check.route.permission` |
| `FileTrait` | código | upload em `storage/app/public/docs` |
| `portal.components.estagiarios.table` | view | tabela compartilhada entre os 6 templates |
| `configurations.layout` | configuração | escolhe o template do portal |

### 2.2 Módulos que dependem do Estagiários

Nenhum. O módulo é uma **folha**: ninguém consulta `estagiarios` nem
`estagiarios_view`. Confirmação: `grep -rn "estagiario" v5/app v5/resources/views`
não retorna referências fora dos arquivos do próprio módulo (exceto o item de
menu e a rota de DataTable).

### 2.3 Padrão da plataforma não usado por este módulo

| Padrão existente | Onde | Situação em Estagiários |
|---|---|---|
| Classes de filtro em `App\Http\Filter\*` | `v5/app/Http/Filter/FilterOutsourceds.php`, `v5/app/Http/Filter/FilterFilaEsperaSus.php`, `v5/app/Http/Filter/FilterBiddings.php` | **Não usado** — os filtros estão inline no controller do portal |
| `activity()` (auditoria) | `v5/app/Http/Controllers/Admin/UserController.php`, `v5/app/Http/Controllers/Admin/PermissionController.php` | **Não usado** — nenhuma ação do módulo é registrada no log de atividades |
| `menuClicks($slug)` | `v5/app/Helpers/functions.php:822` | **Não usado** — `menu_orders.clicks` do slug `estagiario` permanece `0` |
| Namespace `App\Prefecture\*` para models (arquivos em `app/Http/Model/Prefecture/`) | `v5/app/Http/Model/Prefecture/Outsourced.php` (declara `namespace App\Prefecture`) | O model do módulo declara `namespace App\Http\Model\Prefecture` — divergindo do padrão adotado pelo resto da plataforma |
| `LogsActivity` (trait do `spatie/laravel-activitylog` no model) | `v5/app/Http/Model/Prefecture/Outsourced.php` (`use Spatie\Activitylog\Traits\LogsActivity;`) | **Não usado** — o model `Estagiario` só usa `SoftDeletes`, então não há auditoria automática |

## 3. Armadilhas reais encontradas no código

### 3.1 SQL concatenado na busca do portal

`v5/app/Http/Controllers/Portal/EstagiariosController.php`:

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

- A string é montada por concatenação; a proteção depende inteiramente de
  `clean_slug()` (`v5/app/Helpers/functions.php:780`), que remove tudo que não
  for `[A-Za-z0-9. ]`. Não há binding nemprepared statement.
- Se `clean_slug()` for alterado no futuro, o ponto de injeção volta a existir.
  A forma correta (já usada no módulo Terceirizados) é:

```php
$result->when($request->busca, function ($query) use ($request) {
    $search = '%' . $request->busca . '%';
    return $query->where(function ($q) use ($search) {
        $q->where('employee_name', 'ilike', $search)
          ->orWhere('company', 'ilike', $search);
    });
});
```

  (`v5/app/Http/Filter/FilterOutsourceds.php`)
- A função `translate(lower(nome), ...)` envolve a coluna em funções → **não
  usa índice**; cada busca faz varredura sequencial da tabela.

### 3.2 Helpers de sessão e platforma

| Helper | Origem | Uso no módulo | Risco |
|---|---|---|---|
| `flash($msg)->success()` / `->error()` | `laracasts/flash` + `laralabs/toaster` (`v5/composer.json`) | Todas as respostas do CMS | Cada chamada **empilha** mensagem na coleção da sessão (`FlashNotifier::message()`); no `catch` do `store`/`update` são exibidas **duas** mensagens: a exceção original e a genérica |
| `flash($e->getMessage())->error()` | idem | `catch (\Exception $e)` do `store`/`update` | **Exposição de informação interna**: a mensagem da exceção (SQL, caminho de arquivo, trace) é exibida ao operador |
| `activity()` | `spatie/laravel-activitylog` (`v5/composer.json`) | **Não usado** | Sem trilha de auditoria: quem criou/alterou/excluiu um estagiário não é registrado |
| `menuClicks($slug)` | `v5/app/Helpers/functions.php:822` | **Não usado** | Com `menu_interativo = 1`, o menu é ordenado por `clicks DESC`; o item "Estagiários" fica sempre no fim |
| `has_access($slug)` | `v5/app/Library/Helper/appHelpers.php:22` | Views e middleware | `true` para `super-admin`; demais usuários dependem de permissões Sentinel que **não existem** para `estagiario.*` |

### 3.3 Permissões: rotas liberadas e nomes divergentes

- `check.route.permission` (`v5/app/Http/Middleware/CheckRoutePermission.php`)
  libera as ações cujo segmento aparece como **valor** de `resourceRoutes`:
  `store`, `update`, `destroy`. Ou seja, `estagiario.store`,
  `estagiario.update` e `estagiario.destroy` passam sem checagem de permissão
  — **basta estar logado**. Apenas `index`, `create`, `edit` e
  `destroy-arquivo` exigem permissão.
- A rota `datatable.estagiario` está em `openRoutes` → qualquer usuário logado
  lê o JSON completo de estagiários.
- Convenção de nomes inconsistente: rota CMS `estagiario.*` (singular), rota do
  portal `estagiarios.*` (plural), URL `/cms/estagiarios` (plural), resource
  `estagiario` (singular). O middleware checa as duas formas
  (`hasAnyAccess([$access, $accessSegment])`), mas o menu e os botões usam
  apenas a forma singular — o administrador de permissões precisa saber disso.
- Não existe linha em `permissions` com slug `estagiario.*` (a tabela tem só
  as 8 ações genéricas: `index`, `create`, `edit`, `delete`, `inactive`,
  `inactivate`, `restore`, `show`). Consequência: **sem `super-admin`, o
  módulo é inacessível** até que as permissões sejam cadastradas manualmente.

### 3.4 Arquivos: órfãos e link público

- `destroyArquivo()` remove a **entrada do JSON**, mas **não apaga o arquivo**
  do disco (`FileTrait::deleteFile()` não é chamado) → PDFs órfãos em
  `storage/app/public/docs`.
- No `update()`, arquivos novos são **anexados** aos existentes (o array é
  preservado e acrescido); não há como substituir o conjunto de anexos.
- O nome do arquivo é gerado por `uniqid('', false) . str_random(5)` +
  extensão (`v5/app/Http/Traits/FileTrait.php`), o que torna a URL
  praticamente adivinhável por terceiros.
- Os PDFs são servidos por `url('storage/...')`, ou seja, **link público e
  direto** — sem autorização, sem expiração e sem registro de acesso.

### 3.5 Dados pessoais expostos (LGPD)

| Exposição | Detalhe |
|---|---|
| Nome completo do estagiário | Listagem e ficha públicas (`/estagiarios`, `/estagiario/{slug}`) |
| Lotação (secretaria) | Filtro e listagem pública; combina nome + lotação + período |
| Período do contrato | Público |
| Documentos anexados | PDFs publicamente baixáveis; podem conter CPF, endereço, dados bancários ou termo de estágio assinado |
| Exportações | XLSX/CSV/TXT/PDF com nome + lotação + período, gerados sem autenticação |
| Ausência de base legal declarada | Não há aviso de privacidade, nem anonimização, nem mascaramento em nenhum ponto do módulo |

A combinação "nome completo + lotação + período" permite identificar a pessoa
que-extensiona o estágio. Para adequação à LGPD, a reconstrução deveria
avaliar: publicar apenas nome socializado/abreviado, exigir consentimento/base
legal registrada, mover os anexos para storage privado com rota autenticada e
registrar logs de acesso.

### 3.6 Datas e validações frouxas

- O Request só exige `required` para `periodo_inicio` e `periodo_fim` — não há
  `date_format` nem `after_or_equal:periodo_inicio`.
- `prepareData()` usa `date('Y-m-d', strtotime(...))`: uma data inválida
  (`31/02/2026`) faz `strtotime()` retornar `false` e o campo é gravado como
  `1970-01-01`, **sem erro** para o operador.
- Não há validação de que `periodo_fim >= periodo_inicio`.

### 3.7 Performance e volume

- `DatatableController@get_estagiario` faz `DB::table('estagiarios_view')->get()`
  e só depois o Yajra pagina: **toda a tabela é carregada em memória** a cada
  requisição da listagem do CMS.
- A view `estagiarios_view` inclui a coluna `arquivo` (JSON com caminhos e
  descrições dos anexos) → o payload JSON da listagem **traz os caminhos dos
  arquivos**, embora a coluna não seja exibida.
- A exportação carrega a coleção inteira em memória (`$query->get()`).

### 3.8 Duplicação de código

- As três views de portal (`index`, `filtro`, `show`) existem em **6 cópias**
  (`modelo1`..`modelo6`), diferindo apenas no nome do layout. O mesmo padrão
  vale para os demais módulos do portal (Obras, Terceirizados, Concursos PSS,
  Fila de Espera SUS) — a duplicação é do portal, não específica deste módulo.
- O componente `portal/components/estagiarios/table.blade.php` centraliza
  apenas a **tabela**; o formulário de filtro continua duplicado 6 vezes.
- A lógica de JSON de arquivos está replicada em três lugares:
  `EstagiarioController@store`, `@update`, `@destroyArquivo`, a view
  `prefecture/estagiarios/form.blade.php` e a view
  `portal/layouts/modelo1/estagiarios/show.blade.php` (que faz o mesmo
  `array_map` de normalização).

### 3.9 Onde a v6 divergiu da v5

A v6 **removeu o módulo inteiro** — não há divergência de comportamento, há
ausência. Detalhamento em `08-GAPS-E-DIVIDAS.md`.

## 4. Regras implícitas a preservar na reconstrução

1. O nome do estagiário é público; o período e a lotação também.
2. A exportação respeita os filtros da página e entrega **todos** os registros
   filtrados (não só a página atual).
3. O PDF tem cabeçalho com nome da prefeitura e data da pesquisa; o XLSX
   não tem cabeçalho.
4. O portal funciona em 6 templates; a listagem e a ficha devem existir em
   todos (ou em um template único com herança, na reconstrução).
5. A listagem do CMS é paginada no cliente, com ordenação por coluna e botões
   de exportação do próprio DataTable.
6. A exclusão é lógica (soft delete) e o registro não aparece mais no portal.
