# Notícias — Modelagem de Dados

## 1. Tabela `contents`

Fonte: `v6/database/migrations/2018_04_02_180433_create_contents_table.php` +
migrations posteriores. Estado atual conferido em `psql` no banco
`prefeituras_v5` do container `v6_6f22e1bd-postgres-1`.

| Coluna | Tipo | Null | Default | Origem / observação |
|---|---|---|---|---|
| `id` | `integer` | não | serial | PK |
| `title` | `varchar(191)` | sim | — | obrigatório em `ContentRequest` (caso `noticias`: `max:150`) |
| `linha_fina` | `varchar(300)` | sim | — | adicionada em `2026_06_25_000001_add_linha_fina_to_contents_table.php`; exibida como subtítulo |
| `text` | `text` | sim | — | HTML do Summernote |
| `clicks` | `integer` | sim | — | inicializado com `'0'` em `ContentController::store` |
| `highlights` | `boolean` | sim | — | destaque (carrossel) |
| `galery` | `boolean` | sim | — | indica que a matéria tem galeria |
| `status` | `boolean` | sim | — | `true` = publicado |
| `source` | `varchar(191)` | sim | — | fonte da notícia (`max:120` na validação) |
| `author` | `varchar(191)` | sim | — | autor (`max:120`) |
| `icon` | `varchar(191)` | sim | — | — |
| `slug` | `varchar(191)` | sim | — | URL da matéria; gerado a partir do título |
| `publication_date` | `timestamp` | sim | — | agendamento; `$casts => 'datetime'` |
| `order` | `integer` | sim | — | ordenação dentro da categoria |
| `category_contents_id` | `integer` | **não** | — | FK -> `category_contents.id` |
| `created_at` / `updated_at` | `timestamp` | sim | — | Eloquent timestamps |
| `deleted_at` | `timestamp` | sim | — | `SoftDeletes` |
| `secretaria` | `varchar(191)` | sim | — | `2022_04_19_084011_add_secretaria_to_contents_table.php` |
| `carousel` | `boolean` | sim | — | `2022_05_06_103629_add_column_to_contents_table.php` |
| `orgaos_id` | `integer` | sim | — | FK -> `contents.id` (auto-relação para órgão) |
| `position_id` | `bigint` | sim | — | `2023_08_17_094529_add_position_id_to_contents_table.php` |
| `acronym` | `varchar(191)` | sim | — | — |
| `layout` | `integer` | sim | — | layout de exibição (usado por `publicacoes`) |
| `councilmen_id` | `integer` | sim | — | `2026_06_12_100000_add_councilmen_id_to_contents_table.php` |
| `referencia` | `varchar(50)` | sim | — | `2026_06_26_100000_add_referencia_to_contents_table.php` |

## 2. Tabela `category_contents`

Fonte: `v6/database/migrations/2018_04_01_180916_create_category_contents_table.php`.

| Coluna | Tipo | Null | Default | Observação |
|---|---|---|---|---|
| `id` | `integer` | não | serial | PK |
| `name` | `varchar(191)` | não | — | rótulo exibido |
| `slug` | `varchar(191)` | não | — | identificador usado por rotas/views |
| `status_create` | `boolean` | sim | — | `0` = categoria de topo usada em "Mais Conteúdos" (`MenuServiceProvider.php:31`) |
| `quantidade_itens` | `integer` | sim | — | quantos itens a categoria mostra na home |
| `order` | `integer` | sim | — | ordenação do menu |
| `layout` | `integer` | sim | — | layout da listagem (1..3) em `NewsTrait::getLayoutContentActived()` |
| `created_at` / `updated_at` | `timestamp` | sim | — | — |
| `deleted_at` | `timestamp` | sim | — | `SoftDeletes` |
| `category_contents_id` | `integer` | sim | NULL | **auto-FK**: aponta para a categoria pai |
| `status` | `boolean` | sim | `true` | `2021_02_18_123218_add_status_to_category_contents.php` |
| `display_type` | `varchar(191)` | sim | — | `2026_06_01_100000_add_display_type_and_custom_icon_to_category_contents.php` |
| `custom_icon` | `varchar(191)` | sim | — | idem |
| `description` | `text` | sim | — | `2026_06_04_100000_add_description_to_category_contents_table.php` |

