# Servidor — Migração para Laravel Moderno (PHP 8.x / Laravel 11-12)

> **Módulo:** `servidor` (tabela `modulos`, id 35)
> **Objetivo:** reconstruir o módulo preservando o comportamento observável, separando
> o cadastro de servidores do cadastro de secretarias e saneando os dados bancários.

---

## 1. Schema de migração equivalente

```php
// database/migrations/2025_xx_xx_create_servidor_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('servidor', function (Blueprint $table) {
            $table->id();
            $table->string('cpf', 11)->unique();          // só dígitos
            $table->string('name');
            $table->string('cargo');
            $table->string('matricula')->unique();
            $table->string('identidade');
            $table->foreignId('secretaria_id')->nullable()->constrained('secretarias')->nullOnDelete();
            $table->softDeletes();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('servidor');
    }
};
```

```php
// dados bancários — tabela separada, cifrada
Schema::create('servidor_dados_bancarios', function (Blueprint $table) {
    $table->id();
    $table->foreignId('servidor_id')->unique()->constrained('servidor')->cascadeOnDelete();
    $table->string('banco')->nullable();
    $table->text('conta')->nullable();      // cifrado (Laravel Crypt)
    $table->text('agencia')->nullable();    // cifrado
    $table->timestamps();
});
```

Decisões de equivalência:

- **Renomear `secretaria` → `secretaria_id`** e criar a tabela `secretarias` própria,
  migrando os conteúdos da categoria `secretarias` para ela. Alternativa de menor
  esforço: manter a FK para `contents(id)` — mas a v7 deve, no mínimo, trocar a coluna
  para `secretaria_id` e documentar a dependência da categoria de conteúdos.
- **CPF só com dígitos**, único — resolve a duplicidade e permite busca por índice.
- **Matrícula única** — é o identificador funcional natural do servidor.
- **Dados bancários cifrados** com `Crypt::encryptString()`, em tabela separada, com
  acesso restrito a uma permissão própria (ex.: `servidor.dados-bancarios.view`).
- **`nullOnDelete`** na FK de secretaria: ao excluir uma secretaria, o servidor fica
  sem lotação em vez de bloquear a exclusão.
- **A view `servidores` deve ser eliminada**: a listagem passa a consultar o model com
  `->whereNull('deleted_at')` e a expor `id`, `name`, `cpf`. Se for mantida por
  compatibilidade, replicar
  `v6/database/migrations/2023_03_27_154732_create_view_to_servidores.php`.

## 2. Modelos

```php
namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;

class Servidor extends Model
{
    use HasFactory, SoftDeletes;

    protected $table = 'servidor';

    protected $fillable = [
        'cpf', 'name', 'cargo', 'matricula', 'identidade', 'secretaria_id',
    ];

    protected $hidden = ['cpf', 'identidade'];

    protected $casts = [
        'cpf' => 'string',
    ];

    public function secretaria(): BelongsTo
    {
        return $this->belongsTo(Secretaria::class, 'secretaria_id');
    }

    public function dadosBancarios(): HasOne
    {
        return $this->hasOne(ServidorDadosBancarios::class);
    }

    public function requerimentos(): HasMany
    {
        return $this->hasMany(RequerimentoDiarias::class, 'servidor_id');
    }
}
```

- `secretaria()` vira `belongsTo` (corrige a semântica de `hasOne`).
- `requerimentos()` explicita a dependência que hoje só existe como FK.
- `cpf` normalizado para dígitos na entrada (mutator) e exibido mascarado na saída
  (accessor ou `Attribute::make`).

## 3. Form Requests

```php
class StoreServidorRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('servidor.create');
    }

    public function rules(): array
    {
        return [
            'name'       => ['required', 'string', 'max:191'],
            'cpf'        => ['required', 'digits:11', 'cpf', Rule::unique('servidor', 'cpf')],
            'identidade' => ['required', 'string', 'max:191'],
            'matricula'  => ['required', 'string', 'max:191', Rule::unique('servidor', 'matricula')],
            'cargo'      => ['required', 'string', 'max:191'],
            'secretaria_id' => ['nullable', 'integer', Rule::exists('secretarias', 'id')],
            'banco'      => ['nullable', 'string', 'max:191'],
            'conta'      => ['nullable', 'string', 'max:191'],
            'agencia'    => ['nullable', 'string', 'max:191'],
        ];
    }
}
```

Regras preservadas: `name`, `cpf`, `identidade`, `matricula` e `cargo` continuam
obrigatórios; a validação de CPF continua sendo por dígito verificador (reimplementar ou
reusar `Validate_cpf` como `Rule`); `banco`, `conta`, `agencia` e `secretaria` seguem
opcionais.

Mudanças deliberadas: `digits:11` no lugar de `min:11` (a regra antiga contava
caracteres e aceitava a máscara), unicidade de CPF e matrícula, `exists` na secretaria.

## 4. Policies

```php
class ServidorPolicy
{
    public function viewAny(User $user): bool { return $user->can('servidor.index'); }
    public function view(User $user, Servidor $s): bool { return $user->can('servidor.edit'); }
    public function create(User $user): bool { return $user->can('servidor.create'); }
    public function update(User $user, Servidor $s): bool { return $user->can('servidor.update'); }
    public function delete(User $user, Servidor $s): bool { return $user->can('servidor.delete'); }
    public function deleteAny(User $user): bool { return $user->can('servidor.delete'); }
    public function viewBankData(User $user, Servidor $s): bool { return $user->can('servidor.dados-bancarios'); }
    public function export(User $user): bool { return $user->can('servidor.export'); }
}
```

