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

## 1. Objetivo da reconstrução

Manter o comportamento observável (cadastrar nome + código, listar, editar,
excluir em lote, alimentar o `select` de licitações) e **corrigir** os bugs
confirmados: unicidade real, permissões por Policy, relações Eloquent corretas e
ausência da rota `show` quebrada.

## 2. Schema de migração equivalente

```php
// database/migrations/2026_XX_XX_create_management_units_table.php
Schema::create('management_units', function (Blueprint $table) {
    $table->id();
    $table->string('name', 191);
    $table->string('code', 191);
    $table->timestamps();
    $table->softDeletes();
});
```

### 2.1 Unicidade real (índice parcial no PostgreSQL)

A v7 deve materializar a regra que o legado só aplicava na criação:

```php
// índice unique parcial: ignora registros em soft delete
DB::statement('CREATE UNIQUE INDEX management_units_name_unique
    ON management_units (name) WHERE deleted_at IS NULL');
DB::statement('CREATE UNIQUE INDEX management_units_code_unique
    ON management_units (code) WHERE deleted_at IS NULL');
```

Alternativa multiplataforma (Laravel 11/12): usar
`$table->unique('name')->whereNull('deleted_at')` (não suportado pelo
schema builder padrão) ou um `generated`/`citext` + trigger. Em PostgreSQL o
índice parcial é a opção mais direta.

> **Antes de criar os índices**, é preciso verificar se já existem duplicados
> (o bug U1 permitia gravá-los). Query de diagnóstico:

```sql
select name, count(*) from management_units
where deleted_at is null group by name having count(*) > 1;

select code, count(*) from management_units
where deleted_at is null group by code having count(*) > 1;
```

### 2.2 Código: VARCHAR ou coluna dedicada?

O legado trata `code` como texto livre. Duas opções:

| Opção | Quando usar | Implementação |
|---|---|---|
| Manter `string` | se o código é livre por município | como está |
| Coluna tipada | se a prefeitura padronizar (ex.: 1..N) | `unsignedSmallInteger` + tabela de referência |

Decisão de equivalência: **manter `string`** na primeira entrega da v7, pois
alterar o tipo quebraria o seed e os dados já gravados.

## 3. Modelos

```php
// app/Models/ManagementUnit.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;

class ManagementUnit extends Model
{
    use SoftDeletes;

    protected $fillable = ['name', 'code'];

    protected $casts = [
        'created_at' => 'datetime',
        'updated_at' => 'datetime',
        'deleted_at' => 'datetime',
    ];

    /** Correção do bug U12: 1 unidade -> N licitações */
    public function biddings(): HasMany
    {
        return $this->hasMany(Bidding::class, 'management_units_id', 'id');
    }
}
```

```php
// app/Models/Bidding.php (trecho corrigido)
public function managementUnit(): BelongsTo
{
    return $this->belongsTo(ManagementUnit::class, 'management_units_id', 'id');
}
```

| Legado | Moderno | Observação |
|---|---|---|
| `App\Prefecture\ManagementUnit` (em `app/Http/Model/Prefecture/`) | `App\Models\ManagementUnit` | Namespace alinhado à pasta. |
| `biddings()` = `belongsTo` | `biddings()` = `hasMany` | Correção do bug U12. |
| `managementUnits()` = `hasOne` | `managementUnit()` = `belongsTo` | Renomear para singular (uma(unit) por licitação). |
| `protected $table = 'management_units'` | implícito | padrão do Laravel 11. |
| `SoftDeletes` | `SoftDeletes` | mantido. |
| Sem `$casts` | `$casts` de timestamps | recomendado. |

## 4. Form Requests

```php
// app/Http/Requests/StoreManagementUnitRequest.php
class StoreManagementUnitRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('create', ManagementUnit::class);
    }

    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:120', Rule::unique('management_units', 'name')],
            'code' => ['required', 'string', 'max:120', Rule::unique('management_units', 'code')],
        ];
    }

    public function messages(): array
    {
        return [
            'name.required' => 'O campo nome é obrigatório.',
            'name.unique'   => 'O nome já está em uso.',
            'name.max'      => 'O campo nome atingiu o limite máximo de caracteres.',
            'code.required' => 'O campo código é obrigatório.',
            'code.unique'   => 'O código já está em uso.',
            'code.max'      => 'O campo código atingiu o limite máximo de caracteres.',
        ];
    }
}
```

