# Sessões da Câmara Municipal — Modelagem de Dados

> Módulo `sessoes` na `v6` (PostgreSQL 10).
> O módulo **não possui tabela própria**: reaproveita `contents`,
> `category_contents`, `archives` e `links`, e acrescenta a **view SQL**
> `sessoes`.

---

## 1. Visão geral do esquema

```
                    category_contents (árvore)
                    ┌──────────────────────────────────────────┐
                    │ id=35  name='Sessões'  slug='sessoes'    │
                    │        category_contents_id = NULL  (raiz) │
                    └───────────────────┬──────────────────────┘
                                        │ 1:N  (contents.category_contents_id)
                                        ▼
  contents  ─────────────────────────────────────────────────────────────
  ┌───────────────────────────────────────────────────────────────────────┐
  │ id            PK                                                      │
  │ title         varchar(191)   obrigatório, máx. 150 no formulário     │
  │ text          text            obrigatório (HTML do Summernote)          │
  │ slug          varchar(191)   gerado do título (Str::slug)              │
  │ publication_date timestamp(0)  obrigatório; controla a visibilidade    │
  │ status        boolean         true = publicado                         │
  │ galery        boolean         true = exibe a galeria de fotos          │
  │ clicks        integer         contador de acessos                      │
  │ highlights    boolean         (flag genérica de destaque)               │
  │ order         integer         ordenação genérica                       │
  │ created_at / updated_at / deleted_at  timestamp(0)  (SoftDeletes)     │
  └──────────┬──────────────────────────────┬──────────────────────────────┘
             │ 1:N                          │ 1:N
             ▼                              ▼
  archives                          links
  ┌────────────────────────────┐     ┌──────────────────────────────┐
  │ id                PK        │     │ id                PK          │
  │ contents_id       FK        │     │ contents_id       FK          │
  │ highlight         boolean   │     │ link              varchar(3000)│
  │   1  = imagem de destaque  │     │ title / name / alt varchar(191)│
  │   NULL = foto da galeria    │     │ isBlank           boolean       │
  │ path / name / extension     │     │ created_at / updated_at /      │
  │ order / view_pdf / type     │     │   deleted_at                   │
  └────────────────────────────┘     └──────────────────────────────┘

  VIEW sessoes  (read-only, criada por migration)
  ┌───────────────────────────────────────────────────────────────────────┐
  │ value (bigint) | titulo | status_name_sessoes | data_publicacao |    │
  │ id | galeria | slug                                                     │
  └───────────────────────────────────────────────────────────────────────┘
```

## 2. A view SQL `sessoes`

**Migration:** `v6/database/migrations/2024_09_30_004936_create_view_sessoes.php`
(classe `CreateViewSessoes`, data 2024-09-30).
**Consumidores:** `v6/app/Http/Controllers/Prefecture/DatatableController.php:28-32`.

SQL real da migration:

```sql
CREATE OR REPLACE VIEW sessoes AS
SELECT row_number() OVER (ORDER BY c.publication_date DESC) AS value,
c.title AS titulo,
CASE
   WHEN c.status is true  THEN 'Publicado'
   ELSE 'Rascunho'
END AS status_name_sessoes,
to_char(c.publication_date::timestamp with time zone, 'DD/MM/YYYY às HH24:MI'::text) AS data_publicacao,
c.id,
c.galery as galeria,
c.slug as slug
FROM contents c
WHERE c.deleted_at IS NULL AND c.category_contents_id = (
        SELECT id FROM category_contents WHERE slug = 'sessoes' AND category_contents_id IS NULL
);
```

### 2.1 Colunas da view (verificadas com `psql -c "\d sessoes"`)

| Coluna | Tipo PostgreSQL | Origem | Uso |
|---|---|---|---|
| `value` | `bigint` | `row_number() OVER (ORDER BY c.publication_date DESC)` | Número da linha, exibido como coluna "ID" na tabela do CMS (`v6/public/assets/js/custom/contents/sessoes/datatables.js`). |
| `titulo` | `character varying(191)` | `contents.title` | Coluna "Título". |
| `status_name_sessoes` | `text` | `CASE` sobre `contents.status` | `Publicado` ou `Rascunho`. |
| `data_publicacao` | `text` | `to_char(publication_date, 'DD/MM/YYYY às HH24:MI')` | Coluna "Data de publicação" já formatada em pt-BR. |
| `id` | `integer` | `contents.id` | Chave para editar/excluir. |
| `galeria` | `boolean` | `contents.galery` | Habilita/desabilita o botão de galeria na listagem. |
| `slug` | `character varying(191)` | `contents.slug` | Monta a URL de compartilhamento no Facebook. |

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

