# Órgãos — Modelagem de Dados

> Escopo: tabelas, colunas, relacionamentos, views SQL, migrations e seeds.
> Fonte: schema PostgreSQL `prefeituras_v5` (verificado via `psql`) e migrations em `v6/database/migrations/`.

---

## 1. Tabelas

### 1.1 `contents` — a tabela que guarda os órgãos

Não existe tabela `orgaos`. Um órgão é uma linha de `contents` com `category_contents_id = 32`.

| Coluna | Tipo | Null | Default | Papel para o módulo |
|---|---|---|---|---|
| `id` | `serial` | não | `nextval(...)` | PK; é a FK referenciada por `contents.orgaos_id` e `calendar_events.orgaos_id` |
| `title` | `varchar(191)` | não | — | Nome do órgão (ex.: "PROCON"); `max:120` na validação |
| `slug` | `varchar(191)` | — | — | Used na URL `/a-instituicao/orgaos/{slug}`; gerado por `Str::slug` com colisão resolvida por sufixo |
| `text` | `text` | — | — | Texto institucional (HTML do Summernote); `required` |
| `linha_fina` | `varchar` | sim | — | Linha de apoio (não exposto no formulário de órgãos) |
| `category_contents_id` | `integer` | — | — | **32** para órgãos; FK `category_contents.id` |
| `secretaria` | `varchar` | sim | — | Usado por notícias para vincular a secretaria pelo **slug**; não aplicável a órgãos |
| `orgaos_id` | `integer` | sim | — | **Auto-FK**: aponta para o `contents.id` do órgão; preenchido só em notícias |
| `clicks` | `integer` | — | `0` | Contador incremental a cada visita à página do órgão (`updateClicks`) |
| `galery` | `boolean` | — | — | Habilita a aba Galeria no CMS e a galeria no portal |
| `highlights` | `boolean` | — | — | Destaque na home |
| `publication_date` | `timestamp` | sim | — | `casts` => `datetime`; base da ordenação da API |
| `status` | `boolean` | — | — | Publicação |
| `order` | `integer` | — | — | Ordenação manual |
| `created_at` / `updated_at` | `timestamp` | sim | — | `row_number()` da view usa `created_at` |
| `deleted_at` | `timestamp` | sim | — | SoftDeletes; a view SQL filtra `deleted_at IS NULL` |

> `contents.orgaos_id` foi adicionada por `v6/database/migrations/2023_05_19_114307_add_orgao_to_content.php`
> com `->integer('orgaos_id')->nullable()` e FK `->references('id')->on('contents')` — uma **auto-referência**.
>
> `contents.secretaria` foi adicionada por `v6/database/migrations/2022_04_19_084011_add_secretaria_to_contents_table.php`
> (string), e é o gancho do vínculo notícia↔secretaria **por slug** (não por id) — para notícias, não para órgãos.

### 1.2 `category_contents` — a categoria do módulo

| Coluna | Tipo | Valor para `orgaos` |
|---|---|---|
| `id` | `serial` | **32** |
| `name` | `varchar(191)` | `Órgãos` |
| `slug` | `varchar(191)` | `orgaos` |
| `category_contents_id` | `integer` | **NULL** (categoria raiz, não subcategoria) |
| `status` | `boolean` | `true` |
| `status_create` | `boolean` | `1` (criada pelo seeder) |
| `layout` | `varchar` | NULL |

Verificado no banco:

```
 id |  name  | slug  | category_contents_id | status
----+--------+-------+----------------------+--------
 32 | Órgãos | orgaos |                      | t
```

### 1.3 `menu_items` — o menu do portal

| Coluna | Tipo | Papel |
|---|---|---|
| `id` | `serial` | PK |
| `name` | `varchar(191)` | Rótulo do item |
| `menus_id` | `integer` | `1` = menu principal do portal; `2` = menu secundário |
| `menus_items_id` | `integer` | Auto-FK: item pai (NULL = raiz) |
| `type` | `integer` | `0` = link externo, `1` = página estática, `2` = conteúdo |
| `links_id` | `integer` | FK `links.id` (usado no item agrupador "Órgãos", `links_id = 30`) |
| `category_contents_id` | `integer` | **32** para todos os itens de órgão; FK `category_contents.id` |
| `contents_id` | `integer` | FK `contents.id` (órgão específico) |
| `order` | `integer` | Posição no menu |
| `is_active` | `boolean` |(default `true`) | Item ativo no menu |
| `is_mega_menu` | `boolean` |(default `false`) | Renderização como mega menu |

