# Publicações — Modelagem de Dados

## 1. Tabelas envolvidas

Publicações **não possui tabela própria**. Reutiliza:

| Tabela | Papel no módulo |
|---|---|
| `contents` | Documento publicado |
| `category_contents` | Raiz "Publicações" (id 3) e suas subcategorias |
| `archives` | Anexos do documento (galeria + PDF inline) |
| `links` | Não utilizada por Publicações (o módulo de vídeo é o de Notícias) |
| `configurations` | `cor_1` (cor do PDF de exportação), `layout` (template do portal) |

As definições completas de `contents`, `category_contents`, `archives` e
`links` estão em `../noticias/02-MODELAGEM.md`. Este documento detalha apenas
os campos relevantes para Publicações.

## 2. Campos de `contents` usados por Publicações

| Coluna | Uso |
|---|---|
| `id` | PK; âncora dos anexos |
| `title` | Título do documento (máx. 120 na validação) |
| `text` | Corpo em HTML (Summernote) |
| `slug` | **Link Permanente**: `/publicacoes/{categoria}/{slug}` |
| `publication_date` | Data de publicação; **obrigatória**; determina a visibilidade e a ordenação |
| `category_contents_id` | Subcategoria (13..21) do documento |
| `status` | Publicado / rascunho — **não verificado** por `viewPublication` |
| `clicks` | Contador de aberturas do documento |
| `layout` | `1` = com barra lateral, `2` = sem barra lateral |
| `galery` | Indica que há anexos |
| `deleted_at` | Soft delete |

## 3. Árvore de "Publicações"

Consulta: `select id, name, slug, category_contents_id, order from category_contents where id = 3 or category_contents_id = 3 order by id;`

