# Transmissões — Modelagem de Dados

> Versão analisada: `v6`. O módulo **não cria nenhuma tabela própria**.

## 1. Visão geral

O módulo `transmissoes` é um **módulo de conteúdo genérico**. Não existe
`transmissions`, `transmissoes` (tabela) nem qualquer tabela dedicada: o
armazenamento é feito em três tabelas genéricas já existentes na plataforma.

```
 category_contents (id=36, slug='transmissoes')
            |
            |  contents.category_contents_id  (FK)
            v
      contents  --(links.contents_id FK)-->  links
      (title, status, order)                   (link = URL do vídeo)
```

## 2. Tabelas utilizadas

### 2.1 `category_contents`

| Coluna | Tipo | Null | Default | Observação |
|---|---|---|---|---|
| `id` | serial (increments) | não | — | PK. `transmissoes` = **id 36** no banco. |
| `name` | varchar | não | — | `'Transmissões'`. |
| `slug` | varchar | não | — | `'transmissoes'` — chave usada por todas as consultas. |
| `status_create` | boolean | sim | — | `1` no seed. |
| `quantidade_itens` | integer | sim | — | — |
| `order` | integer | sim | — | — |
| `layout` | integer | sim | — | — |
| `category_contents_id` | integer (unsigned) | sim | — | Auto-referência (árvore); `transmissoes` é raiz (`NULL`). |
| `created_at` / `updated_at` | timestamp | sim | — | — |
| `deleted_at` | timestamp | sim | — | Soft delete. |

Migration: `v6/database/migrations/2018_04_01_180916_create_category_contents_table.php`.
Colunas extras: `2021_02_18_123218_add_status_to_category_contents.php`,
`2026_06_01_100000_add_display_type_and_custom_icon_to_category_contents.php`,
`2026_06_04_100000_add_description_to_category_contents_table.php`.

Seed: `v6/database/seeds/CategoryContentTableSeeder.php:201-205`

```php
        // ID 36
        CategoryContent::updateOrCreate(['slug' => 'transmissoes'], [
            'name' => 'Transmissões',
            'status_create' => 1
        ]);
```

### 2.2 `contents`

| Coluna | Tipo | Null | Default | Uso no módulo |
|---|---|---|---|---|
| `id` | serial | não | — | PK; exposto na view SQL. |
| `title` | varchar | sim | — | Título da transmissão (regra: `required|max:120`). |
| `text` | text | sim | — | Não usado pelo formulário do módulo. |
| `status` | boolean | sim | — | `true` = transmissão ativa. |
| `order` | integer | sim | — | Sempre `NULL` neste módulo (o formulário não tem o campo). |
| `slug` | varchar | sim | — | Gerado a partir do título. |
| `category_contents_id` | integer (unsigned) | sim (FK) | — | Aponta para `category_contents.id = 36`. |
| `clicks`, `highlights`, `galery` | integer/boolean | sim | — | Não usados. |
| `publication_date` | datetime | sim | — | Não usado. |
| `author`, `source`, `icon` | varchar | sim | — | Não usados. |
| `linha_fina`, `referencia`, `carousel`, `layout`, `position_id`, `orgaos_id`, `councilmen_id`, `secretaria` | — | sim | — | Existentes na tabela, irrelevantes para transmissões. |
| `created_at` / `updated_at` | timestamp | sim | — | `created_at` é usado pela view SQL (`row_number()`). |
| `deleted_at` | timestamp | sim | — | Filtrado pela view SQL. |

Migration base: `v6/database/migrations/2018_04_02_180433_create_contents_table.php`.
O model `App\Prefecture\Content` (`v6/app/Http/Model/Prefecture/Content.php`)
declara `protected $table = 'contents'`, usa `SoftDeletes` e tem `$fillable` com
`title`, `status`, `slug`, `order`, `category_contents_id`, entre outros.

### 2.3 `links`

