# Mais Conteúdos — Modelagem de Dados

## 1. A tabela `category_contents`

"Mais Conteúdos" **não tem tabela própria**. Tudo mora em `category_contents`, criada por
`v6/database/migrations/2018_04_01_180916_create_category_contents_table.php` e
alterada pelas migrations listadas na seção 6.

Dump real (`prefeituras_v5`):

| Coluna | Tipo | Null | Default | Papel |
|---|---|---|---|---|
| `id` | `integer` (serial) | não | `nextval('category_contents_id_seq')` | PK |
| `name` | `varchar(191)` | não | — | Nome exibido |
| `slug` | `varchar(191)` | não | — | Identificador da URL (`/portal/<slug>`) |
| `status_create` | `boolean` | sim | — | **`0` = criada pelo operador (Mais Conteúdos)**; `1` = categoria de sistema |
| `quantidade_itens` | `integer` | sim | — | Quantidade de itens exibidos no portal (notícias) |
| `order` | `integer` | sim | — | Posição na listagem |
| `layout` | `integer` | sim | — | Layout de exibição (usado por notícias) |
| `created_at` | `timestamp(0)` | sim | — | |
| `updated_at` | `timestamp(0)` | sim | — | |
| `deleted_at` | `timestamp(0)` | sim | — | Soft delete (`SoftDeletes` no model) |
| `category_contents_id` | `integer` | sim | — | **Auto-FK**: id do pai. `NULL` = categoria de topo |
| `status` | `boolean` | sim | `true` | Visibilidade no portal |
| `display_type` | `varchar(191)` | sim | — | `grouped` \| `direct` (usado em Transparência) |
| `custom_icon` | `varchar(191)` | sim | — | Ícone customizado (upload em `public/uploads/icons`) |
| `description` | `text` | sim | — | Descrição (usada por `transparencia-fiscal`) |

Índices e constraints:

```sql
Indexes:
    "category_contents_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
    "category_contents_category_contents_id_foreign" FOREIGN KEY (category_contents_id) REFERENCES category_contents(id)
Referenced by:
    TABLE "contents"        CONSTRAINT "contents_category_contents_id_foreign"
    TABLE "contests"        CONSTRAINT "contests_category_contents_id_foreign"
    TABLE "menu_items"      CONSTRAINT "menu_items_category_contents_id_foreign"
```

## 2. Semântica de `status_create`

| Valor | Significado | Exemplos |
|---|---|---|
| `1` (true) | Categoria de **sistema**, criada por seed/migration | `noticias`, `legislacao`, `publicacoes`, `banners`, `super-banners`, `orgaos`… (33 linhas) |
| `0` (false) | Categoria criada pelo **operador** — é o "Mais Conteúdos" | (nenhuma no ambiente atual) |
| `NULL` | Subcategorias de sistema | 12 linhas (filhas de `publicacoes`, `legislacao`, etc.) |

Distribuição real na base:

```sql
select status_create, count(*) from category_contents group by 1;
 status_create | count
---------------+-------
               |    12
 t             |    33
```

> **Nenhuma categoria com `status_create = false` existe na base `prefeituras_v5`** —
> ou seja, o submenu "Mais Conteúdos" do CMS está vazio nesse ambiente. É o recorte
> esperado: 45 categorias, todas de sistema ou subcategorias.

> `where('status_create', 0)` numa coluna boolean funciona porque o PDO envia o
> parâmetro em formato texto e o PostgreSQL faz a coerção para boolean na entrada
> (`'0'` → `false`). Com literais inteiros em SQL puro a comparação
> `boolean = integer` **não** existe no PostgreSQL.

## 3. O recorte "Mais Conteúdos"

```php
CategoryContent::where('status_create', 0)->whereNull('category_contents_id')->get();
```

| Eixo | Condição | Consequência |
|---|---|---|
| Origem | `status_create = 0` | Só categorias criadas no CMS |
| Hierarquia | `category_contents_id IS NULL` | Só as de topo; as subcategorias são filhas |

As subcategorias **não** aparecem no menu, mas são usadas como destino de conteúdo
(ContentController lista `status_create = false` **com** pai entre os ids das de topo).

## 4. Model `CategoryContent`

Arquivo: `v6/app/Http/Model/Prefecture/CategoryContent.php`

```php
class CategoryContent extends Model
{
    use SoftDeletes;
    use PresentableTrait;

    protected $table = 'category_contents';

    protected $fillable = [
        'name', 'description', 'category_contents_id', 'status_create',
        'order', 'quantidade_itens', 'slug', 'layout', 'display_type', 'custom_icon',
    ];

    protected $presenter = CategoryContentPresenter::class;
```

Relações:

| Relação | Tipo | Uso |
|---|---|---|
| `categoryContents()` | `hasMany(CategoryContent, 'id', 'category_contents_id')` | Relação invertida (chamada em `whereHas('categoryContents', ...)`) |
| `rightWayToGetCategoryContents()` | `hasMany(CategoryContent, 'category_contents_id', 'id')` | Filhos diretos (usada na view `recursive-list`) |
| `categoryContentsBelongings()` | `belongsTo(CategoryContent, 'category_contents_id', 'id')` | Pai |
| `contents()` | `hasMany(Content)` | Conteúdos (filtrados por status/publicação) |
| `categories()` | `hasMany(CategoryContent, 'category_contents_id', 'id')` | Filhos ordenados por nome |

Scopes:

| Scope | Comportamento |
|---|---|
| `byCategoryNews($category, $parentCategory, $categoryHasContent)` | Filtro de categoria para as notícias |
| `byCategoryHasContent()` | Só categorias que possuem conteúdo |
| `bySlug($slug)` | `where('slug', $slug)` |
| `bySlugBanner()` | `whereIn('slug', ['banners'])` (usado pelo `BannerServiceProvider`) |
| `allDescendants()` | Carrega recursivamente os descendentes |

Métodos notáveis:

| Método | Comportamento |
|---|---|
| `getAllDescendants($withParents = false)` | Achata a árvore; com `$withParents = true`, prefixa o nome do pai (exceto `publicacoes` e `legislacao`) |
| `rightWayToGetCategoryContents` | Relação usada como coleção na recursão |

> `getAllDescendants` **não** detecta ciclos: uma categoria apontando para si (ou um
> ciclo A→B→A) causaria recursão infinita.

## 5. Tabelas relacionadas

| Tabela | FK | Papel |
|---|---|---|
| `contents` | `category_contents_id` → `category_contents.id` | Conteúdos publicados nas categorias |
| `menu_items` | `category_contents_id` → `category_contents.id` | Itens de menu do portal |
| `contests` | `category_contents_id` | (módulo de[editais/concursos]) |

## 6. Migrations

| Migration | Efeito |
|---|---|
| `v6/database/migrations/2018_04_01_180916_create_category_contents_table.php` | Cria `category_contents` |
| `v6/database/migrations/2021_02_18_123218_add_status_to_category_contents.php` | Adiciona a coluna `status` (default `true`) |
| `v6/database/migrations/2026_06_01_100000_add_display_type_and_custom_icon_to_category_contents.php` | Adiciona `display_type` e `custom_icon` |
| `v6/database/migrations/2026_06_04_100000_add_description_to_category_contents_table.php` | Adiciona `description` (text) |

Conteúdo das duas últimas (reais):

```php
// 2026_06_01_100000_add_display_type_and_custom_icon_to_category_contents.php
Schema::table('category_contents', function (Blueprint $table) {
    $table->string('display_type')->nullable()->after('layout');
    $table->string('custom_icon')->nullable()->after('display_type');
});
```

> A migration `2026_06_04_100000_add_description_to_category_contents_table.php`
> **não existe na v5** — ver `08-GAPS-E-DIVIDAS.md`.

## 7. Views SQL

O módulo **não** possui view SQL própria. Views do platform que expõem
`category_contents` indiretamente (todas em `v6/database/migrations/`):

| View | Origem |
|---|---|
| `banners` | `contents` JOIN `category_contents` (slug `banners`) |
| `super_banners` | `contents` JOIN `category_contents` (slug `super-banners`) |
| `noticias`, `glossario`, `rede_social`, `telefones_uteis`, `paginas_estaticas` | idem, para outras categorias |

## 8. Seeds

| Seed | Efeito |
|---|---|
| `v6/database/seeds/ResourcesTableSeeder.php:651` | Cria o resource `conteudo` com a descrição "Funcionalidade de Mais Conteudos" e `modulo_id = 54` |
| `v6/database/seeds/AjustesMigracao.php:203-207` | Garante a existência do módulo `mais-conteudos` na tabela `modulos` |
| `v6/database/seeds/UpdateManualAreas.php:307-334` | Cria o "Manual de Mais Conteúdos" e as áreas "Cadastrar Categoria de Conteúdo", "Cadastrar Subcategoria" e "Cadastrar Conteúdo" |
| `v6/database/seeds/ManuaisSinergiaSeeder.php:41` | Registra o manual "Manual de Mais Conteúdos" |

Trecho real do seed de resources:

```php
Resource::updateOrCreate(['slug' => 'conteudo'],
    [
        'name' => 'Conteúdos',
        'description' => 'Funcionalidade de Mais Conteudos',
        'modulo_id' => 54,
        'created_at' => date("Y-m-d H:i:s")
    ]
);
```

Trecho real do `AjustesMigracao.php`:

```php
$modulo = Modulos::where('slug', 'mais-conteudos')->first();
if (!$modulo) {
    DB::table('modulos')->insert([
        'name' => 'Mais Conteudos',
        'slug' => 'mais-conteudos',
        'status' => 1,
    ]);
}
```

## 9. Registro do módulo

| Tabela | Registro |
|---|---|
| `modulos` | `id = 54`, `name = 'Mais Conteudos'`, `slug = 'mais-conteudos'`, `status = 1` |
| `menu_orders` | `id = 42`, `slug = 'categoria-conteudo'`, `modulo_id = 54` |
| `resources` | `id = 73` (`categoria-conteudo`, "Categorias de Conteúdos") e `id = 74` (`conteudo`, "Conteúdos"), ambos `modulo_id = 54` |

> O nome do módulo no registry é "Mais Conteudos" (sem acento) enquanto o menu usa
> "Mais Conteúdos" (com acento) — divergência de rotulagem, não de slug.

## 10. Exemplo de estrutura (hipotético)

Uma categoria "Mais Conteúdos" típica, com suas subcategorias:

```
category_contents
├── id=100  "Aluguéis"      slug=alugueis    status_create=false  parent=NULL   ← aparece no menu
├── id=101  "Residenciais"  slug=residenciais status_create=false parent=100
└── id=102  "Comerciais"    slug=comerciais   status_create=false parent=100
```

O menu do CMS listaria apenas `id=100`; `id=101` e `id=102` apareceriam como opções de
categoria pai e no select do formulário de conteúdo.
