# TV Câmara — Modelagem de Dados

> **Escopo:** módulo `tv-camara` na `v6`. Todas as tabelas e views citadas
> foram verificadas no banco `prefeituras_v5` (PostgreSQL 10) e nos arquivos
> de migration do repositório.

---

## 1. Tabela `tv_camaras`

Criada por `v6/database/migrations/2024_11_11_164932_create_tv_camara_table.php`
(classe `CreateTvCamaraTable`).

```php
Schema::create('tv_camaras', function (Blueprint $table) {
    $table->increments('id');
    $table->string('title');
    $table->text('text')->nullable();
    $table->string('slug');
    $table->boolean('status')->nullable();
    $table->text('link');
    $table->dateTime('publication_date')->default(date('Y/m/d H:i'));
    $table->timestamps();
    $table->softDeletes();
});
```

### 1.1 Colunas (estado real no banco)

| Coluna | Tipo (migration) | Tipo (banco) | Null | Default | Observação |
|---|---|---|---|---|---|
| `id` | `increments` | `integer` | não | `nextval('tv_camaras_id_seq')` | PK. |
| `title` | `string` | `character varying(191)` | não | — | Título do vídeo. |
| `text` | `text` nullable | `text` | sim | — | Descrição; renderizada com `{!! !!}`. |
| `slug` | `string` | `character varying(191)` | não | — | Gerado por `str_slug($title)`; **não há índice único**. |
| `status` | `boolean` nullable | `boolean` | sim | — | `true` = ativo/exibido; `null` equivale a desativado. |
| `link` | `text` | `text` | não | — | URL do vídeo (entrada do `embedVideo()`). |
| `publication_date` | `dateTime` | `timestamp(0) without time zone` | não | `'2026-10-21 08:52:00'` (literal) | Ver armadilha em `05-REGRAS-E-PORTABILIDADE.md`. |
| `created_at` | `timestamps` | `timestamp(0) without time zone` | sim | `NULL` | Usado pelo `row_number()` da view. |
| `updated_at` | `timestamps` | `timestamp(0) without time zone` | sim | `NULL` | — |
| `deleted_at` | `softDeletes` | `timestamp(0) without time zone` | sim | `NULL` | Soft delete. |

Índices: apenas `tv_camaras_pkey` (btree em `id`). **Não há índice em `slug`**,
o que torna a busca da página de detalhes uma varredura sequencial.

### 1.2 O `down()` quebrado

```php
public function down()
{
    Schema::dropIfExists('tv_camara');   // <- nome errado: a tabela é 'tv_camaras'
}
```

O `rollback` da migration não remove a tabela (o nome real nunca é.dropado).
Detalhe em `08-GAPS-E-DIVIDAS.md`.

## 2. Tabela `archives` (coluna adicionada)

A coluna de ligação foi criada por
`v6/database/migrations/2024_11_16_121841_add_tv_camaras_id_in_archives.php`:

```php
Schema::table('archives', function (Blueprint $table) {
    $table->unsignedBigInteger('tv_camara_id')->nullable()->after('id');
    $table->foreign('tv_camara_id')->references('id')->on('tv_camaras')->onDelete('cascade');
});
```

| Coluna relevante | Tipo | Null | Default | Observação |
|---|---|---|---|---|
| `id` | `integer` | não | — | PK da galeria genérica. |
| `tv_camara_id` | `bigint` | sim | `NULL` | FK → `tv_camaras.id`, `ON DELETE CASCADE`. |
| `contents_id` | `integer` | sim | `NULL` | NULL para arquivos de TV Câmara (o módulo não usa `contents`). |
| `path` | `character varying` | sim | `NULL` | `storage/tv-camara/{id}/destaque/{nome}` no CMS. |
| `name` | `character varying` | sim | `NULL` | `img_{YmdHi}{4 aleatórios}.{ext}`. |
| `extension` | `character varying` | sim | `NULL` | Extensão do arquivo. |
| `type` | `character varying` | sim | `NULL` | MIME type do upload. |
| `highlight` | `boolean` | sim | `NULL` | `1`/`true` marca a imagem de destaque. |

Restrição no banco: `archives_tv_camara_id_foreign FOREIGN KEY (tv_camara_id)
REFERENCES tv_camaras(id) ON DELETE CASCADE`.

O `down()` dessa migration remove corretamente a FK e a coluna.

## 3. View SQL `tv_camara`

Criada por `v6/database/migrations/2024_11_11_190611_create_tv_camara_view.php`
(classe `CreateTvCamaraView`).

```sql
CREATE OR REPLACE VIEW tv_camara AS
SELECT
    row_number() OVER (ORDER BY tc.created_at DESC) AS value,
    tc.title AS titulo,
    tc.id AS id,
    to_char(tc.publication_date::timestamp with time zone, 'DD/MM/YYYY'::text) AS data_publicacao,
    CASE
    WHEN tc.status is true  THEN 'Ativado'
    ELSE 'Desativado'
END AS status_name,
    tc.status as status
FROM
    tv_camaras tc
WHERE
    tc.deleted_at IS NULL
ORDER BY
    tc.created_at DESC
```

| Coluna da view | Tipo | Origem | Uso no CMS |
|---|---|---|---|
| `value` | `bigint` | `row_number()` por `created_at DESC` | Número da linha no DataTable (coluna injetada por `runDatatables`). |
| `titulo` | `character varying(191)` | `tc.title` | Coluna "Título". |
| `id` | `integer` | `tc.id` | Identificador das ações. |
| `data_publicacao` | `text` | `to_char(...,'DD/MM/YYYY')` | Coluna "Data" (só data, sem hora). |
| `status_name` | `text` | `CASE` sobre `tc.status` | Coluna "Status" ("Ativado"/"Desativado"). |
| `status` | `boolean` | `tc.status` | Alimenta o botão de toggle. |

