# Sessões da Câmara Municipal — Regras e Portabilidade

> Módulo `sessoes` na `v6`.

---

## 1. Regras de negócio (observáveis)

### 1.1 Visibilidade no portal

| # | Regra | Onde está implementada |
|---|---|---|
| R1 | Um conteúdo só aparece na listagem pública se `status = true`. | `v6/app/Http/Controllers/Portal/SessionsController.php:30` |
| R2 | Um conteúdo só aparece se `publication_date <= now`. | `SessionsController.php:29` |
| R3 | A listagem é paginada em **12** itens. | `SessionsController.php:32` (`paginate(12)`) |
| R4 | A ordenação é `publication_date DESC`. | `SessionsController.php:31` |
| R5 | A ordenação da listagem do CMS é `ASC` sobre a coluna `value` da view — que é o `row_number()` sobre `publication_date DESC` — de modo que a sessão **mais recente aparece primeiro** na tabela do operador. | `v6/public/assets/js/custom/contents/sessoes/datatables.js` (`runDatatables('sessoes', collums, true, 'ASC', null, true)`) |
| R6 | A página de detalhe resolve o conteúdo por `slug` **restrito** à categoria `sessoes`; slug existente em outra categoria devolve 404. | `SessionsController.php:44-46` (`whereHas('categoryContents')` + `firstOrFail`) |
| R7 | A galeria só é renderizada quando `contents.galery === true` (comparação estrita com booleano). | `v6/resources/views/portal/layouts/modelo5/sessoes/show.blade.php:36` |
| R8 | O vídeo só é renderizado quando `linksUnique->link` está definido. | `v6/resources/views/portal/layouts/modelo5/sessoes/show.blade.php:40` |
| R9 | O `highlight` (imagem de destaque) é exclusive da galeria: a galeria usa `highlight IS NULL` e a capa usa `highlight = 1`. | `SessionsController.php:48` e `v6/resources/views/portal/components/galerias/index.blade.php` |
| R10 | A view SQL da listagem do CMS **não** filtra por `status` nem por data; ela mostra rascunhos e agendamentos futuros. | `v6/database/migrations/2024_09_30_004936_create_view_sessoes.php` |

### 1.2 Regras de edição (CMS)

| # | Regra | Onde está implementada |
|---|---|---|
| R11 | Título obrigatório, máximo 150 caracteres. | `v6/app/Http/Requests/ContentRequest.php` (caso `sessoes`) |
| R12 | Data de publicação obrigatória. | idem |
| R13 | Texto obrigatório. | idem |
| R14 | Imagem de destaque opcional, máximo 20 MB, mimes `jpg,jpeg,png,gif,webp`. | idem |
| R15 | A URL do vídeo tem no máximo 2999 caracteres (coluna `links.link` é `varchar(3000)`). | idem |
| R16 | O `slug` é gerado do título no `store` (`Str::slug($title, '-')`) e **preservado** no `update` (o formulário não envia `slug`). | `v6/app/Http/Controllers/Prefecture/ContentController.php:584` e 616-620 |
| R17 | Se já existir um conteúdo com o mesmo slug, o `store` acrescenta `-{max_id}` ao final. | `ContentController.php:665-667` |
| R18 | A categoria é fixa na raiz `sessoes`; o formulário envia `category_slug` e `category_contents_id` como hidden. | `v6/resources/views/prefecture/contents/sessoes/form.blade.php` |
| R19 | Só o **primeiro** link do conteúdo é tratado como vídeo (`linksUnique` é `hasOne`). | `v6/app/Http/Model/Prefecture/Content.php:116` |
| R20 | A exclusão é lógica (`deleted_at`), e a view SQL já exclui os registros apagados. | migration da view + `SoftDeletes` no model |

### 1.3 Conversão do vídeo em *embed*

`embedVideo()` (`v6/app/Helpers/functions.php:607-665`) normaliza a URL
gravada em `links.link`:

| Origem | URL gerada |
|---|---|
| `youtube.com/watch?v=ID`, `youtu.be/ID`, `youtube.com/embed/ID` | `https://www.youtube.com/embed/ID` |
| `vimeo.com/ID` | `https://player.vimeo.com/video/ID` |
| `facebook.com/videos/{id}` | `https://www.facebook.com/plugins/video.php?href={urlencode}&show_text=false` |
| `instagram.com/p|reel|tv/ID` | `https://www.instagram.com/{tipo}/{ID}/embed/` |
| Qualquer outra | a URL original (fallback) |

Antes de gravar, o CMS ainda passa a URL por `normalizeVideoLink()`
(`v6/app/Helpers/functions.php:683`) e `sanitizeLink()`
(`v6/app/Http/Controllers/Prefecture/ContentController.php:595`), que
garantem o esquema `https://` e removem query strings de Instagram.

## 2. Acoplamentos

### 2.1 Dependências de outros módulos

