# Comitê de Investimentos — Modelagem de Dados

## 1. Visão geral

O módulo **não cria nenhuma tabela própria**. Todas as atas do Comitê de
Investimentos são registros da tabela genérica `contents`, classificados por
`category_contents`, e os anexos ficam em `archives`. A única estrutura criada
exclusivamente para o módulo é a **view SQL** `comite_de_investimentos`.

| Objeto | Tipo | Criado por | Observação |
|---|---|---|---|
| `contents` | tabela | herança do núcleo de conteúdo | Linhas das atas |
| `category_contents` | tabela | herança do núcleo de conteúdo | Categoria-pai + subcategorias |
| `archives` | tabela | herança do núcleo de conteúdo | Anexos das atas |
| `comite_de_investimentos` | **view** | `v6/database/migrations/2024_10_16_011540_create_view_comite_de_investimentos.php` | Consumida só pelo DataTable do CMS |
| `modulos` | tabela | `v6/database/migrations/2024_11_01_150206_create_modulos_table.php` | Registro id 48 |

## 2. `contents` — tabela das atas

Schema real (PostgreSQL 10, banco `prefeituras_v5`):

| Coluna | Tipo | Nullable | Default | Uso no módulo |
|---|---|---|---|---|
| `id` | integer | not null | `contents_id_seq` | PK |
| `title` | varchar(191) | sim | — | Título da ata |
| `text` | text | sim | — | Texto rico (HTML do Summernote) |
| `clicks` | integer | sim | — | Contador incrementado na leitura |
| `highlights` | boolean | sim | — | Destaque (não usado no comitê) |
| `galery` | boolean | sim | — | Galeria de anexos |
| `status` | boolean | sim | — | Publicação (campo `status_create` no controller) |
| `source` | varchar(191) | sim | — | — |
| `author` | varchar(191) | sim | — | — |
| `icon` | varchar(191) | sim | — | — |
| `slug` | varchar(191) | sim | — | Slug do documento (2º segmento da URL) |
| `publication_date` | timestamp(0) | sim | — | Data de publicação (filtro e cutoff) |
| `order` | integer | sim | — | — |
| `category_contents_id` | integer | **not null** | — | **FK → `category_contents.id`** |
| `created_at` / `updated_at` / `deleted_at` | timestamp(0) | sim | — | `SoftDeletes` |

Restrições relevantes:

- `contents_category_contents_id_foreign` → `category_contents(id)`.
- `contents` é referenciada por `archives.contents_id`.

Modelo Eloquent: `v6/app/Http/Model/Prefecture/Content.php` (namespace
`App\Prefecture`). Usa `SoftDeletes` e `PresentableTrait`; `$fillable` inclui
`title`, `text`, `clicks`, `highlights`, `order`, `galery`,
`category_contents_id`, `publication_date`, `slug`, `status`, entre outros;
`$casts = ['publication_date' => 'datetime']`; relação
`categoryContents()` (linha 107).

## 3. `category_contents` — categorias do comitê

| Coluna | Tipo | Nullable | Default | Uso no módulo |
|---|---|---|---|---|
| `id` | integer | not null | seq | PK |
| `name` | varchar(191) | **not null** | — | Nome da categoria (exibido no menu lateral) |
| `slug` | varchar(191) | **not null** | — | `comite-de-investimentos` na categoria-pai; slug da subcategoria na URL |
| `status_create` | boolean | sim | — | — |
| `quantidade_itens` | integer | sim | — | — |
| `order` | integer | sim | — | Ordenação das subcategorias |
| `layout` | integer | sim | — | — |
| `category_contents_id` | integer | sim | — | **Auto FK**: `NULL` = categoria-pai; preenchido = subcategoria do comitê |
| `status` | boolean | sim | `true` | — |
| `display_type` | varchar(191) | sim | — | — |
| `custom_icon` | varchar(191) | sim | — | — |
| `description` | text | sim | — | — |
| `created_at` / `updated_at` / `deleted_at` | timestamp(0) | sim | — | `SoftDeletes` |

