# Mais Conteúdos — Rotas e Controllers

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

### 1.1 Categorias de conteúdo (`Route::resource`)

Definidas em `v6/routes/basepro.php:173`:

```php
Route::resource('categoria-conteudo', 'Prefecture\CategoryContentController');
```

| Método | URI | Nome | Controller@ação |
|---|---|---|---|
| GET | `categoria-conteudo` | `categoria-conteudo.index` | `Prefecture\CategoryContentController@index` |
| GET | `categoria-conteudo/create` | `categoria-conteudo.create` | `Prefecture\CategoryContentController@create` |
| POST | `categoria-conteudo` | `categoria-conteudo.store` | `Prefecture\CategoryContentController@store` |
| GET | `categoria-conteudo/{id}` | `categoria-conteudo.show` | `Prefecture\CategoryContentController@show` |
| GET | `categoria-conteudo/{id}/edit` | `categoria-conteudo.edit` | `Prefecture\CategoryContentController@edit` |
| PUT/PATCH | `categoria-conteudo/{id}` | `categoria-conteudo.update` | `Prefecture\CategoryContentController@update` |
| DELETE | `categoria-conteudo/{id}` | `categoria-conteudo.destroy` | `Prefecture\CategoryContentController@destroy` |

### 1.2 Rotas por *slug* de categoria

| Método | URI | Nome | Controller@ação | Linha |
|---|---|---|---|---|
| GET | `categorias/{slug}` | `listar-categorias` | `CategoryContentController@genericIndex` | `v6/routes/basepro.php:238` |
| GET | `categorias/cadastrar/{slug}` | `cadastrar-categorias` | `CategoryContentController@genericCreate` | `v6/routes/basepro.php:239` |
| GET | `categorias/editar/{slug}/{id}` | `editar-categorias` | `CategoryContentController@genericEdit` | `v6/routes/basepro.php:240` |

`genericIndex`/`genericCreate`/`genericEdit` fazem `switch ($slug)` para os casos
`legislacao`, `publicacoes`, `noticias`, `requerimentos` e `transparencia-fiscal`; o
`default` redireciona para `index()` — que é justamente a listagem das categorias
"Mais Conteúdos".

### 1.3 Reordenação

Existem **duas** rotas de reordenação de categorias:

| Método | URI | Nome | Controller@ação | Linha |
|---|---|---|---|---|
| POST | `ajust-categorias` | `categoria-conteudo.edit` | `CategoryContentController@ajustOrder` | `v6/routes/basepro.php:138` |
| POST | `ajax/ajust-categorias` | `ajust.categorias` | `CategoryContentController@ajustOrder` | `v6/routes/basepro.php:164` |

> O nome `categoria-conteudo.edit` da primeira rota **colide** com o nome gerado pelo
> `Route::resource` (linha 173) para `GET categoria-conteudo/{id}/edit`. Como o
> `resource` é registrado **depois**, ele prevalece no registry de nomes: o
> `route('categoria-conteudo.edit')` resolve para a tela de edição e a rota POST
> `ajust-categorias` fica sem nome próprio.

```php
// v6/app/Http/Controllers/Prefecture/CategoryContentController.php:819-825
public function ajustOrder(Request $request)
{
    $array_items = $request->get('sortable_data');
    $this->reorder(new CategoryContent(), $array_items);
    // trecho omitido do código original
```

### 1.4 API interna de categorias

Grupo em `v6/routes/basepro.php:505-509`:

```php
Route::group(['prefix' => 'categories', 'as' => 'categories.'], function () {
    Route::get('/{slug}', 'Prefecture\CategoryContentController@genericShow')->name('show');
    Route::post('/', 'Prefecture\CategoryContentController@apiStore')->name('store');
    Route::put('/', 'Prefecture\CategoryContentController@apiUpdate')->name('update');
    Route::delete('/', 'Prefecture\CategoryContentController@apiDestroy')->name('destroy');
});
```

| Método | URI | Nome | Controller@ação | Form Request |
|---|---|---|---|---|
| GET | `cms/categories/{slug}` | `categories.show` | `genericShow` | — |
| POST | `cms/categories` | `categories.store` | `apiStore` | `ApiStoreRequest` |
| PUT | `cms/categories` | `categories.update` | `apiUpdate` | `ApiUpdateRequest` |
| DELETE | `cms/categories` | `categories.destroy` | `apiDestroy` | `ApiDestroyRequest` |

