# Atas e Pontos de Frequência — Regras e Portabilidade

> Versão analisada: `v6`. Módulo **novo na v6**. Slug `modulos`:
> `atas-e-pontos-de-frequencia` (id 46).

---

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

| # | Regra | Onde está implementada |
|---|---|---|
| R1 | O módulo é identificado pela **categoria raiz** `atas-e-pontos`; o slug `atas-e-pontos-de-frequencia` da tabela `modulos` é apenas o registro administrativo do módulo. | `v6/app/Http/Controllers/Portal/AtasEPontosController.php`, `v6/app/Http/Controllers/Prefecture/ContentController.php:116` |
| R2 | Só existem **categorias filhas** (`atas`, `pontos-de-frequencia` e outras criadas pelo operador); o conteúdo é sempre vinculado a uma filha. | `v6/database/seeds/CategoryContentTableSeeder.php:227-241` |
| R3 | `/atas-e-pontos` redireciona para a **primeira** categoria filha ordenada por `order`. | `AtasEPontosController@getIndex` |
| R4 | Slug de categoria inexistente redireciona para `/atas-e-pontos`. | `AtasEPontosController@show` |
| R5 | Conteúdo com `publication_date` futura **não** aparece na listagem nem pode ser aberto (redireciona). | `AtasEPontosController@show` e `@view` |
| R6 | A listagem é paginada em **12 itens**, ordenada por `publication_date` desc e `id` desc. | `AtasEPontosController@show` |
| R7 | Filtros disponíveis: **ano**, **mês** e **texto livre** (case/acento-insensitive sobre `title` e `text`). | `AtasEPontosController@show` |
| R8 | O filtro de mês só é populado quando há um ano selecionado. | `AtasEPontosController@show` (bloco `$request->ano`) |
| R9 | A busca por texto usa `clean_slug()` antes de comparar, o que normaliza acentuação e espaços. | `v6/app/Helpers/functions.php:823` |
| R10 | Cada abertura do documento incrementa `contents.clicks`. | `AtasEPontosController@view` |
| R11 | O anexo com `view_pdf = true` é exibido embutido (iframe ou imagem); os demais vão para a lista de download. | `v6/resources/views/portal/components/view_pdf/files-list.blade.php` |
| R12 | Anexo com `highlight = true` é a peça de destaque e **não** entra na lista de downloads. | `AtasEPontosController@view` + componente `files-list` |
| R13 | Extensões aceitas para download: `rar, zip, doc, docx, pdf, odt, xls, xlsx, xlsb, xslm, ppt, pptx`. | `v6/resources/views/portal/components/view_pdf/files-list.blade.php` |
| R14 | No CMS, extensões aceitas no upload são mais amplas (inclui imagens, `ods`, áudio e vídeo) e a validação ocorre no `ContentRequest` (branch `default`). | `v6/app/Http/Requests/ContentRequest.php:308-314` |
| R15 | O módulo nasce **desligado** (`modulos.status = 0`) e é habilitado pelo operador em `/cms/modulos`. | `v6/database/migrations/2024_11_01_150206_create_modulos_table.php:298-303` |
| R16 | A abertura da listagem no CMS gera log de auditoria e incrementa `menu_orders.clicks`. | `v6/app/Http/Controllers/Prefecture/ContentController.php` (`returnIndex`, `menuClicks`) |

## 2. Acoplamentos

| Depende de | Natureza |
|---|---|
| `contents` / `category_contents` / `archives` | Dependência total de dados: o módulo não tem tabela própria. |
| `modulos` + `menu_orders` | Dependência administrativa: habilitação e métrica de menu. |
| `configuration` (chave `layout`) | Determina qual tema do portal renderiza as telas. |
| Sentinel (`has_access`, `check.route.permission`) | Autorização de acesso ao CMS. |
| Categoria `atas-e-pontos` | Dependência de configuração: se o seed não rodar, o módulo não funciona. |

| É usado por | Natureza |
|---|---|
| Módulo `sessoes` | Não há vínculo direto; as atas são tratadas como documentos avulsos. |
| Módulo `vereadores` | Não há vínculo; a frequência não é relacionada ao cadastro de vereadores. |
| Menu do portal | O item de menu é configurável em `menu_items`, apontando para `contents`/`menu_orders`. |