| Coluna | Tipo | Null | Default | Uso no módulo |
|---|---|---|---|---|
| `id` | serial | não | — | PK. |
| `contents_id` | integer (unsigned) | sim (FK → `contents.id`) | — | Liga o link ao conteúdo. |
| `link` | varchar | sim | — | **URL do vídeo** (ou ID cru, já normalizado). |
| `title` | varchar | sim | — | Copia o `title` do conteúdo. |
| `name` | varchar | sim | — | Copia o `title` do conteúdo. |
| `alt` | varchar | sim | — | Copia o `title` do conteúdo. |
| `isBlank` | boolean | sim | — | `1` se marcado no formulário (o formulário atual não expõe o campo). |
| `static_pages_id` | integer (unsigned) | sim (FK) | — | Não usado. |
| `status_create` | boolean | sim | — | Não usado. |
| `created_at` / `updated_at` / `deleted_at` | timestamp | sim | — | Soft delete presente na migration, mas **descomentado** no model `Link`. |

Migrations: `v6/database/migrations/2018_04_02_181150_create_links_table.php` e
`v6/database/migrations/2019_09_10_161305_alter_link_to_links_table.php`.

### 2.4 `modulos` (flag do módulo)

| Coluna | Valor para o módulo |
|---|---|
| `id` | `44` |
| `name` | `Transmissões` |
| `slug` | `transmissoes` |
| `status` | `0` (desligado no seed; ligado manualmente em `/cms/modulos`) |

Migration: `v6/database/migrations/2024_11_01_150206_create_modulos_table.php:288-292`.
O `status` é lido por `v6/app/Providers/ModulosServiceProvider.php` via
`Cache::rememberForever('modulos', ...)` e compartilhado com as views.

### 2.5 `menu_orders` (telemetria)

| Campo | Valor |
|---|---|
| `slug` | `transmissoes` |
| `modulo_id` | `44` |
| `clicks` | incrementado a cada `menuClicks('transmissoes')` |

Migration: `v6/database/migrations/2022_04_08_101047_create_menu_order_table.php`
e `v6/database/migrations/2024_11_04_094157_add_modulo_id_to_menu_order_table.php`.
Seed: `v6/database/seeds/MenuOrderSeeder.php:267-273`.

### 2.6 `resources` (permissões)

| Campo | Valor |
|---|---|
| `slug` | `transmissoes` |
| `name` | `Transmissões` |
| `modulo_id` | `44` |

Seed: `v6/database/seeds/ResourcesTableSeeder.php:549-556`.

## 3. View SQL `transmissoes`

Criada em `v6/database/migrations/2024_10_01_180234_create_view_transmissoes.php`
(classe `CreateViewTransmissoes`), que executa `CREATE OR REPLACE VIEW`:

```sql
CREATE OR REPLACE VIEW transmissoes AS
SELECT row_number() OVER (ORDER BY c.created_at DESC) AS value,
c.title AS titulo,
CASE
   WHEN c.status is true THEN 'Ativado'
   ELSE 'Desativado'
END AS status_name_transmissao,
CASE
    WHEN c.status IS TRUE THEN true
    ELSE false
END AS status,
c.id
   FROM contents c
  WHERE c.deleted_at IS NULL AND c.category_contents_id = (
        SELECT id FROM category_contents WHERE slug = 'transmissoes'
  );
```

### 3.1 Colunas da view (verificado com `\d transmissoes`)

| Coluna | Tipo PostgreSQL | Papel |
|---|---|---|
| `value` | `bigint` | Número da linha (`row_number()` por `created_at DESC`); usado pelo JS como coluna "ID". |
| `titulo` | `character varying(191)` | `contents.title`. |
| `status_name_transmissao` | `text` | `'Ativado'` / `'Desativado'`. |
| `status` | `boolean` | Situação lógica; usada pelo toggle. |
| `id` | `integer` | `contents.id`. |

### 3.2 O que a view abre e o que ela esconde

