# Pauta do Dia — Regras de Negócio e Portabilidade

> **Slug:** `pauta-do-dia` · **Módulo:** `modulos.id = 42` · **Novo na v6**
> Objetivo: registrar as regras que **não são óbvias** pelo código e as
> armadilhas que impedem a portabilidade direta do módulo para a v7.

---

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

| # | Regra | Onde está implementada | Efeito para o usuário |
|---|---|---|---|
| R1 | Só existem pautas na **categoria raiz** `category_contents.slug = 'pauta-do-dia'` (a linha com `category_contents_id IS NULL`). | `PautaDoDiaController@index` (`whereNull`), view SQL `pauta_do_dia` (subquery escalar), `ContentRequest` (resolução do slug). | Pauta em subcategoria não aparece no portal nem no CMS. |
| R2 | A listagem pública ordena por **dia** da `publication_date` (decrescente). | `orderByRaw("publication_date::date DESC, ...")` em `v6/app/Http/Controllers/Portal/PautaDoDiaController.php:20`. | A pauta mais recente no topo. |
| R3 | Dentro do mesmo dia, a ordem é definida pelo **número de sessão** extraído do **texto do título**, em ordem **decrescente** (19 antes de 18). | `regexp_replace(title, '[^0-9]', '', 'g')` + `::numeric DESC` (portal) e `converterDataPauta()` (CMS). | A pauta "19ª Sessão" aparece antes da "18ª Sessão" do mesmo dia. |
| R4 | Desempate final: `id` decrescente (portal) / id com zero à esquerda (CMS). | Mesmos lugares de R3. | Ordem estável entre pautas com o mesmo número. |
| R5 | A listagem pública é paginada com **15 itens** (padrão do Laravel 5.5, pois `paginate()` é chamado sem argumento). | `->paginate()` em `PautaDoDiaController@index`. | Link "Anterior/Próxima" renderizado por `$pautas->links()`. |
| R6 | O detalhe público exige o **slug** e a **categoria**: uma URL de slug válido de outra categoria devolve 404. | `whereHas('categoryContents', ...)->where('slug',$slug)->firstOrFail()`. | 404 amigável. |
| R7 | O portal exibe **data e hora** no detalhe, mas **apenas a data** na listagem. | `index.blade.php` (`date('d/m/Y')`) e `show.blade.php` (`date('d/m/Y')` + `date('H:i')`). | Diferença de granularidade entre as duas telas. |
| R8 | No detalhe, anexos com `highlight = 1` são a **imagem de destaque**; os demais (`highlight IS NULL`) são "Arquivos para Download"; um arquivo com `view_pdf = true` é **embutido** em `iframe` (somente desktop). | `PautaDoDiaController@show` + `v6/resources/views/portal/components/view_pdf/files-list.blade.php`. | O PDF aparece dentro da página; no celular não é embutido. |
| R9 | O **corpo do texto** é renderizado sem escaping: `{!! $pauta->text !!}`. | `show.blade.php`. | HTML do Summernote é executado no navegador. |
| R10 | O **slug** é gerado a partir do título no cadastro (`generateSlug`/`slugfy`) e, havendo colisão, recebe sufixo (`-{maxid}` no cadastro, `pub{id}-` ou `-{id}` na edição). | `ContentController@store` e `@update`. | URL pública permanece previsível. |
| R11 | A edição só permite alterar o `slug` quando a categoria é a raiz (`$categoria_pai == 1` no código; na prática, a condição é o id do **conteúdo-pai** da categoria). | `ContentController@update`. | Regra copiada de outros módulos de conteúdo. |
| R12 | Toda abertura da tela do CMS incrementa `menu_orders.clicks` para o slug `pauta-do-dia`. | `menuClicks($slug)` em `v6/app/Helpers/functions.php:865`, chamado por `returnIndex`/`returnData`. | Métrica de uso do item de menu. |
| R13 | O módulo é **desligado por padrão** (`modulos.status = 0` no seed da migration `2024_11_01_150206_create_modulos_table.php`). | `v6/database/migrations/2024_11_01_150206_create_modulos_table.php:277` | O item de menu do CMS só aparece após o operador habilitar o módulo em `/cms/modulos`. |
| R14 | A listagem do CMS mostra, na coluna "ID", o **`row_number()`** da view, não o `contents.id`. | `v6/database/migrations/2024_10_13_010549_create_view_pauta_do_dia.php` | O número exibido muda conforme a data de publicação e não identifica o conteúdo. |
| R15 | A coluna "Data e Hora" do CMS só pode ser ordenada pela **chave calculada em JavaScript**; o texto exibido ("DD/MM/YYYY às HH:MI") não é ordenável. | `converterDataPauta()` em `v6/public/assets/js/custom/contents/pauta-do-dia/datatables.js` | Ordenar por "Título" ordena alfabeticamente, ignorando o número da sessão. |

## 2. A extração do número de sessão é frágil

O número da sessão **não existe como dado**: ele é deduzido do texto livre
digitado pelo operador no campo "Título".