Modelo Eloquent: `v6/app/Http/Model/Prefecture/CategoryContent.php`
(namespace `App\Prefecture`). Auto-relacionamento `categoryContents()`
(`hasMany(CategoryContent::class, 'id', 'category_contents_id')`).

Estado no banco de desenvolvimento (`prefeituras_v5`):

| id | slug | name | category_contents_id | order |
|---|---|---|---|---|
| 39 | `comite-de-investimentos` | Comitê de Investimentos | (nulo) | (nulo) |

Nenhuma subcategoria e nenhum conteúdo existem no banco de desenvolvimento
(`contents` do comitê = 0 linhas). A categoria-pai id 39 **não** tem migration
própria: foi criada manualmente pelo operador (ou por seed não versionado)
— **[NÃO VERIFICADO]** qual script a popula.

## 4. `archives` — anexos das atas

Colunas usadas pelo módulo (schema real):

| Coluna | Tipo | Uso |
|---|---|---|
| `id` | integer (not null) | PK |
| `contents_id` | integer | FK → `contents.id` |
| `title` | varchar(300) | Nome do anexo |
| `path` / `name` / `hash` | varchar(300) | Arquivo no storage |
| `extension` | varchar(191) | Extensão |
| `type` | varchar(191) | MIME |
| `highlight` | boolean | `NULL` = anexo normal; `1` = anexo de destaque |
| `view_pdf` | boolean | `true` = documento embutido via iframe |
| `order` | integer | Ordenação |
| `deleted_at` | timestamp(0) | `SoftDeletes` |

No controller portal, `view()` carrega:

```php
$archives = Archive::where('contents_id', $content->id)->where('highlight', null)->get();
$view_pdf = Archive::where('contents_id', $content->id)->where('view_pdf', true)->first();
```

A view portal `v6/resources/views/portal/layouts/modelo1/comite-de-investimentos/view.blade.php`
inclui `portal.components.view_pdf.files-list` quando
`count($archives) > 0 || $view_pdf != null`.

## 5. View SQL `comite_de_investimentos`

### 5.1 Definição inicial (outubro de 2024)

`v6/database/migrations/2024_10_16_011540_create_view_comite_de_investimentos.php`:

```sql
CREATE OR REPLACE VIEW comite_de_investimentos
as select
    row_number() OVER (ORDER BY c.publication_date asc) as value,
    c.title as titulo,
    to_char(c.publication_date::timestamp with time zone, 'DD/MM/YYYY'::text) AS data_publicacao,
    cc."name" as categoria,
    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 = (
        SELECT id FROM category_contents WHERE slug = 'comite-de-investimentos'
      )
ORDER BY c.publication_date desc;
```

Pontos técnicos:

- A view seleciona apenas conteúdos cuja **subcategoria** tem
  `category_contents_id` = id da categoria-pai `comite-de-investimentos`.
  Como o filtro é `cc.category_contents_id = (subselect)`, **a própria
  categoria-pai não aparece** — correto.
- O `row_number()` numera por `publication_date ASC`, mas o `ORDER BY` final é
  `publication_date DESC` — a numeração e a ordenação exibida não coincidem.
- `to_char(publication_date, 'DD/MM/YYYY')` devolve **texto**, não data.
- A view **não** filtra `publication_date <= now()`: atas futuras aparecem no
  DataTable do CMS (comportamento aceitável no CMS, incoerente com o portal).

### 5.2 Alteração (novembro de 2024)

`v6/database/migrations/2024_11_29_103842_update_view_comite_de_investimentos.php`
faz `DROP VIEW` e recria a view acrescentando **duas colunas**:

| Coluna adicionada | Origem | Finalidade |
|---|---|---|
| `categoria_slug` | `cc.slug` | Montar a URL `/comite-de-investimentos/{categoria_slug}/{slug}` no botão "Acessar" |
| `slug` | `c.slug` | Montar a mesma URL |

