# Notícias — Rotas e Controladores

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

Arquivo: `v6/routes/basepro.php`. O prefixo `/cms` vem do grupo
`Route::group(['prefix' => 'cms', 'before' => 'csrf'], ...)`
(`v6/routes/basepro.php:35`); todas as rotas abaixo exigem `check.logged`.

| Método | URI | Nome | Controller@ação | Observação |
|---|---|---|---|---|
| GET | `/cms/conteudos/noticias` | `listar-conteudo` | `Prefecture\ContentController@genericIndex` | Lista (view `prefecture.contents.noticias.index`) |
| GET | `/cms/conteudos/cadastrar/noticias` | `cadastrar-conteudo` | `Prefecture\ContentController@genericCreate` | Formulário de criação |
| GET | `/cms/conteudos/editar/noticias/{id}` | `editar-conteudo` | `Prefecture\ContentController@genericEdit` | Formulário de edição |
| POST | `/cms/conteudo` | `conteudo.store` | `Prefecture\ContentController@store` | Criação (`ContentRequest`) |
| PUT | `/cms/conteudo/{id}` | `conteudo.update` | `Prefecture\ContentController@update` | Atualização (`ContentRequest`) |
| DELETE | `/cms/conteudo/{id}` | `conteudo.destroy` | `Prefecture\ContentController@destroy` | Soft delete + remoção de `menu_items` |
| DELETE | `/cms/conteudo/delete/{id}` | `generic.conteudo.delete` | `Prefecture\ContentController@genericDestroy` | Soft delete (fluxo genérico de categoria) |
| GET | `/cms/datatable/noticias` | `datatable.noticias` | `Prefecture\DatatableController@get_noticias` | JSON da datatable (view `noticias`) |
| POST | `/cms/marcar-destaque` | `marcar.destaque` | `Prefecture\ContentController@setHighlights` | Alterna `highlights`/`carousel` |
| POST | `/cms/set-status` | `set.status` | `Prefecture\ContentController@setStatus` | Alterna `status` |
| POST | `/cms/conteudo/deleteAll` | `ajax.generic.content.deleteAll` | `Prefecture\ContentController@genericDeleteAll` | Exclusão em lote |
| POST | `/cms/ajust-banners` | `ajust.banners` | `Prefecture\ContentController@ajustOrder` | Reordenação (drag & drop) |
| POST | `/cms/ajust-categorias` | `categoria-conteudo.edit` | `Prefecture\CategoryContentController@ajustOrder` | Reordenação das categorias |
| — | `/cms/categoria-conteudo` (resource) | `categoria-conteudo.*` | `Prefecture\CategoryContentController` | CRUD das categorias |
| — | `/cms/conteudo` (resource) | `conteudo.*` | `Prefecture\ContentController` | CRUD genérico |

Referências: `v6/routes/basepro.php:137-138`, `:141`, `:173-176`, `:223`,
`:229`, `:235-237`, `:346-347`, `:396-398`.

Middleware aplicado no controller: `check.route.permission`
(`v6/app/Http/Controllers/Prefecture/ContentController.php:60`).

## 2. Rotas do Portal

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

| Método | URI | Nome | Controller@ação | Linha |
|---|---|---|---|---|
| ANY | `/noticias/{categorySlug?}` | `noticia.index` | `Portal\NewsController@index` | `v6/routes/web.php:106` |
| ANY | `/noticia/{slug}` | `noticia.show` | `Portal\NewsController@showContent` | `v6/routes/web.php:109` |
| ANY | `/noticias/categoria/{categorySlug}` | `noticia-por-categoria.index` | `Portal\NewsController@getIndexByCategory` | `v6/routes/web.php:110` |
| ANY | `/noticias-ajax/{categorySlug}` | `noticia-categoria.index` | `Portal\NewsController@indexContentsAjax` | `v6/routes/web.php:111` |

Rotas relacionadas:

| Método | URI | Nome | Controller@ação |
|---|---|---|---|
| GET | `/portal/{slug1}/{slug2}/{slug3}` | `conteudo.portal.view` | `Portal\PortalController@contentList` |
| GET | `/portal/{slug1}/{slug2}` | `conteudo.portal.show` | `Portal\PortalController@contentList` |