| Título digitado | `regexp_replace(title,'[^0-9]','','g')` | Valor numérico | Efeito |
|---|---|---|---|
| `19ª Sessão` | `19` | 19 | Correto. |
| `Sessão 19ª` | `19` | 19 | Correto. |
| `19ª Sessão 2026` | `192026` | 192026 | **Errado**: o ano entra no número e domina a ordenação. |
| `Pauta da 2ª Sessão — ordinária` | `2` | 2 | Correto. |
| `Sessão 19ª/2025` | `192025` | 192025 | **Errado** (mesma razão). |
| `Reunião` | `` (vazio) | 0 (`COALESCE`/`NULLIF` no SQL; `'0'` no JS) | Vai para o fim do grupo do dia. |
| `Pauta 7 e 8` | `78` | 78 | **Errado**: dois números concatenados. |

Ou seja: o operador tem de **padronizar o título** para que a ordenação
funcione. Não há validação, dica na tela nem documentação na view do CMS
sobre esse requisito. A duplicação em PHP e JS não elimina o problema: as
duas implementações extraem os mesmos dígitos e cometem o mesmo erro.

## 3. Armadilhas de SQL e portabilidade

### 3.1 `orderByRaw` exclusivo do PostgreSQL

```php
->orderByRaw("publication_date::date DESC, COALESCE(NULLIF(regexp_replace(title, '[^0-9]', '', 'g'), ''), '0')::numeric DESC, id DESC")
```

| Construto | PostgreSQL | MySQL | SQLite | Consequência na v7 |
|---|---|---|---|---|
| `publication_date::date` | ok | não existe | não existe | Quebra em MySQL/SQLite; usar `DATE(publication_date)`. |
| `regexp_replace(txt, '[^0-9]', '', 'g')` | ok | `REGEXP_REPLACE` só no MySQL 8+ (sintaxe e flag `g` diferentes) | função ausente | Portabilidade exige reescrita. |
| `NULLIF(x, '')` | ok | ok | ok | — |
| `COALESCE(...)` | ok | ok | ok | — |
| `::numeric` | ok | não | não | Cast explícito do PostgreSQL. |

Além disso, o `orderByRaw` **interpolia texto fixo** (não há acesso a input
do usuário), o que é seguro, mas impede reaproveitar a consulta em outros
motores sem reescrita.

### 3.2 A view SQL é um objeto de banco

- `v6/database/migrations/2024_10_13_010549_create_view_pauta_do_dia.php`
  usa `DB::statement("CREATE OR REPLACE VIEW ...")` — **sintaxe PostgreSQL**
  (`CREATE OR REPLACE VIEW` também existe no MySQL, mas `to_char` e o cast
  `::timestamp with time zone` não).
- `to_char(c.publication_date::timestamp with time zone, 'DD/MM/YYYY às HH24:MI'::text)`
  devolve **texto** em português; qualquer consumidor que precise comparar
  datas terá de convertê-lo de volta.
- A view é consultada por `DB::table('pauta_do_dia')` — o nome da view está
  **hardcoded** no controller; renomear a view exige alterar código e
  migration.

### 3.3 `paginate()` sem argumento

`->paginate()` usa o padrão do framework (15 itens na v6). Não há constante
nem configuração de módulo; alterar o número de itens por página exige
mudar o código do controller (e o `links()` da view continua funcionando).

### 3.4 Carga total da listagem do CMS

`DB::table("pauta_do_dia")->get()` traz **todas** as pautas para a memória do
PHP e a paginação é feita pelo Yajra em memória. Com volumes grandes de
pauta (municípios com muitos anos de sessões), isso é um risco de memória e
de tempo de resposta. O `row_number()` da view também é recalculado sobre o
conjunto inteiro.

### 3.5 Duplicação da lógica de ordenação

- PHP: `v6/app/Http/Controllers/Portal/PautaDoDiaController.php:20`.
- JS: `converterDataPauta()` em
  `v6/public/assets/js/custom/contents/pauta-do-dia/datatables.js`.

As duas implementações não compartilham código nem teste. Divergências
possíveis já mapeadas: o SQL ordena pelo `::date` (ignora a hora) enquanto a
chave JS ordena por `AAAAMMDD` (também ignora a hora) — nesse ponto concordam;
mas o SQL usa comparação **numérica** e o JS usa comparação **lexical** sobre
valores com zero à esquerda (equivalentes até 10 dígitos; a partir de 11
dígitos, ambos mantêm a ordem numérica, porém por caminhos diferentes).

### 3.6 Dependência do texto digitado pelo operador

Como visto na seção 2, a ordenação depende de o operador escrever o número
da sessão no título, no formato esperado. A modernização deve eliminar essa
dependência (ver `06-MIGRACAO-MODERNA.md`).

### 3.7 Ausência de filtro de publicação (`status`)

Nem o controller do portal nem a view SQL filtram `contents.status`. Uma
pauta cadastrada e mantida como "não publicada" continua visível ao cidadão.
O formulário do CMS nem sequer expõe o campo `status` (o
`v6/resources/views/prefecture/contents/pauta-do-dia/form.blade.php` não tem
o campo), mas `ContentController@store` grava `status` a partir do request
(vazio neste formulário) — logo, na prática, o conteúdo nasce com `status`
nulo/falso **e mesmo assim aparece no portal**.