Estado atual (verificado):

| id | order | name | menus_id | menus_items_id | type | category_contents_id | contents_id |
|---|---|---|---|---|---|---|---|
| 16 | 10 | Órgãos | 1 | 11 | 0 | 32 | NULL |
| 17 | 11 | Gabinete do Prefeito | 1 | 16 | 2 | 32 | 10 |
| 30 | 24 | PROCON | 1 | 16 | 2 | 32 | 23 |
| 31 | 25 | Procuradoria Geral do Município | 1 | 16 | 2 | 32 | 24 |
| 80 | 30 | Gabinete do Prefeito | 2 | 77 | 2 | 32 | 10 |

O item **16** ("Órgãos") é o agrupador; os itens **17, 30, 31** são as entradas de menu de cada órgão e
apontam para `contents_id` (o órgão).

### 1.4 `calendar_events` — agenda vinculada ao órgão

| Coluna | Tipo | Papel |
|---|---|---|
| `orgaos_id` | `integer` | FK `contents.id` (nullable) — o órgão dono do evento |

Criada por `v6/database/migrations/2023_05_26_110237_add_orgaos_to_calendar_events.php`:

```php
Schema::table('calendar_events', function (Blueprint $table) {
    $table->integer('orgaos_id')->nullable(true);

    $table->foreign('orgaos_id')
        ->nullable(true)
        ->references('id')
        ->on('contents');
});
```

Verificado no banco: `SELECT count(*) FROM calendar_events WHERE orgaos_id IS NOT NULL` -> **0**.

### 1.5 `modulos` — flag do módulo

| Coluna | Valor |
|---|---|
| `id` | 17 |
| `name` | `Órgãos` |
| `slug` | `orgaos` |
| `status` | **`false`** |

A migration `v6/database/migrations/2024_11_01_150206_create_modulos_table.php:124-129` já cria o módulo com
`'status' => 0`. Verificado no banco atual: `modulos.status` é `true` apenas para `a-instituicao` (12) e
`legislacao` (19); `orgaos` (17), `a-cidade` (13) e `secretarias` (18) estão `false`.

### 1.6 `resources` e `roles` — permissões

`v6/database/seeds/ResourcesTableSeeder.php:108-115` cria o recurso:

```php
Resource::updateOrCreate(['slug' => 'orgaos'],
    [
        'name' => 'Órgãos',
        'description' => 'Funcionalidade Órgãos',
        'modulo_id' => 17,
        'created_at' => date("Y-m-d H:i:s")
    ]
);
```

`v6/database/seeds/RolesTableSeeder.php:52` concede ao papel `administrador`:

```
"orgaos.index":true,"orgaos.create":true,"orgaos.edit":true,"orgaos.delete":true,
"orgaos.inactive":true,"orgaos.inactivate":true,"orgaos.restore":true,"orgaos.show":true
```

### 1.7 `menu_orders` — posição no menu lateral do CMS

| id | slug | clicks | modulo_id |
|---|---|---|---|
| 12 | `orgaos` | 0 | 17 |

`v6/database/seeds/MenuOrderSeeder.php:87` cria `['slug' => 'orgaos']`.

## 2. Relacionamentos

### 2.1 Eloquent (`v6/app/Http/Model/Prefecture/Content.php`)

```php
// órgão -> notícias vinculadas (hasMany pela auto-FK)
public function orgaos()
{
    return $this->hasMany(Content::class, 'orgaos_id', 'id');
}

// órgão -> eventos de agenda
public function eventosOrgaos()
{
    return $this->hasMany(CalendarEvent::class, 'orgaos_id', 'id');
}

// conteúdo -> categoria (32 = Órgãos)
public function categoryContents()
{
    return $this->belongsTo(CategoryContent::class, 'category_contents_id', 'id');
}
```

### 2.2 Diagrama de relacionamentos

```
        category_contents (32 'orgaos')
                    │ 1
                    │
                    │ N
                 contents  ─────────────────────────────┐
                    ▲  │                                │
                    │  │ hasMany 'orgaos_id' -> 'id'    │ hasOne (highlight=1)
                    │  │                                ▼
                    │  └──────────────►           archives
                    │                           (galeria: highlight NULL)
                    │
                    │  FK contents.orgaos_id (auto)
        calendar_events
        (orgaos_id -> contents.id)

        menu_items.category_contents_id -> category_contents.id  (32)
        menu_items.contents_id          -> contents.id          (órgão no menu)
        resources.modulo_id             -> modulos.id           (17)
        menu_orders.modulo_id           -> modulos.id           (17)
```