| A view **inclui** | A view **exclui** |
|---|---|
| Itens da **categoria raiz** `sessoes` (subcategorias não entram). | Itens com `deleted_at IS NULL` (excluídos logicamente). |
| Rascunhos (`status = false`), para o operador. | Conteúdos de outras categorias. |
| Itens com data futura (o CMS precisa enxergar o que está agendado). | — |

Consequências práticas:

- **A view não é a fonte de verdade** — `contents` é. Qualquer mudança no
  escopo da view exige reexecutar a migration (`DROP VIEW` + `CREATE OR REPLACE`).
- **O filtro de publicação (`status` e `publication_date <= now`) existe
  apenas no portal**, não na view nem no CMS.
- **O `row_number()` reordena a cada consulta** e não é estável entre
  chamadas nem paginado — a listagem do CMS carrega a view inteira em memória.

## 3. Tabela `contents`

Origem: tabela genérica do portal. Colunas usadas por este módulo (verificadas
com `psql -c "\d contents"`):

| Coluna | Tipo | Null | Default | Papel no módulo `sessoes` |
|---|---|---|---|---|
| `id` | `integer` | não | `nextval('contents_id_seq')` | PK; usado por `archives.contents_id`, `links.contents_id` e pelas ações do CMS. |
| `title` | `varchar(191)` | sim | — | Título da sessão. |
| `text` | `text` | sim | — | Texto rico (HTML Summernote). |
| `slug` | `varchar(191)` | sim | — | Identificador da URL `/sessoes/{slug}`. |
| `publication_date` | `timestamp(0)` | sim | — | Data/hora da sessão; define a data de exibição. |
| `status` | `boolean` | sim | — | `true` = publicado (visível no portal). |
| `galery` | `boolean` | sim | — | `true` = exibe a galeria de fotos. |
| `clicks` | `integer` | sim | — | Contador genérico de cliques. |
| `highlights` | `boolean` | sim | — | Flag genérica (não usada pelo módulo de sessões). |
| `order` | `integer` | sim | — | Ordenação genérica. |
| `category_contents_id` | `integer` | **não** | — | FK para `category_contents(id)`; aponta para a raiz `sessoes`. |
| `created_at` | `timestamp(0)` | sim | — | Não usado na ordenação da view. |
| `updated_at` | `timestamp(0)` | sim | — | Não usado na ordenação da view. |
| `deleted_at` | `timestamp(0)` | sim | — | Soft delete; a view filtra `IS NULL`. |

Chave estrangeira: `contents_category_contents_id_foreign` →
`category_contents(id)`.

## 4. Tabela `category_contents`

| Coluna | Tipo | Null | Default | Papel |
|---|---|---|---|---|
| `id` | `integer` | não | seq | PK. A raiz `sessoes` tem `id = 35` na base de análise. |
| `name` | `varchar(191)` | **não** | — | Rótulo exibido ("Sessões"). |
| `slug` | `varchar(191)` | **não** | — | Chave lógica do módulo (`sessoes`). |
| `category_contents_id` | `integer` | sim | — | Auto-referência; `NULL` = raiz. |
| `status` | `boolean` | sim | `true` | Ativação da categoria. |
| `status_create` | `boolean` | sim | — | Permissão de criação na árvore. |
| `quantidade_itens` | `integer` | sim | — | Quantidade exibida na home. |
| `order` | `integer` | sim | — | Ordenação da árvore. |
| `deleted_at` | `timestamp(0)` | sim | — | Soft delete. |

**Observação importante:** a raiz `sessoes` é usada como marcador do módulo
**e** como categoria de armazenamento ao mesmo tempo. A view e o controller do
portal filtram por `category_contents_id = (raiz sessoes)`, o que significa que
**qualquer subcategoria criada dentro de `sessoes` fica invisível para o
módulo** (o portal busca pelo id exato da raiz; a view também).

## 5. Tabela `archives`

| Coluna | Tipo | Papel no módulo |
|---|---|---|
| `id` | `integer` (PK) | Identificador do anexo. |
| `contents_id` | `integer` (FK → `contents.id`) | Sessão dona do arquivo. |
| `highlight` | `boolean` | `1` = **imagem de destaque** (cabeçalho da listagem e topo da página); `NULL` = **foto da galeria**. |
| `path` | `varchar(300)` | Caminho em `storage/content/sessoes/{id}/destaque` ou `storage/content/sessoes/{id}/galeria`. |
| `name` / `title` | `varchar(300)` | Nome do arquivo. |
| `extension` | `varchar(191)` | Mime/tipo; a galeria só exibe `jpg, jpeg, png, gif, bmp, tiff, svg, webp, ico, heic, raw`. |
| `order` | `integer` | Ordenação da galeria. |
| `view_pdf` | `boolean` | Quando preenchido, o arquivo é tratado como PDF visualizável e **não** entra na galeria. |
| `type` / `hash` / `description` | `varchar`/`text` | metadados do arquivo. |
| `deleted_at` | `timestamp(0)` | Soft delete. |

