# Requerimentos — Modelagem de Dados

> **Versão analisada:** `v6` — módulo **novo na v6** (sem tabelas, migrations,
> models ou views equivalentes na `v5`)
> **Slug:** `requerimentos` (`modulos.id = 41`)

---

## 1. Visão geral

O módulo **não possui tabela própria**. Tudo é reaproveitado de três tabelas
genéricas da plataforma, mais uma **view SQL** criada por migrations:

| Objeto | Natureza | Papel no módulo |
|---|---|---|
| `contents` | tabela | Armazena os requerimentos |
| `category_contents` | tabela (árvore) | Raiz `requerimentos` (id 37) e subcategorias |
| `councilmen` | tabela | Vereadores; origem de `contents.councilmen_id` |
| `archives` | tabela | Anexos do requerimento |
| `requerimentos` | **view SQL** | Listagem tabular usada pelo DataTable do CMS |
| `modulos` | tabela | Módulo (id 41), ligado/desligado em `/cms/modulos` |
| `menu_orders` | tabela | Telemetria de clique do menu do CMS |

## 2. `contents` — tabela dos requerimentos

Schema real (verificado em `psql \d contents`):

| Coluna | Tipo | Null | Observação |
|---|---|---|---|
| `id` | `integer` | não | PK, `contents_id_seq` |
| `title` | `varchar(191)` | sim | Título do requerimento (máx. 120 na validação) |
| `text` | `text` | sim | Texto integral (HTML do Summernote) |
| `clicks` | `integer` | sim | Contador de visualizações no portal |
| `slug` | `varchar(191)` | sim | URL do requerimento (`/requerimentos/{cat}/{slug}`) |
| `publication_date` | `timestamp(0)` | sim | Data/hora de publicação; usado para os filtros e para esconder futuros |
| `category_contents_id` | `integer` | **não** | FK → `category_contents.id` |
| `councilmen_id` | `integer` | sim | FK → `councilmen.id` **ON DELETE SET NULL** |
| `status` | `boolean` | sim | Publicação genérica do CMS |
| `order` | `integer` | sim | Ordem dentro da categoria |
| `highlights`, `galery`, `carousel` | `boolean` | sim | Flags genéricas de conteúdo |
| `source`, `author`, `icon`, `secretaria`, `acronym` | `varchar(191)` | sim | Campos genéricos não usados por este módulo |
| `orgaos_id` | `integer` | sim | FK → `contents.id` (genérica) |
| `position_id` | `bigint` | sim | Posição genérica |
| `layout` | `integer` | sim | Layout genérico |
| `linha_fina` | `varchar(300)` | sim | Texto de apoio (adicionado em 2026) |
| `referencia` | `varchar(50)` | sim | Referência (adicionado em 2026) |
| `created_at`, `updated_at`, `deleted_at` | `timestamp(0)` | sim | Timestamps + soft delete |

PK: `contents_pkey`. FKs: `contents_category_contents_id_foreign`,
`contents_councilmen_id_foreign` (ON DELETE SET NULL), `contents_orgaos_id_foreign`.

> A coluna `councilmen_id` foi adicionada **exclusivamente para este módulo** em
> `v6/database/migrations/2026_06_12_100000_add_councilmen_id_to_contents_table.php`.

## 3. `category_contents` — árvore de categorias

| Coluna | Tipo | Null | Observação |
|---|---|---|---|
| `id` | `integer` | não | PK |
| `name` | `varchar(191)` | **não** | Nome exibido (título da aba, coluna `categoria`) |
| `slug` | `varchar(191)` | **não** | Identificador da categoria na URL |
| `status_create` | `boolean` | sim | Marca categorias sem formulário próprio |
| `quantidade_itens` | `integer` | sim | Genérico |
| `order` | `integer` | sim | Ordena as subcategorias do menu lateral |
| `layout` | `integer` | sim | Genérico |
| `status` | `boolean` | sim | Default `true` |
| `display_type`, `custom_icon` | `varchar(191)` | sim | Genéricos (2026) |
| `description` | `text` | sim | Genérico (2026) |
| `category_contents_id` | `integer` | sim | **Auto-referência**: o pai. `NULL` = raiz |
| `created_at`, `updated_at`, `deleted_at` | `timestamp(0)` | sim | Timestamps + soft delete |