> **Autenticação:** o grupo externo (`v6/routes/basepro.php:495`) aplica apenas
> `before => csrf`; o middleware `check.logged` é aplicado **rota a rota** em parte
> do arquivo (por exemplo `v6/routes/basepro.php:38`). O
> `Route::resource('categoria-conteudo', ...)` (linha 173) **não** tem
> `check.logged`, e o `RouteServiceProvider` só aplica o grupo `web`
> (`v6/app/Providers/RouteServiceProvider.php:84-89`). Ver
> `08-GAPS-E-DIVIDAS.md` (G6).
>
> A API interna, por sua vez, valida a sessão **dentro** dos Form Requests, via
> `Sentinel::check()`.

## 2. Rotas do Portal

| Método | URI | Nome | Controller@ação | Linha |
|---|---|---|---|---|
| GET | `portal/{slug}` | `conteudo.portal.list` | `Portal\PortalController@contentList` | `v6/routes/web.php:61` |
| GET | `portal/{slug1}/{slug2}` | `conteudo.portal.show` | `Portal\PortalController@contentList` | `v6/routes/web.php:60` |
| GET | `portal/{slug1}/{slug2}/{slug3}` | `conteudo.portal.view` | `Portal\PortalController@contentList` | `v6/routes/web.php:59` |

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

As categorias "Mais Conteúdos" são acessadas por essas rotas **genéricas** — o
`switch ($slug)` do controller redireciona as categorias de sistema para seus
controllers dedicados e deixa passar as novas.

## 3. Controller — `CategoryContentController`

Arquivo: `v6/app/Http/Controllers/Prefecture/CategoryContentController.php`

| Método | Linha | Comportamento |
|---|---|---|
| `index` | 40 | Lista as categorias "Mais Conteúdos" (`status_create = 0`, topo) |
| `create` | 60 | Formulário; `$categories` = categorias de topo criadas pelo operador |
| `genericIndex` | 87 | `switch ($slug)` por família de categorias; default → `index()` |
| `genericCreate` | 187 | Formulário por família; default → `create()` |
| `genericEdit` | 324 | Edição por família; default → `edit()` |
| `store` | 445 | Cria a categoria (com `status_create = 0`, slug único, rebalanceamento de `order`, upload de `custom_icon`) |
| `show` | 535 | Detalhe da categoria + conteúdos vinculados |
| `edit` | 557 | Formulário de edição |
| `update` | 579 | Atualiza a categoria |
| `destroy` | 692 | Exclui (apenas se `status_create != 1` e sem conteúdos) |
| `genericDestroy` | 755 | Variante do destroy por família |
| `ajustOrder` | 820 | Reordenação (drag & drop) |
| `genericShow` | 827 | JSON da categoria por slug |
| `apiStore` | 847 | API: cria categoria |
| `apiUpdate` | 901 | API: atualiza lote de categorias |
| `apiDestroy` | 945 | API: exclui categoria |

### 3.1 `store` — o que define uma categoria "Mais Conteúdos"

```php
// v6/app/Http/Controllers/Prefecture/CategoryContentController.php:449-459
$categoryContent = [
    'name' => $request->get('name'),
    'slug' => $slug,
    'order' => $request->get('order'),
    'quantidade_itens' => $request->get('quantidade_itens'),
    'status_create' => 0,
    'layout' => $request->get('layout'),
    'display_type' => $request->get('display_type') && $request->get('display_type') != '0' ? $request->get('display_type') : null,
    'category_contents_id' => $request->get('category_contents_id') != 0 ? $request->get('category_contents_id') : null,
];
```

### 3.2 `destroy` — regras de exclusão

```php
// v6/app/Http/Controllers/Prefecture/CategoryContentController.php:702-724 (trecho)
if ($category->status_create != 1) {
    $contents = Content::where('category_contents_id', $id)->get();

    if (count($contents) == 0) {
        $menu_item = MenuItem::where('category_contents_id', $category->id)->get();
        if (count($menu_item) > 0) {
            foreach ($menu_item as $item) { $item->delete(); }
            flash('Foram excluídos os itens de menu referentes à essa categoria.')->info();
        }

        $category->delete();
        flash('A categoria foi deletada com sucesso.')->success();
    } else {
        flash('Esta categoria não pode ser excluída, pois existem conteúdos ligados a ela.')->error();
    }
} else {
    flash('Esta categoria não pode ser deletada')->info();
}
```

Regras:

1. Categorias de sistema (`status_create = 1`) **nunca** são excluídas.
2. Categoria com conteúdos vinculados **não** é excluída.
3. Ao excluir, os `menu_items` vinculados são removidos em cascata manual.