O `down()` da migration restaura exatamente a definição anterior (sem
`categoria_slug` e sem `slug`). O `up()` executa `DROP VIEW if exists` **antes**
do `CREATE OR REPLACE`, o que é redundante no PostgreSQL (o `CREATE OR REPLACE`
já substitui) e destrói as permissões/dependências da view se existirem.

### 5.3 Schema real da view no banco

| Coluna | Tipo | Origem |
|---|---|---|
| `value` | bigint | `row_number()` |
| `titulo` | varchar(191) | `c.title` |
| `data_publicacao` | text | `to_char(publication_date)` |
| `categoria` | varchar(191) | `cc.name` |
| `categoria_slug` | varchar(191) | `cc.slug` |
| `slug` | varchar(191) | `c.slug` |
| `id` | integer | `c.id` |

### 5.4 Consumo da view

Apenas em `v6/app/Http/Controllers/Prefecture/DatatableController.php:40-44`:

```php
public function get_comite_de_investimentos()
{
    $comite = DB::table('comite_de_investimentos')->get();
    return DataTables::of($this->struct_views($comite, 'comite_de_investimentos'))->make(true);
}
```

`struct_views()` (linha 504) injeta em cada linha as flags `edit`, `delete`,
`active` e `inactivate` conforme `has_access('comite_de_investimentos.edit')` e
afins. O JS `v6/public/assets/js/custom/contents/comite-de-investimentos/datatables.js`
usa `titulo`, `categoria`, `data_publicacao`, `id`, `categoria_slug` e `slug`.

**A view não é usada pelo portal público** — o portal consulta `contents` via
Eloquent.

## 6. `modulos`, `menu_orders` e `resources`

| Tabela | Colunas relevantes | Registro do comitê |
|---|---|---|
| `modulos` | `id`, `name`, `slug`, `status` | id 48, `Comitê de Investimentos`, `comite-de-investimentos`, `status = 0` (desativado) |
| `menu_orders` | `id`, `slug`, `clicks` (not null), `modulo_id` | id 31, `comite-de-investimentos`, `clicks = 0`, `modulo_id = 48` |
| `resources` | `id`, `slug`, `name`, `modulo_id` | id 68, `comite-de-investimentos`, `modulo_id = 48` |

O `status` do módulo controla a exibição do item no menu do CMS
(`v6/app/Providers/MenuServiceProvider.php:46`):

```php
if (has_access("$menu->slug.index") && isset($menu->modulo->status) && $menu->modulo->status) {
```

O `clicks` de `menu_orders` é incrementado por `menuClicks($slug)`
(`v6/app/Helpers/functions.php:865`) a cada visita ao index do comitê no CMS.

## 7. Migrations relacionadas

| Data | Arquivo | Efeito |
|---|---|---|
| 2024-10-16 | `v6/database/migrations/2024_10_16_011540_create_view_comite_de_investimentos.php` | Cria a view `comite_de_investimentos` |
| 2024-11-01 | `v6/database/migrations/2024_11_01_150206_create_modulos_table.php` | Cria `modulos` e insere o registro id 48 (`status = 0`) |
| 2024-11-29 | `v6/database/migrations/2024_11_29_103842_update_view_comite_de_investimentos.php` | Recria a view com `categoria_slug` e `slug` |

Não há migration que crie a linha de `category_contents` com slug
`comite-de-investimentos` (id 39 no banco de dev) nem seeds para o módulo.

## 8. Diagrama de relacionamentos

```mermaid
erDiagram
    modulos ||--o{ menu_orders : "modulo_id"
    modulos ||--o{ resources : "modulo_id"
    category_contents ||--o{ category_contents : "category_contents_id (auto FK)"
    category_contents ||--o{ contents : "category_contents_id"
    contents ||--o{ archives : "contents_id"
    category_contents {
        int id PK
        string name
        string slug
        int order
        int category_contents_id FK "NULL = categoria-pai"
    }
    contents {
        int id PK
        string title
        text text
        string slug
        int clicks
        timestamp publication_date
        int category_contents_id FK
        timestamp deleted_at
    }
    archives {
        int id PK
        int contents_id FK
        boolean highlight
        boolean view_pdf
    }
```