### 2.3 Mapeamento "auto-FK" — armadilha conceitual

`contents.orgaos_id` referencia `contents.id`. Isso significa que **a mesma tabela é órgão e notícia**.
A distinção é feita exclusivamente por `category_contents_id`:

| `category_contents_id` | Papel da linha |
|---|---|
| 32 | a linha **é** um órgão; `orgaos_id` deve ser NULL |
| 1 (`noticias`) e filhas | a linha **é** uma notícia; `orgaos_id` aponta para o órgão |

O mesmo padrão vale para secretarias, mas com uma diferença crítica: `contents.secretaria` é uma **string**
que guarda o **slug** da secretaria, e a relação `Content::secretarias()` é
`hasMany(Content::class, 'secretaria', 'slug')` — o id nunca é usado. Para órgãos, a relação usa o **id**.

Verificado no banco: `SELECT count(*) FROM contents` -> 184; `count(orgaos_id)` -> **0**. Nenhuma notícia
está vinculada a órgão.

## 3. Views SQL

### 3.1 `public.a_orgaos` (v6) — usada pela grade do CMS

Definição atual no banco (via `pg_get_viewdef`):

```sql
SELECT row_number() OVER (ORDER BY c.created_at DESC) AS value,
       c.title AS titulo,
       c.id,
       c.galery AS galeria,
       c.slug
FROM contents c
  JOIN category_contents cc ON cc.id = c.category_contents_id
WHERE c.deleted_at IS NULL AND cc.slug::text = 'orgaos'::text
ORDER BY c.created_at DESC;
```

| Coluna | Tipo | Origem | Uso no CMS |
|---|---|---|---|
| `value` | `bigint` | `row_number()` sobre `created_at DESC` | Coluna "ID" ordinal da grade (renderizada pelo `unshift` de `runDatatables`) |
| `titulo` | `varchar(191)` | `c.title` | Coluna "Título" |
| `id` | `integer` | `c.id` | Usado nos botões Editar/Excluir/Galeria |
| `galeria` | `boolean` | `c.galery` | Habilita/desabilita o botão Galeria |
| `slug` | `varchar(191)` | `c.slug` | Monta `/a-instituicao/orgaos/${slug}` |

**O que a view abre:** apenas a grade DataTable do CMS (`DatatableController@get_orgaos`). Nem o portal
público nem a API consultam a view — ambos usam Eloquent sobre `contents`.

### 3.2 Histórico de definição (v6)

| Migration | Efeito |
|---|---|
| `v6/database/migrations/2023_04_10_145838_create_view_to_orgaos.php` | Cria `public.a_orgaos` com `value`, `titulo`, `id`, `galeria` (sem `slug`) |
| `v6/database/migrations/2024_11_29_093120_update_view_orgaos.php` | `DROP VIEW IF EXISTS a_orgaos` + recria **com `slug`**; o `down()` recria a versão sem `slug` |

### 3.3 Divergência v5 (detalhada no capítulo 8)

Na `v5` a view final chama-se **`public.orgaos`**, não `a_orgaos`, e carrega o filtro extra
`AND cc.category_contents_id IS NULL`. A migration que produz essa mudança
(`v5/database/migrations/2025_05_19_165859_update_orgaos_view.php`) **não existe na v6**.

Estado atual do banco (que segue a v6): existe `a_orgaos` e **não** existe `orgaos` (confirmado em
`information_schema.views`).

### 3.4 Outras views que tangenciam o módulo

`public.orgaos` seria a equivalente; `ouvidoria_setores_orgaos` (view) refere-se a **setores da ouvidoria**
(setores de atendimento), que é outro conceito — não confundir com órgãos.

## 4. Migrations que afetam o módulo

| Migration (v6) | Data | Efeito |
|---|---|---|
| `2023_04_10_145838_create_view_to_orgaos.php` | 2023-04-10 | Cria `public.a_orgaos` (sem `slug`) |
| `2023_05_19_114307_add_orgao_to_content.php` | 2023-05-19 | `contents.orgaos_id` (integer nullable, FK `contents.id`) |
| `2023_05_26_110237_add_orgaos_to_calendar_events.php` | 2023-05-26 | `calendar_events.orgaos_id` (integer nullable, FK `contents.id`) |
| `2024_11_01_150206_create_modulos_table.php` | 2024-11-01 | Insere o módulo `orgaos` (id 17) com `status = 0` |
| `2024_11_29_093120_update_view_orgaos.php` | 2024-11-29 | Recria `a_orgaos` **com** `slug` |