### 3.3 `apiStore` / `apiUpdate` / `apiDestroy`

| Método | Entrada | Saída |
|---|---|---|
| `apiStore` | `name`, `order`, `parent_id` | `201` `{"success": {"category": "Categoria salva com sucesso."}, "id": <id>}` |
| `apiUpdate` | `categories[]` com `id`, `name`, `order`, `parent_id` | `201` `{"success": {"category": "Categorias salvas com sucesso."}}` |
| `apiDestroy` | `id` | JSON de sucesso/erro |

Erros de validação devolvem `422` com `{"errors": {...}}` (implementado em
`failedValidation` dos Form Requests).

Exceções `PDOException` devolvem `500` com
`{"errors": {"category": "...", "message": "<mensagem do PostgreSQL>"}}` — **a mensagem
do banco vaza para o cliente**.

## 4. Form Requests

### 4.1 `CategoryContentRequest` (formulário do CMS)

Arquivo: `v6/app/Http/Requests/CategoryContentRequest.php`

```php
$rules = [
    'category_contents_id' => 'required',
    'name' => 'required|max:190',
];

if (isset($request->generic)) {
    $rules['category_contents_id'] = '';
}

if (!empty($request->get('category_contents_id'))) {
    $slug = CategoryContent::find($request->get('category_contents_id'))->slug;
    if ($slug == 'noticias') {
        $rules['order'] = 'required|numeric';
        $rules['quantidade_itens'] = 'required|numeric';
        $rules['layout'] = 'required|numeric';
    }

    if ($slug == 'transparencia-fiscal') {
        $rules['description'] = 'nullable|max:500';
    }
}
```

| Campo | Regra | Observação |
|---|---|---|
| `name` | `required|max:190` | A UI não limita; o banco aceita 191 |
| `category_contents_id` | `required` (ou `''` se `generic`) | No formulário genérico o pai é opcional (categoria de topo) |
| `order` | `required|numeric` | Somente quando o pai é `noticias` |
| `quantidade_itens` | `required|numeric` | idem |
| `layout` | `required|numeric` | idem |
| `description` | `nullable|max:500` | Somente quando o pai é `transparencia-fiscal` |

`authorize()` retorna `true`.

### 4.2 Requests da API

| Request | Regras |
|---|---|
| `ApiStoreRequest` | `name` required max 255; `order` required integer; `parent_id` required `exists:category_contents,id` |
| `ApiUpdateRequest` | `categories` required array; `categories.*.name` required max 255; `categories.*.order` required integer; `categories.*.parent_id` required `exists:category_contents,id` |
| `ApiDestroyRequest` | `id` required `exists:category_contents,id` |

Todos com `authorize()` → `Sentinel::check()` e `failedValidation` customizado
(422 JSON).

## 5. Permissões

| Recurso | Slug | Tabela |
|---|---|---|
| Módulo | `mais-conteudos` | `modulos` (id 54, `status = 1`) |
| Item de menu | `categoria-conteudo` | `menu_orders` (id 42, `modulo_id = 54`) |
| Resource "Categorias de Conteúdos" | `categoria-conteudo` | `resources` (id 73) |
| Resource "Conteúdos" | `conteudo` | `resources` (id 74) |

Uso no código:

- `has_access(['categoria-conteudo.create'])` — botão "Cadastrar Categoria"
  (`v6/resources/views/prefecture/category-contents/index.blade.php`)
- `has_access("$menu->slug.index")` — exibição do item de menu
  (`v6/app/Providers/MenuServiceProvider.php:46`)

## 6. Resources

`v6/app/Http/Resources/CategoryContents/ShowResource.php`:

```php
class ShowResource extends Resource
{
    public function toArray($request)
    {
        return [
            'id' => data_get($this, 'id'),
            'name' => data_get($this,'name'),
            'order' => (int) data_get($this, 'order'),
            'parentId' => (int) data_get($this, 'category_contents_id')
        ];
    }
}
```

Usado por `genericShow` (`v6/app/Http/Controllers/Prefecture/CategoryContentController.php:827`),
que resolve a categoria por *slug* e devolve o JSON.

## 7. Logs de auditoria

Todas as operações registram `activity()` com `Sentinel::getUser()`, ex.:
"Cadastro de categoria de notícia" (store), "Listou categorias" (index),
"Exclusão de categoria de notícia" (destroy), "Atualização de categoria" (apiUpdate).