Relações usadas: `Content::archives()` (`v6/app/Http/Model/Prefecture/Content.php:63`).

## 6. Tabela `links`

| Coluna | Tipo | Papel no módulo |
|---|---|---|
| `id` | `integer` (PK) | Identificador. |
| `contents_id` | `integer` (FK → `contents.id`) | Sessão dona do link. |
| `link` | `varchar(3000)` | URL do vídeo (YouTube, Vimeo, Facebook, Instagram). |
| `title` / `name` / `alt` | `varchar(191)` | Copiados do título da sessão no `store`/`update`. |
| `isBlank` | `boolean` | Flag de abertura em nova aba. |
| `deleted_at` | `timestamp(0)` | Soft delete. |

Ração usada: `Content::linksUnique()` — **`hasOne`**, não `hasMany`
(`v6/app/Http/Model/Prefecture/Content.php:116`). Só o **primeiro** link é
considerado como vídeo; se houver mais de um, os extras são ignorados pelo
portal.

## 7. Relacionamentos (Eloquent)

| Método | Classe | Cardinalidade | Coluna |
|---|---|---|---|
| `Content::categoryContents()` | `CategoryContent` |belongsTo | `category_contents_id` |
| `Content::archives()` | `Archive` | hasMany | `contents_id` |
| `Content::linksUnique()` | `Link` | hasOne | `contents_id` |
| `Content::councilman()` | `CityCouncilor` | belongsTo | `councilmen_id` (não usado por sessões) |

## 8. Migrations e seeds

### 8.1 Migrations

| Caminho | Data | Efeito |
|---|---|---|
| `v6/database/migrations/2024_09_30_004936_create_view_sessoes.php` | 2024-09-30 | `DROP VIEW if exists sessoes` seguido de `CREATE OR REPLACE VIEW sessoes`; o método `down()` derruba a view. |
| `v6/database/migrations/2024_11_01_150206_create_modulos_table.php` | 2024-11-01 | Insere o módulo `sessoes` (id 40, `status = 0`) na tabela `modulos`. |

Não há migration que crie `contents`, `category_contents`, `archives` ou
`links`: são tabelas estruturais do portal, criadas fora do escopo deste
módulo.

### 8.2 Seeds

| Caminho | O que popula |
|---|---|
| `v6/database/seeds/CategoryContentTableSeeder.php` (linhas 196-200) | `CategoryContent::updateOrCreate(['slug' => 'sessoes'], ['name' => 'Sessões', 'status_create' => 1])`. |
| `v6/database/seeds/ResourcesTableSeeder.php` (linhas 486-493) | Recurso Sentinel `sessoes` ligado ao `modulo_id = 40`; é a base das permissões `sessoes.create`, `sessoes.edit`, `sessoes.delete`, etc. |
| `v6/database/seeds/MenuOrderSeeder.php` (linhas 259-265) | `MenuOrder` com `slug = 'sessoes'`, `modulo_id = 40`, `clicks = 0` (contador de telemetria). |
| `v6/database/seeds/AddSessoesToLayout.php` | Cria/atualiza as chaves `layouts.sessoes` (section, `order` 98, path `portal.layouts.modelo5.home`, `status` 1) e `layouts.sessoes-list` (element, `order` 1, path `portal.layouts.modelo5.home.sessoes`, título "Lista de Sessões", `status` 0), e injeta o HTML nas chaves `tags` e `preview` do editor de layout. |

## 9. Estado observado na base de análise

Consultas feitas em `prefeituras_v5` (container `v6_6f22e1bd-postgres-1`):

| Consulta | Resultado |
|---|---|
| `select id,name,slug,category_contents_id,status from category_contents where slug='sessoes'` | `35 | Sessões | sessoes | (null) | t` |
| `select count(*) from contents where category_contents_id = (raiz sessoes)` | `0` |
| `select * from modulos where slug='sessoes'` | `40 | Sessões | sessoes | f` (desligado) |

Ou seja: **a estrutura existe, mas o módulo nasce vazio e desligado**. Os
primeiros registros são criados pelo operador em `/cms/conteudos/sessoes`.