- **Abre:** a listagem do CMS com status derivado e numeração de linha, sem
  precisar de `with()` de relações ou de tratamento de `SoftDeletes` no Eloquent.
- **Esconde:** o `links.link` (o URL do vídeo). A view **não expõe** o link, o
  que impede a edição inline pela DataTable.
- **Filtra:** `deleted_at IS NULL` (soft delete) e a categoria via subselect de
  `category_contents.slug = 'transmissoes'`.
- **Risco:** o subselect é escalar; se houver **duas** linhas em
  `category_contents` com o mesmo slug, o PostgreSQL lança
  `more than one row returned by a subquery used as an expression`.

### 3.3 Consumo

`v6/app/Http/Controllers/Prefecture/DatatableController.php:51-55`

```php
    public function get_transmissoes()
    {
        $transmissoes = DB::table('transmissoes')->get();
        return DataTables::of($this->struct_views($transmissoes, 'transmissoes'))->make(true);
    }
```

## 4. Relacionamentos Eloquent

| Relação | Model | Tipo | Código |
|---|---|---|---|
| `categoryContents()` | `Content` | `belongsTo(CategoryContent::class, 'category_contents_id', 'id')` | `v6/app/Http/Model/Prefecture/Content.php` |
| `linksUnique()` | `Content` | `hasOne(Link::class, 'contents_id', 'id')` | idem |
| `contents()` | `CategoryContent` | `hasMany(Content::class, 'category_contents_id', 'id')` | `v6/app/Http/Model/Prefecture/CategoryContent.php` |
| `contents()` | `Link` | `belongsTo(Content::class, 'contents_id', 'id')` | `v6/app/Http/Model/Prefecture/Link.php` |

**Atenção:** `linksUnique()` é `hasOne` e **pode ser nulo**. O widget acessa
`$transmissao->linksUnique->link` **sem verificação de nulo** — se o conteúdo
não tiver `Link`, isso gera erro em tempo de execução.

## 5. Diagrama ASCII

```
+---------------------------+
|      modulos (44)         |  status = 0 no seed
+-------------+-------------+
              |
              | (flag lida por ModulosServiceProvider -> $modulos)
              v
+---------------------------+
|   category_contents (36)  |  slug = 'transmissoes'
+-------------+-------------+
              |
              | contents.category_contents_id
              v
+---------------------------+        +---------------------------+
|        contents           |        |          links            |
|---------------------------|        |---------------------------|
| id            PK          |<------| id            PK          |
| title        varchar     |  FK   | contents_id  int (FK)    |
| status       boolean     |        | link         varchar     |
| order        int (NULL)  |        | title/alt/name varchar   |
| slug         varchar     |        | isBlank      boolean     |
| category_contents_id FK  |        +---------------------------+
| created_at   timestamp   |
| updated_at   timestamp   |
| deleted_at   timestamp   |  <- filtrado pela view SQL
+---------------------------+
              |
              | consumed by
              v
+---------------------------+
|   VIEW transmissoes      |
|---------------------------|
| value              bigint |
| titulo             v191  |
| status_name_transmissao  |
| status             bool  |
| id                 int   |
+---------------------------+
```

## 6. Fluxo de escrita (cadastro)

1. `Content::create($contentData)` com `category_contents_id = 36`,
   `title`, `status` e `order = NULL` (o formulário não envia `order`).
2. Desativação dos demais ativos da categoria (`ContentController.php:678-680`).
3. `Link::create()` com `link = sanitizeLink(normalizeVideoLink($request->link))`
   (`ContentController.php:715-728`).

> `normalizeVideoLink()` (`v6/app/Helpers/functions.php:672-690`) adiciona
> `https://` quando o valor não começa com `http(s)://`.
> `sanitizeLink()` (`v6/app/Http/Controllers/Prefecture/ContentController.php:595-608`)
> converte `https://<algo-sem-ponto>` em `/<algo-sem-ponto>`.
> Ver `08-GAPS-E-DIVIDAS.md` para o impacto disso sobre o ID cru do YouTube.
