# Estagiários — Plano de Migração para Laravel Moderno (PHP 8.x / Laravel 11-12)

> Este é um plano de reconstrução, **não** uma implementação. O objetivo é
> preservar o comportamento observável do módulo da v5 enquanto se corrigem as
> armadilhas documentadas em `05-REGRAS-E-PORTABILIDADE.md`.

---

## 1. Schema de migração

### 1.1 Tabela `estagiarios`

```php
Schema::create('estagiarios', function (Blueprint $table) {
    $table->id();
    $table->string('nome', 255);
    $table->date('periodo_inicio');
    $table->date('periodo_fim');
    $table->string('lotacao', 255);
    $table->string('situacao', 50)->default(SituacaoEstagiario::Ativo->value);
    $table->text('arquivo')->nullable();   // legado: JSON
    $table->string('slug', 191)->nullable()->unique();
    $table->timestamps();
    $table->softDeletes();

    $table->index('situacao');
    $table->index('lotacao');
});
```

> Decisão: manter `arquivo` (JSON) **somente** durante a transição de dados e
> migrar para a tabela `estagiario_arquivos` (§ 1.2). Se a v7 exigir
> paridade estrita com a v5, o campo pode permanecer — mas a recomendação é
> relationalizar, porque o JSON impede contagem, ordenação, integridade e
> exclusão do arquivo físico.

### 1.2 Tabela `estagiario_arquivos` (novo)

```php
Schema::create('estagiario_arquivos', function (Blueprint $table) {
    $table->id();
    $table->foreignId('estagiario_id')->constrained()->cascadeOnDelete();
    $table->string('path', 255);        // caminho no disco
    $table->string('nome_original', 255)->nullable();
    $table->string('description', 255)->nullable();
    $table->unsignedBigInteger('tamanho')->nullable();
    $table->string('mime', 100)->nullable();
    $table->timestamps();
});
```

### 1.3 Views SQL

Manter a view de leitura para o CMS, mas com o nome padronizado e sem a
coluna `value` enganosa:

```sql
CREATE OR REPLACE VIEW estagiarios_publicos AS
SELECT
    e.id, e.nome, e.periodo_inicio, e.periodo_fim,
    e.lotacao, e.situacao, e.slug
FROM estagiarios e
WHERE e.deleted_at IS NULL;
```

> A coluna `row_number()` deve sair: no legado ela era exibida como "ID", mas
> não correspondia ao `id` real. Se a paridade visual for exigida, expor
> `row_number()` com o nome `sequencia` e rotular a coluna como "Sequência".

### 1.4 Dados de apoio

| Item | Ação na v7 |
|---|---|
| `resources` (slug `estagiario`, id 15 na v5) | Recriar com `modulo_id` apontando para o módulo na tabela `modulos` (`v6/database/migrations/2024_11_01_150206_create_modulos_table.php`) |
| `menu_orders` (slug `estagiario`) | Recriar com `modulo_id` e `clicks = 0` |
| `permissions` | Criar as ações `index`, `create`, `edit`, `delete` **prefixadas** com o slug do recurso (`estagiario.index`, ...), seguindo a convenção usada por `has_access()` |
| `secretarias` | Continuar derivada de `contents` ou migrar para tabela própria; **preferir FK** (`estagiario_lotacao_id`) para evitar lotações órfãs (regra R2) |

## 2. Modelos

### 2.1 `App\Models\Estagiario`

```php
enum SituacaoEstagiario: string
{
    case Ativo = 'Ativo';
    case Encerrado = 'Encerrado';
    case Rescindido = 'Rescindido';
}

class Estagiario extends Model
{
    use HasFactory, SoftDeletes;

    protected $fillable = ['nome', 'periodo_inicio', 'periodo_fim', 'situacao', 'lotacao_id'];

    protected function casts(): array
    {
        return [
            'periodo_inicio' => 'date',
            'periodo_fim'    => 'date',
            'situacao'       => SituacaoEstagiario::class,
        ];
    }

    protected static function booted(): void
    {
        static::creating(function (self $estagiario) {
            $estagiario->slug = Str::slug($estagiario->nome.'-'.$estagiario->id);
        });
    }

    public function arquivos(): HasMany
    {
        return $this->hasMany(EstagiarioArquivo::class);
    }

    public function lotacao(): BelongsTo
    {
        return $this->belongsTo(Lotacao::class);
    }
}
```

Pontos de atenção:

- **Slug estável:** no legado o slug é recalculado a cada `update`, quebrando
  URLs públicas. Na v7, o slug deve ser gerado **uma vez** (no `creating`) e
  alterado apenas por decisão explícita (com redirecionamento 301).
