# Publicações — Rotas e Controladores

## 1. Rotas do Portal

Arquivo: `v6/routes/web.php`, grupo `Route::middleware(['check.site.manutention'])`
(`v6/routes/web.php:7`).

| Método | URI | Nome | Controller@ação | Linha |
|---|---|---|---|---|
| GET | `/publicacoes` | `publicacoes.index` | `Portal\PublicationController@getIndex` | `v6/routes/web.php:38` |
| GET | `/publicacoes/export` | `publicacoes.export` | `Portal\PublicationController@export` | `v6/routes/web.php:39` |
| GET | `/publicacoes/{slug1}` | `publicacoes.show` | `Portal\PublicationController@showPublication` | `v6/routes/web.php:40` |
| GET | `/publicacoes/{slug1}/{slug2}` | `publicacoes.view` | `Portal\PublicationController@viewPublication` | `v6/routes/web.php:41` |

> **Ordem das rotas**: `export` está declarado **antes** de `{slug1}`,
> então `GET /publicacoes/export` é interpretado como a rota de exportação
> e **não** como a categoria de slug "export".

### Semântica dos parâmetros

| Rota | `{slug1}` | `{slug2}` |
|---|---|---|
| `/publicacoes/{slug1}` | slug da **subcategoria** (ex.: `editais`) | — |
| `/publicacoes/{slug1}/{slug2}` | slug da **subcategoria** | slug do **documento** (ignorado pelo controller, que busca só por `slug2`) |

Exemplo real de "Link Permanente" (montado pelo formulário do CMS,
`v6/resources/views/prefecture/contents/publicacoes/form.blade.php:33-56`):

```blade
<div class="mr-2">{{ url("publicacoes/$category->slug") }}/</div>

@include('components.inputs.input', [
    'size' => 4,
    'id' => 'slug',
    'maxlen' => '120',
    'data' => isset($content) ? $content->slug : old('slug'),
])

<div class="d-inline-block ml-3">
    <a href="{{ url("publicacoes/$category->slug/$content->slug") }}" target="_blank">Acessar <i
            class="fa fa-external-link fa-1x"></i></a>
</div>
```

### Rotas relacionadas

| Método | URI | Nome | Controller@ação |
|---|---|---|---|
| GET | `/portal/{slug1}/{slug2}/{slug3}` | `conteudo.portal.view` | `Portal\PortalController@contentList` |
| GET | `/legislacao/` | `legislacao.index` | `Portal\LegislationController@getIndex` |
| GET | `/legislacao/export` | `legislacao.export` | `Portal\LegislationController@export` |
| GET | `/legislacao/{slug}` | `conteudo.legislacao.list` | `Portal\LegislationController@contentShow` |
| GET | `/legislacao/{slug1}/{slug2}` | `conteudo.legislacao.view` | `Portal\LegislationController@contentView` |

`PortalController::contentList` (`v6/app/Http/Controllers/Portal/PortalController.php:327`)
redireciona `publicacoes` para `route('publicacoes.show', $slug2)` — a
rota de breadcrumb `conteudo.portal.view` **perde o documento** e cai na
lista da categoria.

## 2. Rotas do CMS (`/cms/...`)

Arquivo: `v6/routes/basepro.php`, grupo `/cms` (`v6/routes/basepro.php:35`).

| Método | URI | Nome | Controller@ação | Linha |
|---|---|---|---|---|
| GET | `/cms/conteudos/publicacoes` | `listar-conteudo` | `Prefecture\ContentController@genericIndex` | `v6/routes/basepro.php:235` |
| GET | `/cms/conteudos/cadastrar/publicacoes` | `cadastrar-conteudo` | `Prefecture\ContentController@genericCreate` | `v6/routes/basepro.php:236` |
| GET | `/cms/conteudos/editar/publicacoes/{id}` | `editar-conteudo` | `Prefecture\ContentController@genericEdit` | `v6/routes/basepro.php:237` |
| POST | `/cms/conteudo` | `conteudo.store` | `Prefecture\ContentController@store` | `v6/routes/basepro.php:174` |
| PUT | `/cms/conteudo/{id}` | `conteudo.update` | `Prefecture\ContentController@update` | `v6/routes/basepro.php:174` |
| DELETE | `/cms/conteudo/{id}` | `conteudo.destroy` | `Prefecture\ContentController@destroy` | `v6/routes/basepro.php:174` |
| GET | `/cms/datatable/publicacoes` | `datatable.publicacoes` | `Prefecture\DatatableController@get_publicacoes` | `v6/routes/basepro.php:435` |
| POST | `/cms/conteudo/deleteAll` | `ajax.generic.content.deleteAll` | `Prefecture\ContentController@genericDeleteAll` | `v6/routes/basepro.php:141` |
| POST | `/cms/marcar-destaque` | `marcar.destaque` | `Prefecture\ContentController@setHighlights` | `v6/routes/basepro.php:346` |
| POST | `/cms/set-status` | `set.status` | `Prefecture\ContentController@setStatus` | `v6/routes/basepro.php:347` |
| — | `/cms/categoria-conteudo` (resource) | `categoria-conteudo.*` | `Prefecture\CategoryContentController` | `v6/routes/basepro.php:173` |
| GET | `/cms/categorias/cadastrar/{slug}` | `cadastrar-categorias` | `Prefecture\CategoryContentController@genericCreate` | `v6/routes/basepro.php:239` |