### Árvore de "Notícias" (dados reais do banco `prefeituras_v5`)

Consulta executada: `select id, name, slug, category_contents_id from category_contents order by id;`

| id | slug | name | pai (`category_contents_id`) |
|---|---|---|---|
| 1 | `noticias` | Notícias | — (raiz) |
| 2 | `legislacao` | Legislação | — (raiz) |
| 3 | `publicacoes` | Publicações | — (raiz) |
| 4 | `a-cidade` | A cidade | — (raiz) |
| 5 | `licitacao-anterior` | Licitação | — (raiz) |
| 6 | `a-instituicao` | A Instituição | — (raiz) |
| 7 | `secretarias` | Secretarias | — (raiz) |
| 8 | `concursos` | Concursos | — (raiz) |
| 9 | `vagas-de-emprego` | Vagas de emprego | — (raiz) |
| 10 | `transparencia` | Transparência | — (raiz) |
| 11 | `glossario` | Glossário | — (raiz) |
| 12 | `perguntas-frequentes` | Perguntas Frequentes | — (raiz) |
| 13 | `documentos` | Documentos | 3 |
| 14 | `editais` | Editais | 3 |
| 15 | `horarios-funcionarios` | Horários Funcionários | 3 |
| 16 | `manuais` | Manuais | 3 |
| 17 | `mensario-oficial` | Mensário oficial | 3 |
| 18 | `concurso-publico` | Concurso Público | 3 |
| 19 | `campanhas` | Campanhas | 3 |
| 20 | `diario-oficial` | Diário oficial | 3 |
| 21 | `portal-do-contribuinte` | Portal do Contribuinte | 3 |
| 22 | `constituicao-federal` | Constituição Federal | 2 |
| 23 | `codigo-tributario` | Código Tributario | 2 |
| 24 | `decretos` | Decretos | 2 |
| 25 | `transparencia-fiscal` | Transparência Fiscal | — (raiz) |
| 26 | `rede-social` | Rede Social | — (raiz) |
| 27 | `banners` | Banners | — (raiz) |
| 28 | `anuncios` | Anúncios | — (raiz) |
| 29 | `apoio-ao-usuario` | Apoio ao Usuário | — (raiz) |
| 30 | `telefones-uteis` | Telefones Úteis | — (raiz) |
| 31 | `midias` | Mídias | — (raiz) |
| 32 | `orgaos` | Órgãos | — (raiz) |
| 33 | `super-banners` | Super Banners | — (raiz) |
| 34 | `landing-covid` | Landing Page Covid | — (raiz) |
| 35 | `sessoes` | Sessões | — (raiz) |
| 36 | `transmissoes` | Transmissões | — (raiz) |
| 37 | `requerimentos` | Requerimentos | — (raiz) |
| 38 | `pauta-do-dia` | Pautas do Dia | — (raiz) |
| 39 | `comite-de-investimentos` | Comitê de Investimentos | — (raiz) |
| 40 | `agenda-eventos` | Agenda de eventos | — (raiz) |
| 41 | `atas-e-pontos` | Atas e Pontos de Frequência | — (raiz) |
| 42 | `atas` | Atas | 41 |
| 43 | `pontos-de-frequencia` | Pontos de Frequência | 41 |
| 44 | `unidades-de-atendimento` | Unidades de Atendimento | — (raiz) |
| 45 | `acesso-rapido` | Acesso Rápido | — (raiz) |

**Fato crítico verificado no banco:** hoje **nenhuma** categoria tem
`category_contents_id = 1`. Consequentemente:

- a view SQL `noticias` (que filtra `cc.category_contents_id = 1`) retorna
  **0 linhas** (`select count(*) from noticias;` -> `0`);
- o escopo `scopeByCategoryContentNews`, que exige
  `where('category_contents_id', 1)` dentro do `whereHas`, também não devolve
  nenhuma notícia.

Ou seja: **o CMS e o portal de notícias só funcionam depois de a prefeitura
criar subcategorias linked à categoria "Notícias" (id 1)** — o que o CMS
permite via `categoria-conteudo`, mas que não está no seed
(`v6/database/seeds/CategoryContentTableSeeder.php`, que cria as categorias
26-45 como raízes, e não como filhas de "Notícias").