| id | slug | name | pai |
|---|---|---|---|
| 3 | `publicacoes` | Publicações | — (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 |

> No banco atual, as 9 subcategorias têm `order` **NULL** e
> `quantidade_itens` **NULL**. Como `getIndex()` ordena por `order`, a
> ordem de apresentação é indefinida (empate resolvido pela ordem física do
> Postgres).

## 4. Relacionamentos

| Método | Modelo | Tipo | Uso em Publicações |
|---|---|---|---|
| `categoryContents()` | `Content` | belongsTo | Nome da categoria no título e no breadcrumb |
| `archives()` | `Content` | hasMany | Anexos do documento |
| `categoryContents()` | `CategoryContent` | hasMany | Subcategorias |
| `rightWayToGetCategoryContents()` | `CategoryContent` | hasMany | Filhos diretos (relação correta) |
| `getAllDescendants($withParents)` | `CategoryContent` | método | Árvore completa de subcategorias, recursivo |
| `contents()` | `CategoryContent` | hasMany | Conteúdos publicados (status + data <= now) |
| `contentsByCategory()` | `CategoryContent` | hasMany | Idem, ordenação explícita |

Implementação da recursão (`v6/app/Http/Model/Prefecture/CategoryContent.php:56-77`):

```php
public function getAllDescendants($withParents = false)
{
    $sections = new Collection();

    foreach ($this->rightWayToGetCategoryContents as $section) {
        $data = $section->only(['id', 'name', 'order', 'slug', 'category_contents_id']);

        if ($withParents) {
            $parent = CategoryContent::find($section->category_contents_id);

            if ($parent && $parent->slug !== 'publicacoes' && $parent->slug !== 'legislacao') {
                $data['name'] = $parent->name . ' > ' . $data['name'];
            }
        }

        $sections->push($data);
        $sections = $sections->merge($section->getAllDescendants($withParents)); // importante passar o mesmo parâmetro
    }

    return $sections;
}
```

Observações:

- A recursão é feita em **PHP** (N+1: uma consulta por categoria, por nível).
- Os objetos retornados são `Collection` de `Model` (não arrays), e o método
  `only()` devolve um **array**, misturando tipos dentro da coleção.
- A prefixação do nome do pai é suprimida justamente para as raízes
  `publicacoes` e `legislacao`.

## 5. View SQL `publicacoes`

| Item | Valor |
|---|---|
| Arquivo | `v6/database/migrations/2024_11_28_100323_update_view_publicacoes.php` |
| Definição no banco | `pg_get_viewdef('publicacoes'::regclass, true)` |

```sql
CREATE OR REPLACE VIEW publicacoes
as select
    row_number() OVER (ORDER BY c.publication_date DESC) as value,
    c.title as titulo,
    to_char(c.publication_date::timestamp with time zone, 'DD/MM/YYYY às HH24:MI'::text) AS data_publicacao,
    cc.name as categoria,
    cc.slug as categoria_slug,
    c.slug as slug,
    c.id as id
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 = 3;
```

| Observação | Detalhe |
|---|---|
| Filtro | `cc.category_contents_id = 3` — apenas subcategorias de Publicações |
| Não filtra `status` | Rascunhos aparecem na view |
| Não filtra `publication_date` | Documentos futuros aparecem na view |
| **Não é usada pela datatable do CMS** | `DatatableController::get_publicacoes` monta o array em PHP |

Histórico da view: `2023_03_15_145030_create_view_to_publications.php`,
`2023_03_31_134920_update_publications_view_to_publication-date.php`,
`2024_11_28_100323_update_view_publicacoes.php` (atual).

## 6. Migrations relevantes

| Migration | Efeito |
|---|---|
| `v6/database/migrations/2018_04_01_180916_create_category_contents_table.php` | Cria `category_contents` com auto-FK `category_contents_id` |
| `v6/database/migrations/2018_04_02_180433_create_contents_table.php` | Cria `contents` com FK `category_contents_id` NOT NULL |
| `v6/database/migrations/2018_04_02_181807_create_archives_table.php` | Cria `archives` com `contents_id` e `static_pages_id` |
| `v6/database/migrations/2021_02_18_123218_add_status_to_category_contents.php` | `category_contents.status` (default `true`) |
| `v6/database/migrations/2022_11_29_092240_add_viewpdf_to_archives_table.php` | `archives.view_pdf` (PDF inline) |
| `v6/database/migrations/2023_03_15_145030_create_view_to_publications.php` | Cria a view `publicacoes` |
| `v6/database/migrations/2024_11_28_100323_update_view_publicacoes.php` | Versão atual da view |
| `v6/database/migrations/2024_11_01_150206_create_modulos_table.php:114-118` | `modulos` `publicacoes` (id 15) |

## 7. Seeds

| Seed | Conteúdo |
|---|---|
| `v6/database/seeds/CategoryContentTableSeeder.php:22-26` | Cria a raiz "Publicações" (`slug = 'publicacoes'`) |
| `v6/database/seeds/CategoryContentTableSeeder.php:76-125` | Cria as subcategorias 13..21, todas com `category_contents_id = $publicacoes->id` |
| `v6/database/seeds/ContentsTableSeeder.php` | Documentos de demonstração por categoria |
| `v6/database/seeds/ResourcesTableSeeder.php:71-78` | `Resource` `publicacoes` -> `modulo_id = 15` |
| `v6/database/seeds/ArchivesTableSeeder.php` | Anexos de demonstração |
| `v6/database/seeds/UpdateLegislacaoResourceNameSeeder.php` | Ajuste de nome do recurso de Legislação (módulo irmão) |

Trecho do seed das subcategorias:

```php
// ID 13
CategoryContent::updateOrCreate(['slug' => 'documentos'], [
    'name' => 'Documentos',
    'category_contents_id' => $publicacoes->id
]);
// ID 14
CategoryContent::updateOrCreate(['slug' => 'editais'], [
    'name' => 'Editais',
    'category_contents_id' => $publicacoes->id
]);
```

## 8. Onde os arquivos são gravados

| Tipo | Caminho |
|---|---|
| Anexo enviado no formulário | `storage/content/publicacoes/{slug-categoria}/{id}/arquivos/{nome}` |
| Imagem de destaque | `storage/content/publicacoes/{slug-categoria}/{id}/destaque/{nome}` |
| Galeria (upload Ajax) | `storage/content/publicacoes/{slug-categoria}/{id}/galeria/{nome}` |
| Imagem do Summernote | `storage/summernote-files/content/publicacoes/{id}/{nome}` |

A diferenciação entre "com categoria pai" e "sem categoria pai" ocorre em
`ContentController::store` (`v6/app/Http/Controllers/Prefecture/ContentController.php:769-780`):

```php
$verificaSubCategory = CategoryContent::find($category_contents_id);
if (!empty($verificaSubCategory->category_contents_id)) {
    $categoriaPai = CategoryContent::find($verificaSubCategory->category_contents_id);
    $insertArchive->path = "storage/content/$categoriaPai->slug/$slug/$content->id/arquivos/$name";
    $upload = $archive[0]->storeAs("content/$categoriaPai->slug/$slug/$content->id/arquivos/", $name);
} else {
    $insertArchive->path = "storage/content/$slug/$content->id/arquivos/$name";
    $upload = $archive[0]->storeAs("content/$slug/$content->id/arquivos/", $name);
}
```

> Como Publicações **sempre** tem categoria pai (a raiz 3), o caminho real
> é `storage/content/publicacoes/{slug-subcategoria}/{id}/arquivos/`.

## 9. Regras de integridade

| Regra | Onde | Efeito |
|---|---|---|
| `contents.category_contents_id` NOT NULL + FK | migration 2018-04-02 | Documento sem categoria é rejeitado |
| Slug único | apenas na aplicação | `ContentController::store` desambigua com `-<id>`; `update` usa `pub<id>-` |
| `archives.contents_id` FK | migration 2018-04-02 | Anexo órfão é impossível |
| Exclusão remove `menu_items` | `ContentController::destroy` | Documento no menu some junto |
| Categoria inexistente no `export` | `PublicationController::export` | `abort(404)` |
| Slug de categoria inexistente no portal | `PublicationController::showPublication` | `redirect('/publicacoes')` |