## 3. Rotas da API (dados abertos)

`v6/routes/api.php:30-32`:

| Método | URI | Nome | Controller@ação |
|---|---|---|---|
| GET | `/api/v1/publicacoes` | `v1.publicacoes.index` | `Swagger\V1\Portal\PublicationController@index` |

Parâmetros (`v6/app/Http/Controllers/Swagger/V1/Portal/PublicationController.php:14-85`):
`titulo`, `data_publicacao` (`dd/mm/yyyy` ou `dd/mm/yyyy_dd/mm/yyyy`),
`descricao`, `limite` (default 100).

A consulta base (compartilhada) está em
`v6/app/Http/Controllers/Swagger/V1/Portal/Controller.php:30-49`; para
Publicações, `$this->category = 'publicacoes'`, e a busca aceita
categoria direta **ou** categoria filha de "publicacoes".

Formato de resposta (documentado nas anotações OpenAPI do controller):
`data[]` com `titulo`, `data_publicacao`, `descricao`, `interacoes`, `slug`
e `arquivos[]` (`titulo`, `descricao`, `tipo`, `url`).

## 4. Breadcrumbs

`v6/routes/breadcrumbs.php`:

| Nome | Linha | Hierarquia | Rota |
|---|---|---|---|
| `publicacoes` | `:77-80` | Home > Publicações | `route('publicacoes.index')` |
| `category-publicacoes` | `:121-124` | Home > Publicações > Categoria | `route('publicacoes.show', [$category->slug])` |
| `view-publicacoes` | `:127-130` | Home > Publicações > Categoria > Documento | `route('conteudo.portal.view', ['publicacoes', $cat->slug, $content->slug])` |

> A breadcrumb do documento aponta para `conteudo.portal.view`, que
> redireciona para a **lista** da categoria — o usuário não chega ao
> documento pelo breadcrumb.

## 5. Validação — `ContentRequest`

Caso `publicacoes` (`v6/app/Http/Requests/ContentRequest.php:91-105`):

```php
case 'publicacoes':

    $rules = [
        "title" => "required|max:120",
        "publication_date" => "required",
        "category_contents_id" => new SelectRule('categoria'),
        "text" => "required",
        "layout" => "required|numeric",
    ];
    if (Route::getCurrentRoute()->getName() == "conteudo.update") {
        $rules += [
            "slug" => 'required|max:200'
        ];
    }
    break;
```

Validação de anexos (aplicada a **todas** as categorias,
`v6/app/Http/Requests/ContentRequest.php:42-63`): extensões aceitas
`rar, zip, doc, docx, pdf, odt, xls, xlsx, xlsb, xslm, ppt, pptx`.

`SelectRule` (`v6/app/Rules/SelectRule.php`) valida que o valor de
`category_contents_id` corresponde a uma categoria existente e **não**
obrigatoriamente a uma subcategoria de Publicações — a checagem de
pertencimento à árvore é feita apenas pela view do formulário.

## 6. `PublicationController` (portal) — métodos

| Método | Linha | Comportamento |
|---|---|---|
| `getIndex` | `:19-27` | Redireciona para a primeira subcategoria por `order` |
| `showPublication` | `:28-113` | Lista paginada + filtros ano/mês/texto; resposta Ajax devolve só o fragmento `publicacoes.list` |
| `export` | `:115-152` | Exporta em PDF/XLS/CSV/TXT conforme `type` |
| `viewPublication` | `:153-171` | Exibe o documento, anexos e PDF inline; incrementa `clicks` |