Elimina o comportamento herdado de `check.route.permission`, em que as rotas `.store`,
`.update` e `.destroy` ficavam abertas a qualquer usuário autenticado.

## 5. Endpoints REST/JSON sugeridos

| Método | URI | Quem usa | Descrição |
|---|---|---|---|
| GET | `/api/servidores` | CMS | lista paginada (DataTables server-side) |
| POST | `/api/servidores` | CMS | cadastra |
| GET | `/api/servidores/{servidor}` | CMS | detalhe (substitui a rota `show` quebrada) |
| PUT | `/api/servidores/{servidor}` | CMS | edita |
| DELETE | `/api/servidores/{servidor}` | CMS | soft delete |
| POST | `/api/servidores/bulk-delete` | CMS | exclusão em lote (ids no corpo) |
| GET | `/api/servidores/{servidor}/dados-bancarios` | CMS | dados bancários (exige permissão própria) |
| GET | `/api/servidores/opcoes` | CMS/Requerimento de Diárias | lista simples `{id, name, cpf}` para selects |

O endpoint `/api/servidores/opcoes` resolve o carregamento de **todos** os servidores no
formulário de Requerimento de Diárias: a v7 deve paginar ou filtrar por secretaria.

## 6. API Resources

```php
class ServidorResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id'           => $this->id,
            'name'         => $this->name,
            'cpf'          => $this->cpf_formatado,      // só com permissão
            'cargo'        => $this->cargo,
            'matricula'    => $this->when($this->canSeeMatricula($request), $this->matricula),
            'identidade'   => $this->when($this->canSeeMatricula($request), $this->identidade),
            'secretaria'   => SecretariaResource::make($this->whenLoaded('secretaria')),
            'dados_bancarios' => ServidorDadosBancariosResource::make($this->whenLoaded('dadosBancarios')),
            'deleted_at'   => $this->deleted_at,
        ];
    }
}
```

Regra de ouro da v7: **CPF, identidade e dados bancários só saem com permissão
específica**.

## 7. Views Blade modernas

- Componente `<x-servidor-form :servidor="$servidor" :secretarias="$secretarias" />`,
  usado por create e edit (hoje a mesma view atende os dois casos via `isset($data)`).
- Componente `<x-servidor-select name="servidor_id" />` para o formulário de
  Requerimento de Diárias, com busca por nome/CPF via AJAX (hoje são todos os
  servidores materializados).
- A listagem pode migrar para tabela paginada do Laravel (Livewire/Inertia), eliminando
  a dependência da view `servidores` e do JS `datatable.js`.
- Remover o carregamento do Summernote (não há campo de texto rico no formulário).

## 8. Fila, cache e storage

| Item | Situação na v6 | Sugestão v7 |
|---|---|---|
| Cache | nenhum neste módulo | cachear a lista de secretarias (`Cache::remember`) por alguns minutos, ou usar `spatie/laravel-model-caching` |
| Fila | nenhuma | opcionalmente, gerar o "Termo de Posse" ou o PDF do requerimento em background |
| Storage | nenhum arquivo | se houver comprovantes, usar `storage/app/servidores` |
| Auditoria | **ausente** | registrar create/update/delete em log de atividade (`spatie/laravel-activitylog`), com mascaramento de CPF |
| Rate limit | não se aplica (rota de CMS) | proteger a busca pública de diárias |

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

| Item | Decisão obrigatória |
|---|---|
| Tabela `servidor` (singular) | manter o nome ou renomear para `servidores` com migração de dados; se renomear, atualizar a FK de `requerimento_diarias` e as views SQL de diárias |
| Coluna `secretaria` apontando para `contents` | **decisão de projeto**: manter (comentário explícito) ou migrar para uma tabela `secretarias` própria |
| CPF armazenado com ou sem máscara | padronizar em dígitos; impacta a busca por CPF da rota pública |
| Exclusão lógica | manter |
| Sem unicidade de CPF | a v7 deve passar a exigir unicidade — exige tratar duplicidades existentes no banco |
| Campos `banco`/`conta`/`agencia` | **decisão de negócio**: manter (cifrados) ou remover do cadastro |
| Lista de secretarias carregada no construtor | mover para o controller da ação que precisa dela (`only` em `create`/`edit`) |
| Ausência de rota `show` | criar o endpoint de detalhe ou manter a ausência de propósito |
| Exclusão em lote reportando sucesso | corrigir para devolver o número de registros afetados |
| Coluna "ID" da listagem | decidir se passa a ser o `servidor.id` real (recomendado) ou mantém o `row_number()` |
| Exigência de preenchimento dos dados bancários | hoje opcional; o "Manual de Servidor" explica que só é obrigatório se o servidor usar Requerimento de Diárias — manter essa regra ou torná-la condicional no backend |

## 10. Checklist de migração de dados

1. Criar a tabela `secretarias` e migrar os conteúdos da categoria `secretarias`.
2. Migrar `servidor.secretaria` → `servidor.secretaria_id`.
3. Normalizar o CPF (só dígitos), resolver duplicidades e criar o índice único.
4. Extrair `banco`, `conta`, `agencia` para `servidor_dados_bancarios` e cifrar.
5. Substituir a view `servidores` por query Eloquent (ou recriá-la com o novo schema).
6. Atualizar a FK `requerimento_diarias.id_servidor` → `servidor.id` (ou
   `servidor_id`, se renomeado) e as views de requerimentos que fazem
   `join servidor s on s.id = rd.id_servidor`.
7. Corrigir a busca pública (`BuscaDiariasController`), eliminando a concatenação em
   `whereRaw`.
8. Criar as policies, os Form Requests e a factory `ServidorFactory`.
9. Remover `dd()` do `Repository` base (ou isolar o tratamento de exceção por módulo).