- O `creating` acima depende do `id`; usar `created` (como no legado) ou
  `Str::uuid()`/`Str::orderedUuid()` para gerar o id antes do insert.

### 2.2 `App\Models\EstagiarioArquivo`

```php
class EstagiarioArquivo extends Model
{
    protected $fillable = ['estagiario_id', 'path', 'nome_original', 'description', 'tamanho', 'mime'];

    protected function casts(): array
    {
        return ['tamanho' => 'integer'];
    }

    public function estagiario(): BelongsTo
    {
        return $this->belongsTo(Estagiario::class);
    }
}
```

## 3. Form Request

```php
class EstagiarioRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()?->can('update', Estagiario::class) ?? false;
    }

    public function rules(): array
    {
        return [
            'nome'           => ['required', 'string', 'max:255'],
            'periodo_inicio' => ['required', 'date_format:d/m/Y'],
            'periodo_fim'    => ['required', 'date_format:d/m/Y', 'after_or_equal:periodo_inicio'],
            'lotacao_id'     => ['required', 'exists:lotacoes,id'],
            'situacao'       => ['required', Rule::enum(SituacaoEstagiario::class)],
            'arquivo'        => ['nullable', 'array', 'max:10'],
            'arquivo.*'      => ['nullable', 'file', 'mimes:pdf', 'max:20480'],
            'arquivo_desc'   => ['nullable', 'array'],
            'arquivo_desc.*' => ['nullable', 'string', 'max:255'],
        ];
    }
}
```

Correções em relação à v5:

- `date_format` evita a data `1970-01-01` silenciosa.
- `after_or_equal` impede período invertido.
- `Rule::enum` restringe a situação no servidor (não só no `<select>`).
- `authorize()` deixa de retornar `true` incondicionalmente.
- `max:10` limita a quantidade de anexos (a v5 não tem limite).

## 4. Autorização (Policies)

```php
class EstagiarioPolicy
{
    public function viewAny(User $user): bool   { return $user->can('estagiario.index'); }
    public function view(User $user): bool      { return $user->can('estagiario.show'); }
    public function create(User $user): bool    { return $user->can('estagiario.create'); }
    public function update(User $user): bool    { return $user->can('estagiario.edit'); }
    public function delete(User $user): bool    { return $user->can('estagiario.delete'); }
    public function restore(User $user): bool   { return $user->can('estagiario.restore'); }
}
```

Regras a adotar (o legado não as tinha):

- `update`/`delete` exigem permissão **e** status do usuário; no mínimo,
  `store`/`update`/`destroy` devem deixar de ser liberados por
  `check.route.permission` (na v5 basta estar logado).
- Exclusão em lote e restauração exigem `super-admin` ou permissão específica.
- `destroyArquivo` deve exigir permissão de edição do registro.

## 5. Endpoints

### 5.1 Portal (público)

| Método | URI | Nome | Descrição |
|---|---|---|---|
| GET | `/estagiarios` | `estagiarios.index` | Listagem paginada + filtros (`busca`, `lotacao`, `situacao`) |
| GET | `/estagiarios/{estagiario:slug}` | `estagiarios.show` | Ficha individual |
| GET | `/estagiarios/exportar` | `estagiarios.export` | Exportação (`type=xls\|pdf\|csv\|txt`) |

A exportação deve sair da rota principal para permitir cache e streaming:

```php
Route::get('/estagiarios/exportar', [EstagiarioExportController::class, 'index'])
    ->name('estagiarios.export')
    ->middleware('throttle:30,1');
```

### 5.2 CMS

| Método | URI | Descrição |
|---|---|---|
| GET | `/cms/estagiarios` | Listagem (DataTable server-side) |
| GET | `/cms/estagiarios/criar` | Formulário |
| POST | `/cms/estagiarios` | Cadastro |
| GET | `/cms/estagiarios/{estagiario}/editar` | Formulário de edição |
| PUT | `/cms/estagiarios/{estagiario}` | Atualização |
| DELETE | `/cms/estagiarios/{estagiario}` | Exclusão (soft) |
| DELETE | `/cms/estagiarios/{estagiario}/arquivos/{arquivo}` | Remoção de anexo (apaga o arquivo do disco) |

Para o DataTable, o ideal é um endpoint dedicado (em vez do genérico
`datatable.*` liberado a qualquer logado):

```php
Route::get('/cms/estagiarios/datatable', EstagiarioDataTableController::class)
    ->name('cms.estagiarios.datatable')
    ->middleware(['auth', 'can:viewAny,Estagiario']);
```

com paginação real no banco (`->paginate()`) e `with('arquivos')` apenas quando
necessário.

### 5.3 API JSON (opcional)