### Filtros de `showPublication`

```php
$contents = Content::when($request->ano, function ($query, $reqAno) {
    $query->whereYear('publication_date', $reqAno);
})
    ->when($request->mes, function ($query, $reqMes) {
        $query->whereMonth('publication_date', $reqMes);
    })
    ->when(empty($request->mes), function ($query, $reqMes) {
        $query->where('publication_date', '<=', date('Y-m-d H:i'));
    })
    ->when($request->texto, function ($query, $reqTexto) {
        $query->where(function ($query) use ($reqTexto) {
            $newTXT = clean_slug($reqTexto);
            $query->whereRaw("translate(lower(title), ' ''àáâãäéèëêíìïîóòõöôúùüûçÇ', '--aaaaaeeeeiiiiooooouuuucc') ilike '%" . $newTXT . "%'")
                ->orWhereRaw("translate(lower(text), ' ''àáâãäéèëêíìïîóòõöôúùüûçÇ', '--aaaaaeeeeiiiiooooouuuucc') ilike '%" . $newTXT . "%'");
        });
    })->with('categoryContents')
    ->where('category_contents_id', $category->id)
    ->orderBy('publication_date', 'desc')
    ->orderBy('id', 'desc')->paginate(12);
```

Regras derivadas:

- `ano` e `mes` aplicam `whereYear`/`whereMonth`;
- **sem** `mes`, aplica-se `publication_date <= now` (documentos futuros
  somem); **com** `mes` e **sem** `ano`, não há filtro de data — documentos
  futuros podem aparecer;
- `texto` busca em `title` **ou** `text`, sem acento, via `translate()`;
- paginação fixa em 12.

### Resposta Ajax

```php
if (request()->ajax()) {
    return Response::json(
        view(
            'portal.layouts.' . $this->getTemplateActived() . '.publicacoes.list',
            compact('text', 'mesSelecionado', 'title', 'meses', 'contents', 'categories', 'category', 'anos', 'anoSelecionado', 'mesSelecionado')
        )->render()
    );
}
```

O JSON é o **HTML já renderizado** da partial `list`, não uma estrutura de
dados.

## 7. `DatatableController::get_publicacoes`

`v6/app/Http/Controllers/Prefecture/DatatableController.php:80-99` (código
completo em `01-ARQUITETURA.md`).

Diferenças em relação à view SQL `publicacoes`:

| Aspecto | View SQL | Datatable do CMS |
|---|---|---|
| Origem dos dados | SQL | PHP (todos os conteúdos em memória) |
| Paginação | não | não (o Yajra pagina em memória) |
| `status` | não filtrado | não filtrado |
| `publication_date` futura | aparece | aparece |
| Ordenação | `publication_date DESC` | `orderByDesc('publication_date')` |

O `struct_views($data, 'publicacoes')`
(`v6/app/Http/Controllers/Prefecture/DatatableController.php:504-521`)
acrescenta `edit`, `delete`, `active` e `inactivate` conforme as permissões.

## 8. Permissões

| Permissão | Uso |
|---|---|
| `publicacoes.create` | Botão "Cadastrar Publicação" e "Categorias de Publicação" (`v6/resources/views/prefecture/contents/publicacoes/index.blade.php:12` e `:20`) |
| `publicacoes.edit` | Botão de edição na datatable (`struct_views`) |
| `publicacoes.delete` | Exclusão individual e em lote |

Base: `Resource` `publicacoes` -> `modulo_id = 15`
(`v6/database/seeds/ResourcesTableSeeder.php:71-78`;
`v6/database/migrations/2024_11_01_150206_create_modulos_table.php:114-118`).

> O módulo `publicacoes` está com `status = 0` na tabela `modulos` — o que
> **não** é lido pelo portal para exibir/ocultar a seção.

## 9. Recursos de API

| Recurso | Caminho | Campos |
|---|---|---|
| `PublicationResource` | `v6/app/Http/Resources/Swagger/V1/Portal/PublicationResource.php` | `titulo`, `data_publicacao`, `descricao`, `interacoes`, `slug`, `arquivos[]` |
| `ArchiveResource` | `v6/app/Http/Resources/Swagger/V1/Portal/ArchiveResource.php` | `titulo`, `descricao`, `tipo`, `url` |