```php
// app/Http/Requests/UpdateManagementUnitRequest.php
public function rules(): array
{
    return [
        'name' => ['required', 'string', 'max:120',
                   Rule::unique('management_units', 'name')->ignore($this->route('id'))],
        'code' => ['required', 'string', 'max:120',
                   Rule::unique('management_units', 'code')->ignore($this->route('id'))],
    ];
}
```

> Correção do bug U1: a tabela e a coluna passam a ser `management_units.name` e
> `management_units.code` **nos dois casos**, e o id da unidade é o do
> `Route::resource` (não `Request::segment(3)`).
>
> Observação de equivalência: o legado usa `sometimes|required` no update, o que
> permite submissões parciais. Com `required` (sem `sometimes`), a v7 exige os
> dois campos sempre — comportamento **mais estrito**, mas mais seguro. Se
> compatibilidade estrita for exigida, usar `sometimes` + `Rule::unique('management_units', 'name')->ignore($id)` (e o mesmo para `code`).

## 5. Policies / autorização

```php
// app/Policies/ManagementUnitPolicy.php
class ManagementUnitPolicy
{
    public function viewAny(User $user): bool  { return $user->can('unidade-gestora.index'); }
    public function create(User $user): bool   { return $user->can('unidade-gestora.create'); }
    public function update(User $user, ManagementUnit $u): bool { return $user->can('unidade-gestora.edit'); }
    public function delete(User $user, ManagementUnit $u): bool { return $user->can('unidade-gestora.delete'); }
    public function deleteAny(User $user): bool { return $user->can('unidade-gestora.delete'); }
}
```

Mapeamento das permissões legadas (tabela `resources` id 21 + `permissions`):

| Permissão legada | Uso |
|---|---|
| `unidade-gestora.index` | listagem |
| `unidade-gestora.create` | botão "Cadastrar Unidade Gestora" + `store` |
| `unidade-gestora.edit` | botão de editar + `update` |
| `unidade-gestora.delete` | botão de excluir, exclusão em lote + `destroy` |
| `unidade-gestora.inactivate` / `.restore` | declaradas no seed, sem uso no módulo |
| `unidade-gestora.inactive` / `.show` | declaradas no seed, sem uso no módulo |

> **Correção obrigatória (bug U8):** a heurística do
> `CheckRoutePermission` liberava `create/store/edit/update/destroy` para
> qualquer usuário autenticado. Na v7 a autorização é exclusivamente a Policy.
> O papel `super-admin` continua com acesso total via `Gate::before`.

## 6. Endpoints REST/JSON sugeridos

| Método | URI | Descrição | Equivalente legado |
|---|---|---|---|
| GET | `/api/admin/management-units` | listagem paginada + filtros | `/cms/datatable/unidade-gestora` |
| POST | `/api/admin/management-units` | cria | `POST /cms/unidade-gestora` |
| GET | `/api/admin/management-units/{id}` | detalhe | `show` (quebrado na v6) |
| PUT | `/api/admin/management-units/{id}` | atualiza | `PUT /cms/unidade-gestora/{id}` |
| DELETE | `/api/admin/management-units/{id}` | exclui (soft) | `DELETE /cms/unidade-gestora/{id}` |
| POST | `/api/admin/management-units/bulk-destroy` | exclusão em lote | `POST /cms/ajax/unidade-gestora/delete-por-lote` |
| PUT | `/api/admin/management-units/reorder` | reordena | (não existia) |

> Não criar rota `show` sem controller correspondente (bug U6). Se a tela de
> detalhe não for necessária, **omitir** a rota.

Recurso de API:

```php
// app/Http/Resources/ManagementUnitResource.php
return [
    'id'   => $this->id,
    'name' => $this->name,
    'code' => $this->code,
];
```

> Diferença deliberada em relação ao legado: o recurso **inclui `code`**, que a
> view SQL `unidade_gestora` não expunha. É uma melhoria (o operador passa a ver
> o código na listagem) e não quebra nada.

## 7. Views Blade modernas

| Legado | Moderno |
|---|---|
| `prefecture/management-units/index.blade.php` | `resources/views/admin/management-units/index.blade.php` com `<x-admin.datatable>` |
| `prefecture/management-units/create.blade.php` | `resources/views/admin/management-units/create.blade.php` |
| `prefecture/management-units/edit.blade.php` | `resources/views/admin/management-units/edit.blade.php` |
| `prefecture/management-units/form.blade.php` | `<x-admin.form.management-unit-fields>` (componente) |
| `prefecture/management-units/show.blade.php` | **remover** (bug U4) |

