# Unidade Gestora — Modelagem de Dados

O módulo tem **uma tabela própria** (`management_units`), uma view SQL de leitura
para o CMS e uma FK que a consome em `biddings`.

## 1. Tabela `management_units`

Migration: `v6/database/migrations/2018_04_02_181802_create_management_units_table.php`
(classe `CreateManagementUnitsTable`). É o arquivo **mais antigo** do módulo
(2018-04-02) e é **byte-idêntico** entre v5 e v6.

```php
Schema::create('management_units', function (Blueprint $table) {
    $table->engine = 'InnoDB';
    $table->increments('id');
    $table->string('name');
    $table->string('code');
    $table->timestamps();
    $table->softDeletes();
});
```

Dump real do banco `prefeituras_v5`:

| Coluna | Tipo | Null | Default | Observação |
|---|---|---|---|---|
| `id` | `integer` (serial) | não | `nextval('management_units_id_seq')` | PK |
| `name` | `character varying(191)` | **não** | — | Nome da unidade gestora |
| `code` | `character varying(191)` | **não** | — | Código da unidade |
| `created_at` | `timestamp(0)` sem fuso | sim | — | Eloquent `timestamps` |
| `updated_at` | `timestamp(0)` sem fuso | sim | — | Eloquent `timestamps` |
| `deleted_at` | `timestamp(0)` sem fuso | sim | — | `SoftDeletes` |

Índices: apenas `management_units_pkey` (btree em `id`).

> **Observações de schema:**
> - A migration declara `$table->engine = 'InnoDB'`, mas o banco é **PostgreSQL 10**;
>   a diretiva é ignorada pelo driver `pgsql`.
> - `name` e `code` são `NOT NULL` **sem índice unique**. A unicidade é garantida
>   apenas pela validação da aplicação (e ela é quebrada na atualização — ver
>   `08-GAPS-E-DIVIDAS.md`, bug U1).
> - `string()` sem tamanho geraria `varchar(255)`, mas o projeto define
>   `Schema::defaultStringLength(191)` em
>   `v6/app/Providers/AppServiceProvider.php:23` — daí o `varchar(191)` real.
> - O `code` é `NOT NULL` mas **não tem restrição** de formato: o seed usa `'1'` e
>   `'2'` (texto), o formulário aceita qualquer string de até 120 caracteres.

## 2. FK `biddings.management_units_id`

| Aspecto | Valor |
|---|---|
| Coluna | `biddings.management_units_id` — `integer` **NOT NULL** |
| Constraint | `biddings_management_units_id_foreign` |
| Referência | `management_units(id)` |
| `ON DELETE` | **não há** `ON DELETE` — padrão `NO ACTION` |

A migration de `biddings` (`v6/database/migrations/2018_04_02_181804_create_biddings_table.php:36-38`)
declara a FK assim:

```php
$table->foreign('management_units_id')
    ->nullable(true)
    ->references('id')
    ->on('management_units');
```

O `->nullable(true)` é um **no-op** no Laravel 5.5: `Blueprint::foreign()` devolve
um `Fluent` e `Grammar::compileForeign()`
(`v6/vendor/laravel/framework/src/Illuminate/Database/Schema/Grammars/Grammar.php:57-88`)
nunca lê o atributo `nullable` — ele só emite `alter table <tabela> add constraint <nome> foreign key (<colunas>) references <tabela_referenciada> (<colunas>)`.
Por isso a coluna ficou **NOT NULL** no PostgreSQL, apesar da intenção da migration.

Sem `ON DELETE`, o PostgreSQL **impede** a remoção física de uma unidade que
tenha licitações. Na prática, como o módulo usa `SoftDeletes`, a remoção é lógica
e nunca esbarra na FK — mas qualquer `TRUNCATE`/`DELETE` físico em scripts de
manutenção falha.

Dump real de `biddings` (colunas relevantes):

```
 management_units_id     | integer                        |           | not null | 
Foreign-key constraints:
    "biddings_management_units_id_foreign" FOREIGN KEY (management_units_id) REFERENCES management_units(id)
```

> `management_units_id` é `NOT NULL` em `biddings`: **toda licitação precisa de uma
> unidade gestora**. O `BiddingRequest` reforça isso com
> `'management_units_id' => [new SelectRule]`
> (`v6/app/Http/Requests/BiddingRequest.php:60,74`).