### 3.8 Outras armadilhas

| Armadilha | Onde | Observação |
|---|---|---|
| `str_limit` e `strip_tags`/`stripslashes` no resumo | `index.blade.php` | `str_limit` é helper global do Laravel 5.5, removido nas versões modernas (`Str::limit`). |
| `file_exist($archive->path)` com caminho relativo | `files-list.blade.php` | A verificação depende do diretório de trabalho do PHP; `public/` relativo. |
| `$pauta->archives` lazy loading | `show.blade.php` | Consulta extra por requisição (N+1 potencial em laços). |
| `where('highlight', null)` | `PautaDoDiaController@show` | O Eloquent converte em `IS NULL`; arquivos com `highlight = false` ficariam de fora (a coluna é booleana com `NULL` no seed). |
| `Archive::where(...)->where('view_pdf', true)->first()` | `PautaDoDiaController@show` | Apenas **um** PDF é exibido; os demais viram download (ou são omitidos no desktop, conforme o filtro do componente). |
| HTML sem escaping | `show.blade.php` (`{!! !!}`) | Depende da confiança no operador do CMS. |
| `menuClicks()` no `returnData` | `ContentController@returnData` | Cada abertura do formulário também conta um clique. |
| Slug derivado do título | `ContentController@store` | Títulos com acentos ou símbolos geram slugs inesperados; colisões recebem sufixo numérico. |
| Dependência de CDN/serviço externo (AddThis) | `show.blade.php` | A rede social AddThis foi descontinuada; o botão pode não funcionar. |
| `check.site.manutention` | grupo de `v6/routes/web.php` | Se a prefeitura colocar o portal em manutenção, `/pauta-do-dia` responde conforme o middleware. |

## 4. Acoplamentos

### 4.1 Dependências (o módulo precisa)

| Dependência | Tipo | Observação |
|---|---|---|
| `contents` + `category_contents` + `archives` | dados | Núcleo de conteúdo do CMS; compartilhado com Notícias, Requerimentos, Sessões, Transmissões, Atas, Publicações, Legislação, Banners, etc. |
| `ContentController` (CRUD genérico) | código | Não há CRUD próprio: qualquer mudança no genérico atinge o módulo. |
| `ContentRequest` | código | Validação por `switch` no slug. |
| `MenuServiceProvider` | código | Item de menu do CMS. |
| `MenuItemsTableSeeder` | dado | Item de menu do portal. |
| `check.route.permission` + `has_access()` | segurança | Autorização por slug de permissão. |
| PostgreSQL | infra | View e `orderByRaw`. |

### 4.2 Dependências do módulo em outros módulos

| Consumidor | Tipo | Observação |
|---|---|---|
| Menu público (`menu_items`) | menu | Exposição no portal. |
| `menu_orders.clicks` | telemetria | Métrica de uso. |
| Tabela de atividades | auditoria | Registro de criação/edição/exclusão. |
| Módulo `sessoes` | conceitual | A pauta descreve a sessão, mas **não há FK** entre `contents` de pauta e as sessões. |
| Módulo `atas-e-pontos` | conceitual | Mesma sessão, sem vínculo de dados. |

## 5. Onde a v6 divergiu da v5

O módulo **não existe na v5**. A verificação (`grep -riE "pauta" v5/routes/
v5/app/Http/Controllers/`) não retorna nenhum resultado: não há rota, model,
migration, view, seed ou referência a "pauta" na versão anterior. O mapa
completo v5 → v6 está em `08-GAPS-E-DIVIDAS.md`.

Consequências práticas da novidade na v6:

1. Não há continuidade de dados nem migração a fazer a partir da v5.
2. Não há legado de interface a preservar: o desenho do formulário e das
   telas é livre (o que abre espaço para corrigir os problemas listados
   acima).
3. O módulo depende de peças da v6 que também são novas ou foram remodeladas
   nesse ciclo: `modulos` (tabela de habilitação), `resources` (funcionalidade
   por módulo), `menu_orders` (telemetria) e o CRUD genérico de `contents`
   com categorias.

## 6. Checklist de portabilidade para a v7

- [ ] Substituir `orderByRaw` com SQL PostgreSQL por ordenação em Eloquent
      (`orderByDesc(session_date)->orderByDesc(session_number)->orderByDesc(id)`).
- [ ] Eliminar a extração de dígitos do título (campo `session_number`).
- [ ] Substituir a view `pauta_do_dia` por query Eloquent (ou por uma view
      portável, gerada por migration do schema da v7).
- [ ] Definir o `perPage` explicitamente em vez de depender do padrão.
- [ ] Eliminar a duplicação PHP/JS da ordenação (ordenar no servidor).
- [ ] Filtrar por status de publicação na listagem pública.
- [ ] Trocar `str_limit` por `Str::limit` e `file_exist` por `Storage::exists`.
- [ ] Substituir o AddThis por uma solução de compartilhamento mantida.
- [ ] Definir o que é a "coluna ID" do CMS (id real do conteúdo).