- FK: `category_contents_category_contents_id_foreign` → `category_contents.id`.
- A raiz do módulo é a categoria com `slug = 'requerimentos'`
  (`id = 37`, `name = 'Requerimentos'`, `category_contents_id IS NULL`),
  criada por `v6/database/seeds/CategoryContentTableSeeder.php:206-209`.
- As **subcategorias** são linhas com `category_contents_id = 37`. Elas são
  criadas pelo operador no CMS (`/cms/categorias/cadastrar/requerimentos`).
- Uma subcategoria pode ter filhas (a view SQL vigente usa `WITH RECURSIVE`),
  mas a listagem do **portal** considera apenas as filhas **diretas** da raiz
  (`whereHas('categoryContents', ...)`), enquanto a **view SQL** considera toda
  a árvore. Ver `08-GAPS-E-DIVIDAS.md`.

## 4. `councilmen` — o requerente

| Coluna | Tipo | Null | Observação |
|---|---|---|---|
| `id` | `integer` | não | PK, referenciado por `contents.councilmen_id` |
| `name` | `varchar(191)` | **não** | Nome do vereador → coluna `requerente` da view |
| `slug` | `varchar(191)` | **não** | Slug do vereador |
| `party` | `varchar(191)` | **não** | Partido |
| `text` | `text` | sim | Biografia |
| `archives_id` | `integer` | sim | FK → `archives.id` (foto) |
| `status` | `boolean` | **não** | `true` = mandato ativo; só ativos são listados no formulário |
| `created_at`, `updated_at`, `deleted_at` | `timestamp(0)` | sim | Timestamps + soft delete |

- Tabela criada em `v6/database/migrations/2024_09_11_104741_create_councilmen_table.php`.
- Model: `v6/app/Http/Model/Prefecture/CityCouncilor.php` (`protected $table = 'councilmen'`).
- A view SQL usa `LEFT JOIN councilmen cm ON cm.id = c.councilmen_id AND cm.deleted_at IS NULL`:
 mantém o vínculo, mas **apaga** o nome se o vereador foi removido logicamente.

## 5. A view SQL `requerimentos`

### 5.1 Colunas (verificado em `psql \d requerimentos`)

| Coluna | Tipo | Origem | Descrição |
|---|---|---|---|
| `value` | `bigint` | `row_number() OVER (ORDER BY c.created_at)` | Número sequencial (instável) |
| `titulo` | `varchar(191)` | `c.title` | Título do requerimento |
| `data_publicacao` | `text` | `to_char(c.publication_date, 'DD/MM/YYYY')` | Data formatada (string) |
| `categoria` | `varchar(191)` | `cc.name` | Nome da categoria |
| `requerente` | `varchar(191)` | `cm.name` | Nome do vereador (pode ser `NULL`) |
| `id` | `integer` | `c.id` | Id do conteúdo (usado pelas ações do DataTable) |

### 5.2 As quatro migrations da view, em ordem cronológica

| # | Migration | O que mudou | Situação |
|---|---|---|---|
| 1 | `v6/database/migrations/2024_10_08_005745_create_view_requerimentos.php` | Primeira criação: `CREATE OR REPLACE VIEW requerimentos`, join `contents` × `category_contents` e filtro `cc.category_contents_id = (raiz 'requerimentos')` — ou seja, **apenas as filhas diretas**. Sem `requerente` | superseded |
| 2 | `v6/database/migrations/2025_08_12_112801_recreate_view_requerimentos.php` | Recria com `IN (SELECT ... WHERE category_contents_id = (raiz))` — mesma semântica (filhas diretas), mas com `CREATE OR REPLACE` explícito e ordenação `ORDER BY c.created_at DESC` | superseded |
| 3 | `v6/database/migrations/2026_06_09_110000_fix_view_requerimentos_recursive.php` | Introduz `WITH RECURSIVE cat_tree` (sobe a partir da raiz e desce a árvore), excluindo a própria raiz do resultado. `down()` volta à versão 2 | superseded |
| 4 | `v6/database/migrations/2026_06_12_100100_add_requerente_to_view_requerimentos.php` | **Versão vigente**: `DROP VIEW IF EXISTS` + `CREATE VIEW` com `WITH RECURSIVE cat_tree`, `LEFT JOIN councilmen cm` (com `cm.deleted_at IS NULL`) e a coluna `requerente`. O `down()` recria a view sem `requerente` | **VIGENTE** |