## 3. Armadilhas (bugs e riscos)

### 3.1 Injeção SQL via `$_GET['ano']` — **severidade alta**

Em `AtasEPontosController@show`:

```php
$meses = DB::select("select extract(month from publication_date) as mes from contents  WHERE category_contents_id = {$category->id} and extract(year from publication_date) = {$_GET['ano']} and publication_date <= NOW() and deleted_at IS NULL GROUP BY mes order by extract(month from publication_date) asc;");
```

O valor vem do array superglobal `$_GET`, sem validação, e é interpolado
diretamente no SQL. Um `ano=2024) OR 1=1 --` altera a consulta; um payload de
`UNION SELECT` permite extrair dados de outras tabelas. `$category->id` também
é interpolado, mas vem do banco.

**Correção obrigatória na v7**: `DB::select('... extract(year from publication_date) = ? ...', [$request->ano])`,
com validação `integer|min:1900|max:2100` e uso de `$request->ano` (não
`$_GET`).

### 3.2 `whereRaw` sem binding na busca textual — **severidade alta**

```php
$newTXT = clean_slug($reqTexto);
$query->whereRaw("translate(lower(title), ...) ilike '%" . $newTXT . "%'")
       ->orWhereRaw("translate(lower(text), ...) ilike '%" . $newTXT . "%'");
```

`clean_slug()` remove acentos e espaços, mas **não** escapa aspas simples.
Um texto com `'` quebra a consulta (erro 500) e uma string com `'; DROP TABLE
contents; --` executa comandos. A concatenação deve ser substituída por
binding: `whereRaw("... ilike ?", ['%'.$termo.'%'])`.

### 3.3 Precedência `AND`/`OR` na view SQL — **severidade média/alta**

```sql
WHERE c.deleted_at IS NULL AND cc.category_contents_id = cc2.id or c.category_contents_id = cc2.id
```

Como `AND` liga mais forte que `OR`, a condição efetiva é:

```
(c.deleted_at IS NULL AND cc.category_contents_id = cc2.id)
OR
(c.category_contents_id = cc2.id)
```

A segunda disjuncão **não** verifica `deleted_at IS NULL`. Resultado: conteúdo
com soft delete vinculado **diretamente** à categoria raiz `atas-e-pontos`
aparece na listagem do CMS, enquanto conteúdo removido em categorias filhas
não aparece (pois depende da primeira disjuncão). O comportamento é
inconsistente e inesperado.

Correção: remover o `or c.category_contents_id = cc2.id` ou envolvê-lo em
parênteses, ex. `WHERE c.deleted_at IS NULL AND (cc.category_contents_id = cc2.id OR c.category_contents_id = cc2.id)`.

### 3.4 `getIndex()` sem guarda — **erro 500**

```php
$categoryFirst = CategoryContent::whereHas('categoryContents', function ($query) {
    $query->where('slug', '=', 'atas-e-pontos');
})->orderBy('order')->first();

return redirect()->route('atas-e-pontos.show', $categoryFirst->slug);
```

Se a raiz existir mas **nenhuma categoria filha** existir (ou todas estiverem
com soft delete), `$categoryFirst` é `null` e o acesso a `->slug` gera
`Error: Call to a member function slug() on null` — página 500 no link do
menu. Correção: `abort(404)` ou redirecionar para uma página institucional.

### 3.5 `view()` sem `firstOrFail` — **erro 500 e vazamento**

```php
$content = Content::where('slug', $slug2)->first();

if ($content->publication_date > $current_date) {
```

Se o slug não existir, `$content` é `null` e o acesso a `->publication_date`
gera erro 500. Além disso, a busca ignora a categoria: um conteúdo de **qualquer**
módulo do portal com o mesmo slug é servido em `/atas-e-pontos/{cat}/{slug}` —
o que expõe, por exemplo, uma notícia sob o caminho de atas. Correção:
`Content::where('slug',$slug2)->whereHas('categoryContents', fn($q) => $q->where('slug','atas-e-pontos')->orWhereIn('id', ...))->firstOrFail()`.

### 3.6 Título errado no CMS

`v6/app/Http/Controllers/Prefecture/ContentController.php:116`:

```php
if ($slug === 'atas-e-pontos') return $this->returnIndex('Unidades de Atendimento', $slug);
```

O título da tela do CMS e o **log de auditoria** gravam "Unidades de
Atendimento". O texto correto já existe na linha 242
(`'Atas e Pontos de Frequencia'`), o que evidencia cópia do bloco do módulo
de unidades de atendimento.

### 3.7 Duplicação do bloco de filtros e do JS de Ajax

O JavaScript de Ajax (`getPosts`, `getPostsByAno`, `getPostsByMes`,
`getPostsByTexto`, `apagaTexto`, `exibirBotaoCancelar`) está **copiado** nas 6
variantes de tema, junto com o bloco de filtros de `list.blade.php`. Qualquer
correção precisa ser replicada 6 vezes. Na v7 isso deve virar um componente
Blade único com `@include` ou um controller de componente.

### 3.8 AJAX sem `encodeURIComponent`

Em `show.blade.php`, a URL é montada por concatenação
(`'?ano=' + ano + '&mes=' + mes + '&texto=' + text`). Um texto com `&` ou `#`
quebra a query string. O `fail()` exibe um `alert` genérico.

### 3.9 Ausência de rota de download e de controle de acesso aos arquivos

O download é um link direto `url($archive->path)`, aberto em nova aba, sem
controller, sem verificação de permissão e sem verificação de sessão. O nome do
arquivo vem do upload original e é previsível. Na v7 deve existir uma rota
`/atas-e-pontos/{cat}/{slug}/arquivo/{id}` com autorização e
`Content-Disposition`.

### 3.10 SQL cru para `$anos` e `$meses`

Ambos os queries são montados por concatenação, além do problema de
injeção do `$_GET['ano']`. Além disso, o `$anos` **não** filtra
`publication_date <= NOW()`, de modo que anos de conteúdo futuro aparecem no
seletor (a view só oculta anosgreater que o ano corrente).

### 3.11 Ausência de `text` e `publication_date` na validação

O `ContentRequest` não tem `case 'atas-e-pontos'`; o `default` exige apenas
`title`, `category_contents_id` e os arquivos. O formulário do CMS marca texto e
data como obrigatórios, mas a validação é apenas do lado do cliente.

## 4. Código copiado / origem dos blocos

| Trecho | Origem aparente |
|---|---|
| `ContentController@genericIndex` linha 116 | copiado do bloco `unidades-de-atendimento` (linha 115). |
| `ContentController@genericGenerate` linhas 241-247 | adaptation do bloco `requerimentos` (linhas 218-236), sem a parte de vereadores. |
| `DatatableController@get_atas_e_pontos` | mesmo formato de `get_sessoes`, `get_requerimentos`, `get_transmissoes`. |
| `portal/layouts/modeloN/atas-e-pontos/*` | replicação do padrão de `sessoes`/`requerimentos` nos 6 temas. |

## 5. Onde a v6 divergiu da v5

O módulo **não existe na v5**: nenhuma rota, migration, model, view ou seed
com `atas`, `pontos de frequência`, `pauta-do-dia`, `sessoes` ou `vereadores`
aparece em `v5/`. Detalhamento no `08-GAPS-E-DIVIDAS.md`.

## 6. Riscos de portabilidade para a v7

| Risco | Mitigação |
|---|---|
| URLs públicas mudarem | Preservar `/atas-e-pontos`, `/atas-e-pontos/{slug}`, `/atas-e-pontos/{slug1}/{slug2}` e os nomes de rota. |
| Dependência do slug da categoria | Migrar para uma FK explícita (`category_contents_id` já existe e é usada) e um enum de tipo de documento. |
| Injeção SQL | Bindings em toda consulta; nunca interpolar entrada do usuário. |
| Duplicação de telas | Componente Blade único parametrizado pelo tema. |
| Views SQL em migração | Substituir por Eloquent (ou `DB::table` sem `CREATE VIEW`) para evitar estado divergente entre ambientes. |
| Exposição de arquivos | Rota de download autorizada. |
| Ausência de presença estruturada | Modelar `SessionAttendance` se o negócio exigir (ver `06-MIGRACAO-MODERNA.md`). |