| Método | URI | Descrição |
|---|---|---|
| GET | `/api/v1/estagiarios` | Listagem paginada (`per_page`, `busca`, `lotacao`, `situacao`) |
| GET | `/api/v1/estagiarios/{slug}` | Detalhe |
| GET | `/api/v1/estagiarios/{slug}/arquivos/{arquivo}/download` | Download autenticado do anexo |

## 6. API Resources

```php
class EstagiarioResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'             => $this->id,
            'nome'           => $this->nome,
            'periodo_inicio' => $this->periodo_inicio->toDateString(),
            'periodo_fim'    => $this->periodo_fim->toDateString(),
            'lotacao'        => $this->whenLoaded('lotacao', fn () => $this->lotacao->titulo),
            'situacao'       => $this->situacao->value,
            'slug'           => $this->slug,
            'arquivos'       => EstagiarioArquivoResource::collection($this->whenLoaded('arquivos')),
        ];
    }
}
```

## 7. Views Blade modernas

| Legado (v5) | Proposta (v7) |
|---|---|
| 6 cópias de `index/filtro/show` por template | Um componente `<x-portal.estagiarios.listing>` parametrizado pelo template ativo |
| `prefecture/estagiarios/form.blade.php` com JS inline | Componente `<x-admin.estagiario.form>` + Alpine.js para o construtor de arquivos |
| Tabela HTML gerada à mão no componente do portal | `<x-portal.estagiarios.table>` com `wire`/Turbo ou paginação server-side |
| Exportações dentro do controller do portal | `EstagiarioExportController` + `Excel::download` com `FromQuery`/`Exportable` (streaming, sem carregar tudo em memória) |

Para o `FromView` (usado hoje), a alternativa moderna é implementar
`FromQuery` + `WithHeadings`, o que gera XLSX/CSV de verdade — o arquivo
`.csv` do legado contém HTML.

## 8. Fila, cache e storage

| Aspecto | Proposta |
|---|---|
| Storage dos anexos | Disco `s3`/privado (`local` apenas em dev); o `php artisan storage:link` público deixa de ser suficiente |
| Download de anexo | Rota autenticada/assinada (expiração curta) em vez de URL pública |
| Fila | Processamento de PDF/thumbnail em `ShouldQueue` (job `ProcessarArquivoEstagiario`) |
| Cache | `Cache::remember` na lista de secretarias (view `secretarias`) e no template ativo; cache de página do portal com tag `estagiarios` invalidado no `saved`/`deleted` do model |
| Busca | Índice funcional em `lower(nome)` ou extensão `unaccent`; consulta parametrizada (`ilike` com binding) |

## 9. Riscos e decisões de equivalência funcional

| # | Risco | Decisão recomendada |
|---|---|---|
| M1 | Mudar o formato do `slug` quebra links públicos e indexação | Gerar slug uma única vez e manter o valor legado durante a migração |
| M2 | Trocar `lotacao` (texto) por FK altera o contrato da API pública | Manter a coluna textual em paralelo ou expor `lotacao` como texto na API |
| M3 | Tornar os anexos privados altera o comportamento observável (download público) | Decisão do negócio: se os PDFs contiverem dados pessoais, o download deve exigir autenticação — mudança consciente, com aviso |
| M4 | Restringir `store/update/destroy` a permissões pode bloquear operadores que dependiam da liberalidade da v5 | Migrar as permissões antes de ativar as Policies (criar `estagiario.*` para os papéis existentes) |
| M5 | Limitar anexos a 10 e 20 MB muda o comportamento | Confirmar com a área de RH/Compras; manter 20 MB no PDF |
| M6 | Remover a coluna "ID" da listagem do CMS | Trocar o rótulo para "Sequência" ou exibir o `id` real |
| M7 | Migração de `arquivo` (JSON) para tabela | Script de backfill idempotente, mantendo o JSON até a conferência |
| M8 | Exportação sem paginação pode estourar memória | Streaming via `FromQuery` + `chunk` |
| M9 | Ausência de trilha de auditoria | Implementar observer (`spatie/laravel-activitylog`) com eventos `created/updated/deleted` |

## 10. Sequência sugerida de implementação

1. Migration de `estagiarios` + `estagiario_arquivos` + índices.
2. Enums, models, factories e seeders (3 secretarias de exemplo).
3. Policies + criação das permissões `estagiario.*`.
4. `EstagiarioRequest` + service de upload (com remoção física do arquivo).
5. Controllers do CMS (CRUD + DataTable paginado) e do portal (listagem,
   ficha, exportação).
6. Componentes Blade (1 template, parametrizado).
7. Testes (feature + unit) e backfill do JSON de arquivos.
8. Auditoria (`activity()`), cache e indexação da busca.