> Categoria "Notícias" = **id 1**. Subcategorias = `category_contents.category_contents_id = 1`.

## 3. Tabela `archives`

Fonte: `v6/database/migrations/2018_04_02_181807_create_archives_table.php`.

| Coluna | Tipo | Null | Observação |
|---|---|---|---|
| `id` | `integer` | não | PK |
| `title` | `varchar(300)` | sim | título do arquivo |
| `description` | `text` | sim | descrição livre |
| `type` | `varchar(191)` | sim | MIME type |
| `hash` | `varchar(300)` | sim | — |
| `path` | `varchar(300)` | sim | caminho relativo `storage/content/...` |
| `name` | `varchar(300)` | sim | nome físico |
| `extension` | `varchar(191)` | sim | — |
| `status_create` | `boolean` | sim | — |
| `highlight` | `boolean` | sim | `1` = imagem de destaque; `NULL` = galeria; `0` = galeria "a-cidade" |
| `deleted_at` | `timestamp` | sim | `SoftDeletes` |
| `created_at` / `updated_at` | `timestamp` | sim | — |
| `contents_id` | `integer` | sim | FK -> `contents.id` |
| `static_pages_id` | `integer` | sim | FK -> `static_pages.id` |
| `biddings_id` | `integer` | sim | FK -> `biddings.id` |
| `calendar_events_id` | `integer` | sim | FK -> `calendar_events.id` |
| `order` | `integer` | sim | ordem na galeria |
| `view_pdf` | `boolean` | sim | `2022_11_29_092240_add_viewpdf_to_archives_table.php` — marca o PDF exibido inline |
| `reference_id` | `text` | sim | — |
| `type_size_id` | `bigint` | sim | FK -> `types_sizes.id` |
| `width` / `height` | `varchar(191)` | sim | dimensões do banner |
| `requerimento_id` | `bigint` | sim | — |
| `tv_camara_id` | `bigint` | sim | — |
| `highlight_id` | `integer` | sim | FK -> `highlights.id` `ON DELETE CASCADE` |

## 4. Tabela `links`

Fonte: `v6/database/migrations/2018_04_02_181150_create_links_table.php` +
`2019_09_10_161305_alter_link_to_links_table.php`.

| Coluna | Tipo | Null | Observação |
|---|---|---|---|
| `id` | `integer` | não | PK |
| `title` | `varchar(191)` | sim | — |
| `link` | `varchar(3000)` | sim | URL do vídeo; sanitizada por `ContentController::sanitizeLink()` |
| `alt` | `varchar(191)` | sim | — |
| `name` | `varchar(191)` | sim | — |
| `isBlank` | `boolean` | sim | abre em nova aba |
| `status_create` | `boolean` | sim | — |
| `created_at` / `updated_at` | `timestamp` | sim | — |
| `deleted_at` | `timestamp` | sim | `SoftDeletes` |
| `contents_id` | `integer` | sim | FK -> `contents.id` |
| `static_pages_id` | `integer` | sim | FK -> `static_pages.id` |

## 5. Relacionamentos Eloquent

| Método | Modelo | Tipo | Implementação |
|---|---|---|---|
| `archives()` | `Content` | hasMany | `hasMany(Archive::class, 'contents_id', 'id')` |
| `archiveHighlight()` | `Content` | hasOne | `hasOne(Archive::class)->byHighLight(1)` |
| `archiveGallery()` | `Content` | hasMany | `hasMany(Archive::class)->byHighLight(null)->orderBy('created_at','desc')` |
| `archiveAcidade()` | `Content` | hasOne | `hasOne(Archive::class)->byHighLight(0)` |
| `categoryContents()` | `Content` | belongsTo | `belongsTo(CategoryContent::class, 'category_contents_id', 'id')` |
| `linksUnique()` | `Content` | hasOne | `hasOne(Link::class, 'contents_id', 'id')` |
| `secretarias()` | `Content` | hasMany | `hasMany(Content::class, 'secretaria', 'slug')` (auto-relação por slug) |
| `orgaos()` | `Content` | hasMany | `hasMany(Content::class, 'orgaos_id', 'id')` |
| `councilman()` | `Content` | belongsTo | `belongsTo(CityCouncilor::class, 'councilmen_id')` |
| `position()` | `Content` | hasOne | `hasOne(Position::class, 'id', 'position_id')` |
| `categoryContents()` | `CategoryContent` | hasMany | `hasMany(CategoryContent::class, 'id', 'category_contents_id')` |
| `rightWayToGetCategoryContents()` | `CategoryContent` | hasMany | `hasMany(CategoryContent::class, 'category_contents_id', 'id')` (relação correta) |
| `categoryContentsBelongings()` | `CategoryContent` | belongsTo | pai |
| `contents()` | `CategoryContent` | hasMany | filtrado por `byStatus()->byPublication()->byOrder('publication_date')` |
| `contentsByCategory()` | `CategoryContent` | hasMany | `status = true` e `publication_date <= now` |