`PortalController::contentList` (`v6/app/Http/Controllers/Portal/PortalController.php:312-336`)
redireciona `noticias` para `route('noticia.index', $slug2)`.

## 3. Rotas da API (dados abertos)

Arquivo: `v6/routes/api.php:27-29`, prefixo `api`, middleware `api`,
grupo `prefix => v1`, `as => v1.`.

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

Parâmetros aceitos (`v6/app/Http/Controllers/Swagger/V1/Portal/NewController.php:26-66`):
`titulo`, `autor`, `fonte`, `descricao`, `data_publicacao`
(`dd/mm/yyyy` ou `dd/mm/yyyy_dd/mm/yyyy`) e `limite`.

A consulta base é montada em
`v6/app/Http/Controllers/Swagger/V1/Portal/Controller.php:30-49`:

```php
public function getContent()
{
    $this->limit = request()->get('limite');
    $content = Content::query()->with('archives');

    $directExists = Content::query()->whereHas('categoryContents', function ($query) {
        return $query->where('slug', $this->category);
    })->exists();

    $nestedExists = Content::query()->whereHas('categoryContents.categoryContents', function ($query) {
        return $query->where('slug', $this->category);
    })->exists();

    if ($directExists || $nestedExists) {
        $content->where(function ($query) use ($directExists, $nestedExists) {
            if ($directExists) {
                $query->orWhereHas('categoryContents', function ($query) {
                    return $query->where('slug', $this->category);
                });
            }
            if ($nestedExists) {
                $query->orWhereHas('categoryContents.categoryContents', function ($query) {
                    return $query->where('slug', $this->category);
                });
            }
        });
    }

    $content->orderByDesc('publication_date')->orderByDesc('clicks');
```

> A API busca por `categoryContents.slug = 'noticias'` **ou** por conteúdo cuja
> categoria tenha pai `noticias` — ao contrário do portal, que exige a segunda
> condição. As duas listagens **não retornam** o mesmo conjunto.

## 4. Breadcrumbs

Arquivo: `v6/routes/breadcrumbs.php:83-99`.

| Nome | Hierarquia | Rota usada |
|---|---|---|
| `noticias` | Home > Notícias | `route('noticia.index', 'noticias')` |
| `category-noticias` | Home > Notícias > Categoria | `route('noticia.index', $category->slug)` |
| `view-noticias` | Home > Notícias > Categoria > Notícia | `route('noticia.show', ['noticias', $content->categoryContents->slug, $content->slug])` |

> A chamada `route('noticia.show', [...])` passa 3 argumentos para uma rota que
> aceita 1 (`/noticia/{slug}`): os extras são ignorados, mas o breadcrumb fica
> dependente da assinatura da rota.

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

Arquivo: `v6/app/Http/Requests/ContentRequest.php:29-140`. O `switch ($slug)`
escolhe as regras conforme o slug da categoria (derivado de
`category_slug` ou de `category_contents_id`).

Caso `noticias` (`ContentRequest.php:60-77`):

```php
case 'noticias':
    $rules = [
        "title" => "required|max:150",
        "linha_fina" => "nullable|string|max:300",
        "publication_date" => "required",
        "source" => "max:120",
        "author" => "max:120",
        "link" => "max:2999",
        "text" => "required",
        "highlights" => "boolean",
        "archive_highlight" => "max:20000|mimes:jpg,jpeg,png,gif,webp",
    ];
    if (Route::getCurrentRoute()->getName() == "conteudo.update") {
        $rules += [
            "slug" => 'required|max:200'
        ];
    }
    break;
```

Validação de anexos (fora do `switch`, `ContentRequest.php:42-63`): as
extensões aceitas são `rar, zip, doc, docx, pdf, odt, xls, xlsx, xlsb, xslm,
ppt, pptx`; qualquer outra devolve `back()->withErrors(...)`.

Mensagens em português: `v6/app/Http/Requests/ContentRequest.php:319-347`.

## 6. Controladores — detalhes de comportamento

### `Prefecture\ContentController`