Componente de campos:

```blade
{{-- resources/views/components/admin/management-unit-fields.blade.php --}}
<x-admin.input name="code" label="Código" :maxlength="120" required />
<x-admin.input name="name" label="Nome" :maxlength="120" required />
```

> Correção do bug U11: usar a chave correta (`maxlength`/`maxlen`) para que o
> atributo HTML `maxlength` seja emitido.

Formulário de criação (Laravel 11/12, sem `laravelcollective/html`):

```blade
<form method="POST" action="{{ route('management-units.store') }}">
    @csrf
    <x-admin.management-unit-fields />
    <button type="submit" class="btn btn-primary">Cadastrar</button>
</form>
```

> Correção do bug U9: o `action` passa a apontar explicitamente para `store`.

## 8. Fila, cache, storage

| Aspecto | Legado | Moderno |
|---|---|---|
| Cache | nenhum | `Cache::remember('management_units.active', 300, fn () => ManagementUnit::orderBy('name')->get(['id', 'name']))` para o `select` do formulário de licitação (que hoje faz `ManagementUnit::get()` a cada abertura). |
| Fila | nenhuma | dispensável. |
| Storage | — | não se aplica (o módulo não tem anexos). |
| Exclusão em lote | `whereIn('id', $array)->delete()` (1 query) | manter 1 query com `whereIn` + `SoftDeletes` (idempotente e atômico). |
| Transação | `DB::transaction` em `store`/`update`/`destroy` | manter (o Laravel 11 já faz transação automática em requests, mas explicitar é mais claro). |

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

| # | Decisão | Justificativa |
|---|---|---|
| D1 | **Manter** a exclusão lógica (`SoftDeletes`). | Licitações referenciam a unidade via FK `NOT NULL`; exclusão física quebraria a referência. |
| D2 | **Manter** os limites de 120 caracteres em `name` e `code`. | Comportamento já publicado ao operador. |
| D3 | **Manter** as mensagens de erro em português ("O nome já está em uso."). | São exibidas ao operador e podem estar registradas em manuais. |
| D4 | **Manter** a ordem de exibição por nome (`ASC`) na listagem do CMS. | O legado força `ASC` no DataTable, apesar da view ser `created_at DESC`. |
| D5 | **Adicionar** `code` à listagem (não existia). | Melhora de UX sem quebrar contrato. |
| D6 | **Exigir** `name` e `code` em toda atualização (o legado usava `sometimes`). | Mais estrito, porém evita registros com campos vazios. Se a compatibilidade estrita for exigida, usar `sometimes`. |
| D7 | **Não** criar índice unique se já houver duplicados. | A aplicação do índice falharia; é preciso um plano de saneamento antes. |
| D8 | **Preservar** a FK `biddings.management_units_id` como `NOT NULL`. | A migração `2018_04_02_181804` pretendia nullable, mas o Laravel 5.5 ignorou; mudar a nulabilidade na v7 é uma decisão de negócio. |
| D9 | **Manter** o rótulo "Unidades Gestoras" e o slug `unidade-gestora`. | Ambos aparecem em permissões, logs e rotas existentes. |
| D10 | **Corrigir** o `menuClicks` para `unidade-gestora` **e** criar a linha em `menu_orders`. | Hoje as visitas são contabilizadas em "Licitações". Só faz sentido se o módulo for habilitado (`modulos.status = true`). |

## 10. Checklist de migração

1. Criar `management_units` (id, name, code, timestamps, softDeletes).
2. Model `ManagementUnit` com `HasMany biddings()`; corrigir `Bidding::managementUnit()`.
3. Diagnosticar e sanear duplicados de `name`/`code`.
4. Criar os índices unique parciais.
5. Form Requests `Store`/`Update` com `Rule::unique` correto.
6. Policy `ManagementUnitPolicy` + `Gate::before` para `super-admin`.
7. Controller `ManagementUnitController` (Laravel 11, `#[Route]` attributes ou `routes/web.php` do CMS) usando a Policy.
8. `ApiResource` + endpoints `/api/admin/management-units` (incluindo `bulk-destroy`).
9. Componente Blade de campos com `maxlength` correto; remover `show.blade.php`.
10. Atualizar o `select` do formulário de licitações para usar a lista cacheada.
11. Migrar o seed para `updateOrCreate` (nunca `delete()` físico).
12. Testes de equivalência: criação, unicidade, edição, exclusão, exclusão em lote e listagem.