## 6. Views SQL

| View | Definição (origem) | Usada por |
|---|---|---|
| `noticias` | `v6/database/migrations/2024_07_01_094338_udate_noticias_view.php` | `DatatableController::get_noticias` (listagem do CMS) |

SQL efetivo (confere com `pg_get_viewdef`):

```sql
CREATE OR REPLACE VIEW noticias
as select
    row_number() OVER (ORDER BY c.highlights is true desc) as value,
    c.title as titulo,
    CASE WHEN c.status is true THEN 'Publicado' ELSE 'Rascunho' END AS status_name_noticias,
    to_char(c.publication_date::timestamp with time zone, 'DD/MM/YYYY'::text) AS data_publicacao,
    cc."name" as categoria,
    c.id as id,
    c.highlights as destaque,
    c.galery as galeria,
    c.slug as slug,
    c.clicks as visualizacoes
from contents c
    JOIN category_contents cc ON cc.id = c.category_contents_id
where c.deleted_at is null
and cc.category_contents_id = 1
order by c.highlights is true desc;
```

Observações:

- O filtro é `cc.category_contents_id = 1`, ou seja, a view lista **as
  subcategorias de Notícias**, e não a categoria 1 em si.
- A view **não** filtra `status` nem `publication_date`: a datatable do CMS
  mostra rascunhos e matérias agendadas para o futuro.
- A ordenação é por destaque, não por data.

Histórico da view: `2023_03_15_115654_create_view_to_noticias.php`,
`2023_03_17_082735_update_noticias_view.php`,
`2023_03_31_112621_update_noticias_view_to_publication-date.php`,
`2023_05_16_111448_update_view_data_create_add_hours.php`,
`2023_05_17_153155_update_noticias_view_to_slug.php`,
`2023_05_26_090139_correcao_de_view_noticias.php`,
`2023_05_29_083929_update_noticias_view_data_publicacao.php`,
`2024_07_01_094338_udate_noticias_view.php` (atual).

## 7. Seeds

| Seed | Conteúdo |
|---|---|
| `v6/database/seeds/CategoryContentTableSeeder.php` | Cria as 45 categorias; a raiz "Notícias" (id 1) e as subcategorias filhas |
| `v6/database/seeds/ContentsTableSeeder.php` | Conteúdos de demonstração por categoria (1251 linhas) |
| `v6/database/seeds/ResourcesTableSeeder.php:62-69` | `Resource` `noticias` -> `modulo_id = 14` |
| `v6/database/migrations/2024_11_01_150206_create_modulos_table.php:108-112` | `modulos` `noticias` (id 14, `status = 1`) |
| `v6/database/seeds/ArchivesTableSeeder.php` | Arquivos de demonstração |
| `v6/database/seeds/UpdateArchiveSeeder.php` | Ajustes de arquivos legados |

## 8. Regras de integridade

| Regra | Onde | Efeito |
|---|---|---|
| `contents.category_contents_id` NOT NULL | migration 2018-04-02 | Conteúdo sem categoria é rejeitado pelo banco |
| FK `contents.category_contents_id` -> `category_contents.id` | migration 2018-04-02 | Integridade referencial |
| Slug único por conteúdo | apenas na aplicação | `ContentController::store` desambigua com `-<id>`; `update` usa prefixo `pub<id>-` fora da categoria 1 |
| Exclusão de conteúdo remove itens de menu | `ContentController::destroy` (`ContentController.php:1320-1367`) | `MenuItem::where('contents_id', ...)` é apagado junto |