| Método | Linha | Comportamento |
|---|---|---|
| `genericIndex` | `:91-119` | Mapa slug -> título; `noticias` -> `returnIndex('Notícias', 'noticias')`; slug desconhecido -> `redirect()->route('dashboard.index')` |
| `genericGenerate` | `:163-273` | Monta o payload da view; para `noticias` envia `categoryContentSelect` (filhas da categoria 1), `secretarias` e `orgaos` |
| `returnData` | `:275-317` | Carrega `content`, `gallery`, `archives`, `category`, `destaque`; escolhe a view `create` ou `edit` |
| `store` | `:616-935` | Transação: conteúdo + destaque + galeria + link + auditoria; desambigua slug; move imagens do Summernote |
| `update` | `:937-1318` | Atualiza campos, reordena (`order`), trata `view_pdf`, destaca e link |
| `destroy` | `:1320-1367` | Soft delete + apaga `menu_items` ligados |
| `genericDestroy` | `:1369-1409` | Idem, mas redireciona para a categoria |
| `setHighlights` | `:1510-1546` | Alterna `highlights`; ao desmarcar, zera `carousel` |
| `setStatus` | `:1548-1587` | Alterna `status` |
| `ajustOrder` | `:1498-1502` | `reorder()` do `AppTrait` com `sortable_data` |
| `ajaxUploadFiles` | `:438-523` | Galeria: grava em `storage/content/<pai>/<slug>/<id>/galeria/` e comprime a 1024px |
| `pathUpdate` | `:373-435` | Substitui a imagem de destaque preservando o diretório |

### `Portal\NewsController`

| Método | Linha | Comportamento |
|---|---|---|
| `index` | `:31-60` | Modo agrupado (config `noticias == 0`) ou simples; renderiza `portal.layouts.<modelo>.noticias.index` |
| `showContent` | `:62-89` | Matéria + galeria + PDF inline + vídeo; incrementa `clicks` |
| `getIndexByCategory` | `:90-109` | Lista por categoria; redireciona se vazio |
| `indexContentsAjax` | `:111-135` | Retorna `['contents' => ..., 'layout' => ...]`; substitui imagem ausente por `storage/img/exemplos/exemplos/sem-imagem-min.png` |
| `updateClicks` | `:137-142` | `update(['clicks' => $content->clicks + 1])` (read-modify-write) |

### `Portal\NewsTrait`

| Método | Linha | Comportamento |
|---|---|---|
| `getContentNews` | `:28-40` | Consulta paginada padrão (filtra data, status, categoria pai) |
| `getCategoriesNews` / `getCategoryContentNews` | `:54-75` | Categorias filhas com `quantidade_itens > 0` |
| `getLayoutContentActived` | `:88-96` | `layout` da categoria "noticias"; fora de 1..3 -> 2 |
| `getContentNewsAjax` | `:97-119` | Versão sem paginação, limitada por `quantidade_itens` |
| `showContentNew` | `:121-143` | Busca por slug, data e status |
| `getMorePopularNews` | `:145-163` | Top 2 por `clicks`, excluindo destaques |
| `getContentsByParentCategory` | `:164-212` | Conteúdos da home, excluindo os já usados no carrossel |

## 7. Permissões

| Permissão | Verificação | Efeito |
|---|---|---|
| `noticias.create` | `has_access(['noticias.create'])` em `v6/resources/views/prefecture/contents/noticias/index.blade.php:12` | Exibe o botão "Cadastrar Notícia" |
| `noticias.edit` | `has_access("$permission.edit")` em `DatatableController::struct_views` (`:504-521`) | Habilita o botão de edição na datatable |
| `noticias.delete` | `has_access("$permission.delete")` | Habilita exclusão individual e em lote |
| `noticias.restore` / `noticias.inactivate` | `has_access(...)` | Flags `active`/`inactivate` devolvidas ao JS |

Base: `Resource` com `slug = 'noticias'` e `modulo_id = 14`
(`v6/database/seeds/ResourcesTableSeeder.php:62-69`;
`v6/database/migrations/2024_11_01_150206_create_modulos_table.php:108-112`).

## 8. Saída da API — Resources

| Resource | Caminho | Conteúdo |
|---|---|---|
| `NewResource` | `v6/app/Http/Resources/Swagger/V1/Portal/NewResource.php` | `titulo`, `data_publicacao`, `descricao`, `interacoes`, `slug`, `arquivos[]` |
| `ArchiveResource` | `v6/app/Http/Resources/Swagger/V1/Portal/ArchiveResource.php` | `titulo`, `descricao`, `tipo`, `url` |

Ambos usam o envelope padrão `data` do Laravel.