### 5.3 SQL vigente (copiado de `v6/database/migrations/2026_06_12_100100_add_requerente_to_view_requerimentos.php`)

```sql
CREATE VIEW public.requerimentos AS
WITH RECURSIVE cat_tree AS (
    SELECT id
    FROM category_contents
    WHERE slug = 'requerimentos'
      AND category_contents_id IS NULL
    UNION ALL
    SELECT cc.id
    FROM category_contents cc
    JOIN cat_tree ct ON cc.category_contents_id = ct.id
)
SELECT
    row_number() OVER (ORDER BY c.created_at) AS value,
    c.title AS titulo,
    to_char(c.publication_date::timestamp with time zone, 'DD/MM/YYYY') AS data_publicacao,
    cc.name AS categoria,
    cm.name AS requerente,
    c.id
FROM contents c
JOIN category_contents cc ON cc.id = c.category_contents_id
LEFT JOIN councilmen cm ON cm.id = c.councilmen_id AND cm.deleted_at IS NULL
WHERE c.deleted_at IS NULL
  AND c.category_contents_id IN (
      SELECT id FROM cat_tree
      WHERE id NOT IN (
          SELECT id FROM category_contents
          WHERE slug = 'requerimentos' AND category_contents_id IS NULL
      )
  )
ORDER BY c.created_at DESC
```

### 5.4 Regras implícitas da view

- A raiz `requerimentos` é **excluída** do resultado (`id NOT IN (...)`): apenas
  as descendentes aparecem.
- A view **não** filtra `publication_date`: conteúdo futuro aparece no CMS e
  **não** aparece no portal — divergência de comportamento entre os dois.
- A view **não** filtra `status`: o CMS lista inclusive rascunhos.
- `row_number()` reordena a cada consulta; a coluna `value` não é uma chave
  estável (não usar em joins ou em persistência).

## 6. Diagrama de relacionamentos

```
              category_contents (raiz id=37, slug='requerimentos')
                    │  ▲
      category_contents_id │ (auto-referência: subcategorias)
                    │  │
                    ▼  │
              contents ────────────────┐
              id, title, text,         │
              publication_date,        │
              slug, clicks,            │
              category_contents_id ────┘
              councilmen_id (FK, SET NULL)
                    │
                    ▼
              councilmen (id, name, party, status, deleted_at)
                    ▲
                    │ archives_id
              archives ─── contents_id (FK)
              (path, extension, view_pdf, highlight)
```

```
  modulos (id=41, slug='requerimentos', status=false)
        ▲
        │ modulo_id
  menu_orders (id=35, slug='requerimentos', clicks)
```

## 7. Seeds e dados iniciais

| Arquivo | O que faz |
|---|---|
| `v6/database/seeds/CategoryContentTableSeeder.php:206-209` | Cria a categoria raiz `requerimentos` (`status_create = 1`) — comentário no seed indica “ID 37” |
| `v6/database/seeds/ResourcesTableSeeder.php:495-502` | Cria o `Resource` “Requerimentos” com `modulo_id = 41` |
| `v6/database/seeds/MenuOrderSeeder.php:276` | Registra o item de menu `requerimentos` (telemetria de cliques) |
| `v6/database/seeds/MenuItemsTableSeeder.php:386-394` | Cria o item de menu do portal “Requerimentos” (`order = 27`, `type = 2`, `category_contents_id = <raiz>`) |

> Nenhum conteúdo de requerimento é populado por seed — a base de produção
> inicia a categoria raiz **sem subcategorias**, o que faz o `index()` do portal
> cair no fallback para a própria raiz.

## 8. Módulos e menu (estado verificado no banco)

| Tabela | Registro |
|---|---|
| `modulos` | `id = 41`, `name = 'Requerimentos'`, `slug = 'requerimentos'`, `status = false` (desligado) |
| `menu_orders` | `id = 35`, `slug = 'requerimentos'`, `modulo_id = 41`, `clicks = 0` |
| `category_contents` | `id = 37`, `name = 'Requerimentos'`, `slug = 'requerimentos'`, `category_contents_id = NULL`, `status = true` |

O liga/desliga de módulos acontece em `v6/app/Http/Controllers/Admin/ModulosController.php`
(rota `/cms/modulos`, `v6/routes/basepro.php:40-41`), que atualiza
`modulos.status` e limpa o cache `Cache::pull('modulos')`.