| Módulo | Acoplamento | Risco |
|---|---|---|
| **Notícias** | O `modelo3` e o `modelo4` condicionam a listagem de sessões a `getStatusDisplayNews()['displayNews'] == 0` (`v6/resources/views/portal/layouts/modelo3/sessoes/index.blade.php`). | Desligar o módulo de Notícias **esconde** as sessões. |
| **Notícias** | O `modelo4` mostra, no card, `$content->categoryContents->name` (o nome da categoria). | Exibe "Sessões" como categoria, o que é correto, mas depende da relação carregada. |
| **Layout / Home** | O bloco da home depende das chaves `layouts.sessoes` e `layouts.sessoes-list` (seed `v6/database/seeds/AddSessoesToLayout.php`) e do `$sessions` montado em `v6/app/Http/Controllers/Portal/PortalController.php:145-150`. | Se o layout ativo não for o `modelo5`, o widget não é renderizado. |
| **Módulos (CMS)** | O item de menu só aparece se `modulos.status = 1` para o slug `sessoes`. | O módulo nasce **desligado** (`status = 0` no seed). |
| **Permissões (Sentinel)** | Recurso `sessoes` criado em `v6/database/seeds/ResourcesTableSeeder.php` e vinculado ao `modulo_id = 40`. | Sem o seed, nenhuma permissão existe e o menu some. |
| **Configurações** | A chave `layout` da tabela `configurations` define o tema usado pelo controller (`getTemplateActived()`). | Temas `default`/`newsbit` quebram `/sessoes` com 404. |

### 2.2 Dependências técnicas (não-negociáveis)

| Dependência | Detalhe |
|---|---|
| View SQL `sessoes` | Precisa existir no banco. É criada por `migrate`; em ambiente novo, sem a migration aplicada, a listagem do CMS quebra com erro de relação inexistente. |
| Tabela `contents` e categoria raiz | Sem o seed `CategoryContentTableSeeder.php` não há categoria `sessoes`; `CategoryContent::where('slug','sessoes')->first()` retorna `null` e o controller do portal quebra em `$category->id`. |
| `archives` e `links` | Sem elas não há destaque, galeria nem vídeo. |
| `Storage` do Laravel | Os anexos são gravados em `content/sessoes/{id}/destaque` e `content/sessoes/{id}/galeria` (`storage/app/public`). |

## 3. Armadilhas

### 3.1 A view SQL duplica a fonte de verdade

A listagem do CMS lê `DB::table('sessoes')` — a **view** — e não a tabela
`contents`. Consequências:

- qualquer mudança no esquema de `contents` que afete a view exige migration;
- a view não é "self-healing": se alguém a dropar, a listagem quebra;
- o `row_number()` é recalculado a cada consulta e **não** é paginado nem
  estável — o DataTable ordena por essa coluna;
- a view é criada com `DB::statement` (SQL cru), fora do esquema declarativo
  do Laravel, e não é versionada no repositório.

### 3.2 A rota `sessoes.show` convive com `conteudo.portal.show`

- `v6/resources/views/portal/layouts/modelo1/sessoes/index.blade.php:18` e
  `modelo2/sessoes/index.blade.php:18` apontam para
  `route('conteudo.portal.show', [slug da categoria, slug do conteúdo])`;
- o breadcrumb do detalhe (`v6/routes/breadcrumbs.php:73`) também aponta para
  `conteudo.portal.show`.

Resultado: `/sessoes/{slug}` e `/portal/sessoes/{slug}` são **duas URLs
públicas** para o mesmo conteúdo, com o breadcrumb apontando para a segunda.
Na v7 é preciso escolher uma canônica e emitir *redirect* da outra.

### 3.3 Filtro de status apenas no portal

`status` e `publication_date` só são filtrados em `SessionsController@index`.
Consequências observáveis:

- a **página de detalhe** (`show`) **não** filtra por `status` nem por data:
  uma sessão em rascunho ou com data futura é acessível por URL direta
  (`/sessoes/{slug}`), desde que o layout exista;
- o **widget da home** (`modelo5`) também não filtra: usa a consulta crua do
  `PortalController`;
- o **CMS** lista rascunhos por design.

### 3.4 `galery` é booleano e a view compara com `===`

`$session->galery === true` falha se o valor vier como `1` (inteiro) em vez de
`true` (booleano). O PostgreSQL devolve `boolean`, e o cast do Eloquent não
está declarado no model `Content` (`v6/app/Http/Model/Prefecture/Content.php`),
mas o `PDO` do PostgreSQL devolve `true`/`false` para colunas booleanas, então
na prática funciona. Em migração para o MySQL ou para outra camada de
persistência, o `=== true` pode quebrar silenciosamente.

### 3.5 `linksUnique` é `hasOne`