Migrations da `v5` ausentes na `v6`:

| Migration (v5) | Data | Efeito |
|---|---|---|
| `2025_05_19_165859_update_orgaos_view.php` | 2025-05-19 | `DROP VIEW a_orgaos` + cria `public.orgaos` **com** `AND cc.category_contents_id IS NULL` |
| `2026_03_09_000000_update_views_to_include_slug.php` | 2026-03-09 | Recria `public.orgaos` com `slug` (e ajusta `a_cidade`, `a_prefeitura`, `secretarias`) |

## 5. Seeds

| Seed | Trecho | Efeito |
|---|---|---|
| `v6/database/seeds/CategoryContentTableSeeder.php:180-185` | `CategoryContent::updateOrCreate(['slug' => 'orgaos'], ['name' => 'Órgãos', 'status_create' => 1]);` | Cria a categoria (id 32) |
| `v6/database/seeds/ContentsTableSeeder.php:78-85` | `Content::updateOrCreate(['slug' => 'gabinete-do-prefeito'], ['category_contents_id' => $categoryOrgaos->id])` | Conteúdo de exemplo "Gabinete do Prefeito" |
| `v6/database/seeds/MenuItemsTableSeeder.php:196-224` | `MenuItem::updateOrCreate(['name' => 'Órgãos', 'menus_id' => 1], [... 'links_id' => $links['Órgãos'], 'category_contents_id' => $categories['orgaos']])` e o item filho "Gabinete do Prefeito" | Item agrupador + item filho no menu do portal |
| `v6/database/seeds/MenuOrderSeeder.php:87` | `['slug' => 'orgaos']` | Entrada do menu lateral do CMS |
| `v6/database/seeds/ResourcesTableSeeder.php:108-115` | `Resource::updateOrCreate(['slug' => 'orgaos'], [... 'modulo_id' => 17])` | Recurso de permissões |
| `v6/database/seeds/RolesTableSeeder.php:52` | bloco `orgaos.*` | Permissões do papel `administrador` |

## 6. Dados observados no banco atual

| Métrica | Valor |
|---|---|
| Conteúdos na categoria 32 (`orgaos`) | **3** |
| `id` / `title` / `slug` | 10 / Gabinete do Prefeito / `gabinete-do-prefeito`; 23 / PROCON / `procon`; 24 / Procuradoria Geral do Município / `procuradoria-geral` |
| `contents` com `orgaos_id` preenchido | **0** de 184 |
| `calendar_events` com `orgaos_id` preenchido | **0** |
| `archives` ligados a conteúdos da categoria 32 | **0** |
| `contents.galery` na categoria 32 | `false` nos 3 registros |
| Itens de menu com `category_contents_id = 32` | 5 (ids 16, 17, 30, 31, 80) |
| `modulos.status` de `orgaos` | `false` |

> Os 3 órgãos cadastrados **não têm imagem de destaque, nem galeria** no banco atual; todos têm
> `text` com 11 caracteres (`Em breve...`, gravado pelo seed) e `publication_date`/`status` **NULL**.
> `publication_date` NULL importa: o filtro aplicado à lista de notícias do órgão é
> `where("publication_date", "<=", $date)` em memória, e, como o filtro roda sobre a Coleção do PHP,
> itens com data NULL **não** são removidos.
> O slot `text` do componente
> `v6/resources/views/portal/layouts/modelo1/informacoes-instituicao/components/components-a-prefeitura/index.blade.php`
> decide entre conteúdo e o bloco "CONTEÚDO NÃO ENCONTRADO" via `if (!empty($text))`.

## 7. Índices e observações de banco

- Não há índice dedicado em `contents(category_contents_id)` nem em `contents(orgaos_id)` na definição
  legada [NÃO VERIFICADO para índices adicionais criados fora de migrations].
- `contents.slug` é usado como chave de URL e como chave de deduplicação no `ContentController::store()`
  (`Content::where('slug', str_slug($title))->get()`), mas **não há constraint `UNIQUE`** — a colisão é
  resolvida em aplicação com o sufixo `-<maxid+1>`.
- A view `a_orgaos` usa `row_number() OVER (ORDER BY c.created_at DESC)`, que é reavaliado a cada
  consulta; combinado com `DB::table(...)->get()` sem `LIMIT`, o custo cresce linearmente com a tabela.