## 3. View SQL `unidade_gestora`

É a fonte de dados da listagem do CMS.

Migration: `v6/database/migrations/2023_04_11_101345_create_view_to_unidade_gestora.php`
(classe `CreateViewToUnidadeGestora`) — **byte-idêntica** entre v5 e v6.

```php
DB::statement("
    CREATE OR REPLACE view unidade_gestora
    AS SELECT row_number() OVER (ORDER BY mu.created_at DESC) AS value,".
        'mu."name" as nome,'.
        ' mu.id
    FROM management_units mu 
        WHERE mu.deleted_at IS NULL
    ORDER BY mu.created_at DESC; '
);
```

| Coluna da view | Origem | Uso |
|---|---|---|
| `value` | `row_number()` sobre `created_at DESC` | numeração exibida na 1ª coluna do DataTable |
| `nome` | `management_units.name` | coluna "Nome" |
| `id` | `management_units.id` | coluna "ID" e ações (editar/excluir) |

Características:

- Filtra `deleted_at IS NULL` — unidades excluídas (logicamente) somem da listagem.
- **Não expõe `code`** — o código da unidade não aparece na listagem do CMS,
  apenas no formulário de edição.
- A ordenação da view é por `created_at DESC` (mais recentes primeiro), mas o
  DataTable força `ASC` por nome:
  `runDatatables('unidade-gestora', collums, true, 'ASC', null, true)`.
- A view **não é atualizada** quando o `order`/`name` muda: ela é um snapshot
  calculado em tempo de consulta (a ordenação só reflete a data de criação).

Rollback: `DB::statement("DROP VIEW IF EXISTS PUBLIC.unidade_gestora");`

## 4. Modelo Eloquent

```php
// v6/app/Http/Model/Prefecture/ManagementUnit.php
namespace App\Prefecture;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Model;

class ManagementUnit extends Model
{
    use SoftDeletes;
    protected $table = 'management_units';

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

    public function biddings()
    {
        return $this->belongsTo(Bidding::class, 'management_units_id','id');
    }
}
```

Pontos frágeis do model:

- **Namespace `App\Prefecture`**, apesar de o arquivo estar em
  `app/Http/Model/Prefecture/`. O autoload do `v6/composer.json` usa
  `"classmap": ["app/"]` + `"psr-4": {"App\\": "app/"}`; a classe é resolvida pelo
  classmap, o que permite o namespace "desalinhado" da pasta. O mesmo ocorre com
  `App\Prefecture\Bidding`, `App\Prefecture\Content`, etc.
- **Sem `$casts`** — `deleted_at` e os timestamps chegam como string.
- **Sem `$dates`** — `deleted_at` não é convertido para `Carbon` automaticamente
  (o `SoftDeletes` do Laravel 5.5 já o trata como data).
- **Relação `biddings()` com cardinalidade errada**: `belongsTo` para uma FK que
  é `many-to-one` (várias licitações -> uma unidade). O correto seria
  `hasMany(Bidding::class, 'management_units_id', 'id')`.
  Do lado do `Bidding`, `managementUnits()` é `hasOne(ManagementUnit::class, 'id', 'management_units_id')`
  (`v6/app/Http/Model/Prefecture/Bidding.php:114`) — o correto seria
  `belongsTo(ManagementUnit::class, 'management_units_id', 'id')`.
  As duas relações estão **invertidas** (bug U4).
- **`protected $primaryKey = 'id'`** é redundente (já é o default).

## 5. Registro do módulo

| Tabela | Colunas | Registro (consulta somente-leitura em `prefeituras_v5`) |
|---|---|---|
| `modulos` | `id`, `name`, `slug`, `status` | `id = 50`, `name = 'Unidades Gestoras'`, `slug = 'unidade-gestora'`, **`status = false`** (desabilitado) |
| `resources` | `id`, `slug`, `name`, `description`, `modulo_id` | `id = 21`, `slug = 'unidade-gestora'`, `name = 'Unidades Gestoras'`, `description = 'Funcionalidade Unidade Gestora'`, `modulo_id = 50` |
| `menu_orders` | `id`, `slug`, `clicks`, `modulo_id` | **não existe** linha com `slug = 'unidade-gestora'` (45 linhas na tabela; nenhuma para este módulo) |