Se o conteúdo tiver mais de um link em `links`, apenas o primeiro (por ordem
do PK) é usado como vídeo. O CMS sempre grava **um** link por sessão
(`ContentController.php:1064-1112`), mas a tabela `links` é genérica e pode
ter sido populada por outros caminhos.

### 3.6 A view `sessoes` filtra pela raiz, não pela subárvore

`contents.category_contents_id = (SELECT id FROM category_contents WHERE slug =
'sessoes' AND category_contents_id IS NULL)`. Se alguém criar uma subcategoria
dentro de `sessoes` (por exemplo "Sessões extraordinárias") e mover itens
para lá, **esses itens somem** do módulo — tanto da listagem do CMS (view)
quanto do portal (controller usa o id exato da raiz).

### 3.7 Slug e colisão

O `store` desambigua slugs duplicados com `slug-{max_id}`; o `update` usa
`pub{id}-{slug}` quando há colisão e a categoria pai **não** é a id 1. Como a
categoria `sessoes` tem `id = 35`, o `update` cai no ramo `else` e o slug
colide é prefixado com `pub{id}-`. O `ContentRequest` do caso `sessoes`
**não** exige `slug`, de modo que a edição normalmente preserva o slug.

### 3.8 Carga da view inteira em memória

`get_sessoes()` faz `DB::table('sessoes')->get()` — sem `limit`, sem `offset`,
sem paginação no servidor. O DataTables server-side pagina **no cliente**,
após carregar todas as linhas. Para Portal com Acervo grande, isso é um
problema de memória e tempo de resposta.

### 3.9 Acoplamento com `getTemplateActived()` e o 404

`getTemplateActived()` verifica apenas a existência de
`portal.layouts.{tema}.home.index`. Temas `default` e `newsbit` não têm esse
arquivo nem a pasta `sessoes/`, então o acesso a `/sessoes` devolve 404
`view_inexistente` — e o efeito colateral é gravar
`configurations.pagina_em_manutencao = 1`, colocando o portal inteiro em
manutenção.

### 3.10 Dependência de `getStatusDisplayNews()` no `modelo3`/`modelo4`

Já descrito em 2.1. É a armadilha mais escondida: nada no módulo `sessoes`
indica que a listagem depende da configuração do módulo de Notícias.

### 3.11 Arquivo de JavaScript inexistente

`v6/resources/views/prefecture/contents/sessoes/form.blade.php` referencia
`assets/js/custom/contents/sessoes/edit.js`; o diretório
`v6/public/assets/js/custom/contents/sessoes/` contém somente
`datatables.js`. O 404 é silencioso e não quebra a tela.

### 3.12 Uso de `str_limit` e `str_slug`

`str_limit` e `str_slug` são helpers do Laravel 5.5 **removidos** no Laravel
6+. Uma reconstrução precisa trocá-los por `Str::limit` e `Str::slug`.

## 4. Onde a v6 divergiu da v5

**O módulo não existe na v5.** A busca por `sessoes`, `sessions`, `sessoes`
em `v5/routes/`, `v5/app/`, `v5/database/`, `v6/resources/views/` não retorna
nenhum equivalente funcional (apenas arquivos de configuração de sessão do
framework e bibliotecas de terceiros).

Consequências práticas da ausência na v5:

| Item | v5 | v6 |
|---|---|---|
| Rotas públicas | — | `sessoes.index`, `sessoes.show` |
| View SQL | — | `sessoes` (migration de 2024-09-30) |
| Categoria | — | raiz `sessoes` (id 35) |
| Módulo do CMS | — | `modulos.id = 40`, `status = 0` |
| Recurso de permissão | — | `resources.slug = 'sessoes'` |
| Widget da home | — | `layouts.sessoes` / `layouts.sessoes-list` |
| Blades do portal | — | 6 layouts × 2 arquivos + widget |
| Blades do CMS | — | `index`, `create`, `edit`, `form` |

Não há "código duplicado ou copiado entre v5 e v6" para este módulo: ele é
integralmente novo.

## 5. Checklist de portabilidade para a v7

| # | Item |
|---|---|
| 1 | Remover a dependência da view SQL; listar por Eloquent com `scope`/`where` e paginação real. |
| 2 | Fixar uma URL canônica para a sessão e redirecionar as demais. |
| 3 | Aplicar o filtro de publicação também no detalhe e no widget da home. |
| 4 | Decidir o destino das subcategorias dentro de `sessoes` (hoje invisíveis). |
| 5 | Trocar `str_limit`/`str_slug` por `Str::limit`/`Str::slug`. |
| 6 | Declarar `casts` explícitos no model (`status`, `galery`, `publication_date`). |
| 7 | Eliminar a dependência de `getStatusDisplayNews()` nos layouts de sessão. |
| 8 | Garantir que `edit.js` exista ou remover a referência do Blade. |
| 9 | Paginar o endpoint do CMS no servidor. |
| 10 | Manter `embedVideo()` (ou equivalente) com os mesmos formatos de URL. |