Propósito: servir de projeção estável para o DataTable do CMS
(`DB::table("tv_camara")->get()` em
`v6/app/Http/Controllers/Prefecture/DatatableController.php:469-473`), já que a
tabela `tv_camaras` não é consultada diretamente pelo CMS.

Pontos de atenção:

- A view **não** filtra por `status`; a listagem do CMS mostra ativados e
  desativados.
- A view **não** ordena por `publication_date`, e sim por `created_at`.
- `to_char` com `::timestamp with time zone` sobre uma coluna
  `timestamp without time zone` usa o fuso do servidor do banco; em UTC
  (padrão do container) a data exibida pode estar deslocada em relação ao
  horário do PHP.
- O `down()` faz `DROP VIEW if exists public.tv_camara` — correto.

## 4. Módulos, recursos e menu

| Tabela | Colunas usadas | Valor para `tv-camara` | Origem |
|---|---|---|---|
| `modulos` | `id`, `name`, `slug`, `status` | 45, "TV Câmara", `false` | `v6/database/migrations/2024_11_01_150206_create_modulos_table.php:293-297` |
| `resources` | `id`, `name`, `slug`, `modulo_id` | 64, "TV Câmara", `modulo_id = 45` | `v6/database/seeds/ResourcesTableSeeder.php:558-564` |
| `menu_orders` | `id`, `slug`, `modulo_id`, `clicks` | 39, `modulo_id = 45`, `clicks = 0` | `v6/database/seeds/MenuOrderSeeder.php:307-312` |

O `resources` é a base das permissões: a tela de perfis monta a matriz
`<resource.slug>.<permission.slug>`
(`v6/resources/views/admin/role/form.blade.php:55`), ou seja
`tv-camara.index`, `tv-camara.create`, `tv-camara.edit`, `tv-camara.delete`,
`tv-camara.inactivate`, `tv-camara.restore`, `tv-camara.show`,
`tv-camara.publish`.

## 5. Relacionamentos

```
tv_camaras (1) ──────< (N) archives   via archives.tv_camara_id  (ON DELETE CASCADE)

App\Prefecture\TvCamara
   └── archives(): HasMany(Archive::class, 'tv_camara_id', 'id')
```

```php
public function archives()
{
    return $this->hasMany(Archive::class, 'tv_camara_id', 'id');
}
```

Observações:

- A relação inversa (`Archive::tvCamara()`) **não existe** no model `Archive`;
  só há `belongsTo(Content::class)`, `belongsTo(StaticPage::class)` e demais.
- O model `TvCamara` **não** define `linksUnique()`, embora o
  `ContentPresenter` acesse `$this->linksUnique->link` — o operador `??`
  neutraliza o acesso nulo (`v6/app/Http/Presenters/ContentPresenter.php:96`).
- Não há FK entre `tv_camaras` e `contents`, `category_contents` ou
  `sessoes`: o módulo é **independente** do conteúdo genérico do portal.

## 6. Diagrama relacional (ASCII)

```
                    ┌──────────────────────────────┐
                    │        tv_camaras            │
                    ├──────────────────────────────┤
                    │ id           PK  integer     │
                    │ title           varchar(191) │
                    │ text            text  (null) │
                    │ slug            varchar(191) │
                    │ status          boolean(null)│
                    │ link            text         │
                    │ publication_date timestamp   │
                    │ created_at / updated_at      │
                    │ deleted_at      timestamp(null)│
                    └───────────┬──────────────────┘
                                │ 1
                                │
                                │ N
                    ┌───────────▼──────────────────┐
                    │         archives               │
                    ├───────────────────────────────┤
                    │ id              PK integer    │
                    │ tv_camara_id  FK  bigint(null)│───► tv_camaras.id
                    │ contents_id        integer    │      ON DELETE CASCADE
                    │ path / name / extension / type │
                    │ highlight         boolean      │  (1 = imagem de destaque)
                    │ deleted_at        timestamp    │
                    └────────────────────────────────┘

                    ┌────────────────────────────────┐
                    │  view tv_camara (read-only)     │
                    │  value | titulo | id |          │
                    │  data_publicacao | status_name  │
                    │  status                          │
                    │  FROM tv_camaras WHERE           │
                    │       deleted_at IS NULL        │
                    └────────────────────────────────┘
```

## 7. Seeds

| Arquivo | O que popula |
|---|---|
| `v6/database/migrations/2024_11_01_150206_create_modulos_table.php` | linha do módulo 45 (`status = 0`). |
| `v6/database/seeds/ResourcesTableSeeder.php` | recurso `tv-camara` (id 64) ligado ao módulo 45. |
| `v6/database/seeds/MenuOrderSeeder.php` | item de menu `tv-camara` (id 39, `clicks = 0`). |

**Não há seed de conteúdo**: a tabela `tv_camaras` nasce vazia, e o módulo só
aparece no portal depois de habilitado em `/cms/modulos`.

## 8. Ausências notáveis no schema

| Ausência | Consequência |
|---|---|
| Sem índice/unique em `slug` | Títulos repetidos geram slugs iguais; o portal resolve pelo primeiro `firstOrFail()`. |
| Sem índice em `publication_date` / `status` | Ordenação e filtros fazem varredura sequencial. |
| Sem `provider` / `video_id` | O provedor só é deduzido em tempo de renderização por `embedVideo()`. |
| Sem `author` / `source` | As views do portal testam `$content->source` e `$content->author`, que **não existem** na tabela; o bloco "Do(a) ... por ..." nunca é renderizado. |
| Sem `views`/contador de acesso | Não há métrica de audiência por vídeo. |