Seed de `modulos` (`v6/database/migrations/2024_11_01_150206_create_modulos_table.php:322-327`):

```php
// ID 50
DB::table('modulos')->insert([
    'name' => 'Unidades Gestoras',
    'slug' => 'unidade-gestora',
    'status' => 0,
]);
```

Seed de `resources` (`v6/database/seeds/ResourcesTableSeeder.php:171-177`):

```php
Resource::updateOrCreate(['slug' => 'unidade-gestora'],
    [
        'name' => 'Unidades Gestoras',
        'description' => 'Funcionalidade Unidade Gestora',
        'modulo_id' => 50,
        'created_at' => date("Y-m-d H:i:s")
    ]
);
```

> Como `modulos.status = false`, o módulo **não aparece** na matriz de permissões
> do editor de perfis (`v6/resources/views/admin/role/form.blade.php:45` exige
> `$resource->modulo == null || $resource->modulo->status`). Ou seja: **não é
> possível conceder as permissões `unidade-gestora.*` a partir do CMS** enquanto o
> módulo estiver desabilitado — o papel `administrador` só as tem porque o seed
> `v6/database/seeds/RolesTableSeeder.php:62` as grava diretamente.

## 6. Estado atual dos dados

```sql
select id, name, code, created_at, deleted_at from management_units order by id;
--  id |                               name                               | code |       created_at       | deleted_at
--   1 | Instituto de Prev. Social dos Servidores da Prefeitura Municipal |    1 | 2026-10-21 08:52:42 |
--   2 | Prefeitura Municipal                                             |    2 | 2026-10-21 08:52:42 |

select count(*) from biddings where management_units_id is not null;  -- 0
```

As duas linhas são exatamente as do seed `ManagementUnitTableSeeder`. Não há
licitações na base de desenvolvimento, e **nenhum** `biddings` referencia as
unidades.

## 7. Seed

`v6/database/seeds/ManagementUnitTableSeeder.php` (registrado em
`v6/database/seeds/DatabaseSeeder.php:43`):

```php
DB::table('management_units')->delete();
DB::table('management_units')->insert([
    'name' => 'Instituto de Prev. Social dos Servidores da Prefeitura Municipal',
    'code' => '1',
    'created_at' => date("Y-m-d H:i:s")
]);
DB::table('management_units')->insert([
    'name' => 'Prefeitura Municipal',
    'code' => '2',
    'created_at' => date("Y-m-d H:i:s")
]);
```

Características:

- **Não é idempotente**: `delete()` + `insert()` — roda sempre limpo, mas apaga
  qualquer unidade cadastrada pelo operador.
- O `delete()` é **físico** (Query Builder, sem SoftDeletes) — por isso a FK de
  `biddings` quebraria o seed caso houvesse licitações. Na base atual não há.
- `updated_at` não é preenchido (o `insert` do Query Builder ignora o cast de
  timestamps do Eloquent).
- O `code` é uma string numérica (`'1'`, `'2'`) — o campo aceita texto livre.

## 8. Migrations do módulo

| Migration | Efeito |
|---|---|
| `v6/database/migrations/2018_04_02_181802_create_management_units_table.php` | Cria `management_units` (id, name, code, timestamps, softDeletes). |
| `v6/database/migrations/2023_04_11_101345_create_view_to_unidade_gestora.php` | Cria a view SQL `unidade_gestora`. |
| `v6/database/migrations/2024_11_01_150206_create_modulos_table.php` | Registra o módulo (id 50) e cria a tabela `modulos`. |

> Não há migration que **altere** `management_units` (nada de rename de coluna,
> nada de índice unique). A tabela está como foi criada em 2018.

## 9. Colunas homônimas em outras tabelas (cuidado)

| Tabela | Coluna | Referência real |
|---|---|---|
| `biddings` | `management_units_id` | `management_units.id` (FK real deste módulo) |
| `emendas_parlamentares` | `unidade_gestora_id` | `emendas_parlamentares_categories.id` (**outro** domínio) |
| `emendas_parlamentares_categories` | `type` | coluna de texto que pode valer `'unidade_gestora'` |

Não confundir: as emendas parlamentares têm sua própria taxonomia de unidades
gestoras, independente de `management_units`.
