# A Instituição — Rotas e Controladores

> Versão analisada: `v6`. Todas as URIs abaixo são relativas à raiz do domínio.

---

## 1. Rotas do portal público (`v6/routes/web.php`)

```php
// v6/routes/web.php:77-83
Route::prefix('a-instituicao')->group(function () {
    Route::any('/{slug}', 'Portal\PrefectureController@getIndex')->name('a-instituicao.index');
    Route::any('secretarias/{slug}', 'Portal\PrefectureController@getIndex')->name('a-instituicao.index');
    Route::any('secretarias/paginas/{slug}/{id}', 'Portal\SecretariaController@show_pages_secretary')->name('secretarias-pages.index');
    Route::any('orgaos/{slug}', 'Portal\PrefectureController@getIndex')->name('a-instituicao-orgaos.index');
});
```

| Método | URI | Nome | Controller@ação | Middleware | Observação |
|---|---|---|---|---|---|
| ANY | `/a-instituicao/{slug}` | `a-instituicao.index` | `Portal\PrefectureController@getIndex` | — | Rota principal do módulo. |
| ANY | `/a-instituicao/secretarias/{slug}` | `a-instituicao.index` **(duplicado)** | `Portal\PrefectureController@getIndex` | — | Mesma ação, **mesmo nome** da anterior. |
| ANY | `/a-instituicao/secretarias/paginas/{slug}/{id}` | `secretarias-pages.index` | `Portal\SecretariaController@show_pages_secretary` | — | Páginas internas de secretaria (módulo `../secretarias/`). |
| ANY | `/a-instituicao/orgaos/{slug}` | `a-instituicao-orgaos.index` | `Portal\PrefectureController@getIndex` | — | Página de órgão, com `sidenews`. |

### 1.1 Efeito do nome de rota duplicado

O Laravel registra as duas rotas com o mesmo nome; a **última** registrada
(`a-instituicao/secretarias/{slug}`) sobrescreve a resolução do nome. Portanto:

| Chamada | URL gerada | Efeito |
|---|---|---|
| `route('a-instituicao.index', 'sobre')` | `/a-instituicao/secretarias/sobre` | Acessa a página institucional **pelo prefixo de secretaria**. O controller decide a view pela categoria do conteúdo, então o conteúdo renderizado continua sendo o institucional, mas a URL pública é outra. |
| `route('a-instituicao.index')` (sem parâmetro) | `Route [a-instituicao.index] not defined` / erro de geração | Falha em qualquer template que use o nome sem argumento. |

Impactos diretos:

- `v6/app/Http/Controllers/Portal/PortalController.php:331` — `case 'a-instituicao': return redirect(route('a-instituicao.index', $slug2));` gera `/a-instituicao/secretarias/{slug}`.
- `v6/routes/breadcrumbs.php:203` — o breadcrumb do conteúdo institucional aponta para a URL com `/secretarias/`.
- `v6/resources/views/prefecture/contents/a-instituicao/form.blade.php:30` — deveria apontar para `/a-instituicao/{slug}`, mas aponta para `a-cidade` (ver bug em `08-GAPS-E-DIVIDAS.md`).

Na v5 os nomes eram distintos e o problema não existia:

```php
// v5/routes/web.php:69-74
Route::prefix('a-prefeitura')->group(function () {
    Route::any('/{slug}', 'Portal\PrefectureController@getIndex')->name('a-prefeitura.index');
    Route::any('secretarias/{slug}', 'Portal\PrefectureController@getIndex')->name('a-prefeitura-secretarias.index');
    Route::any('secretarias/paginas/{slug}/{id}', 'Portal\SecretariaController@show_pages_secretary')->name('secretarias-pages.index');
    Route::any('orgaos/{slug}', 'Portal\PrefectureController@getIndex')->name('a-prefeitura-orgaos.index');
});
```

### 1.2 Rota inexistente referenciada

```php
// v6/app/Http/Controllers/Portal/PortalController.php:331-332
case 'a-instituicao': return redirect(route('a-instituicao.index', $slug2));
case 'secretarias':   return redirect(route('a-instituicao-secretarias.index', $slug2));
```

O nome `a-instituicao-secretarias.index` **não existe** em `v6/routes/web.php`
(nem em qualquer outro arquivo de rotas da v6 — verificado por busca). O acesso a
`/portal/secretarias/{slug}` resulta em `RouteNotFoundException` (HTTP 500).

## 2. Rotas do CMS (`v6/routes/basepro.php`)

Todas sob o grupo `Route::group(['prefix' => 'cms', 'before' => 'csrf'], ...)`.

| Método | URI | Nome | Controller@ação | Middleware |
|---|---|---|---|---|
| GET | `/cms/conteudos/a-instituicao` | `listar-conteudo` | `Prefecture\ContentController@genericIndex` | `check.route.permission` (via construtor) |
| GET | `/cms/conteudos/cadastrar/a-instituicao` | `cadastrar-conteudo` | `Prefecture\ContentController@genericCreate` | idem |
| GET | `/cms/conteudos/editar/a-instituicao/{id}` | `editar-conteudo` | `Prefecture\ContentController@genericEdit` | idem |
| POST | `/cms/conteudo` | `conteudo.store` | `Prefecture\ContentController@store` | idem |
| PUT | `/cms/conteudo/{id}` | `conteudo.update` | `Prefecture\ContentController@update` | idem |
| DELETE | `/cms/conteudo/{id}` | `conteudo.destroy` | `Prefecture\ContentController@destroy` | idem |
| DELETE | `/cms/conteudo/delete/{id}` | `generic.conteudo.delete` | `Prefecture\ContentController@genericDestroy` | idem |
| POST | `/cms/ajax/generic/content/deleteAll` | `ajax.generic.content.deleteAll` | `Prefecture\ContentController@genericDeleteAll` | idem |
| GET | `/cms/datatable/a_instituicao` | `datatable.a_instituicao` | `Prefecture\DatatableController@get_a_instituicao` | idem |
| GET | `/cms/galeria/{column?}/{id?}` | `galeria.*` | `Prefecture\ArchiveController@getArchivesFromGalleryOrDriver` | — |
| POST | `/cms/galeria/` | `galeria.*` | `Prefecture\ArchiveController@saveArchivesFromGalleryOrDriver` | — |

Trechos reais:

```php
// v6/routes/basepro.php:174
Route::resource('conteudo', 'Prefecture\ContentController');

// v6/routes/basepro.php:236-238
Route::get('conteudos/{slug}', ['uses' => 'Prefecture\ContentController@genericIndex'])->name('listar-conteudo');
Route::get('conteudos/cadastrar/{slug}', ['uses' => 'Prefecture\ContentController@genericCreate'])->name('cadastrar-conteudo');
Route::get('conteudos/editar/{slug}/{id}', ['uses' => 'Prefecture\ContentController@genericEdit'])->name('editar-conteudo');

// v6/routes/basepro.php:396,415
Route::group(['prefix' => 'datatable', 'as' => 'datatable.'], function () {
    // ...
    Route::get('/a_instituicao', 'Prefecture\DatatableController@get_a_instituicao')->name('a_instituicao');
```

O `Route::resource('conteudo', ...)` é **genérico** — o mesmo par de rotas atende
todos os módulos de conteúdo. O módulo não tem rotas próprias de criação/edição;
o slug é o que decide a view e as regras.

## 3. Rotas da API (`v6/routes/api.php`)

```php
// v6/routes/api.php:19-37
Route::group(["prefix" => 'v1', 'as' => 'v1.'], function () {
    Route::group(['prefix' => 'secretarias', 'as' => 'secretarias.'], function () {
        Route::get('/', 'Swagger\V1\Portal\SecretariaController@index')->name('index');
    });
    Route::group(['prefix' => 'orgaos', 'as' => 'orgaos.'], function () {
        Route::get('/', 'Swagger\V1\Portal\OrgaoController@index')->name('index');
    });
    Route::group(['prefix' => 'legislacoes', 'as' => 'legislacoes.'], function () {
        Route::get('/', 'Swagger\V1\Portal\LegislationController@index')->name('index');
    });
    Route::group(['prefix' => 'noticias', 'as' => 'noticias.'], function () {
        Route::get('/', 'Swagger\V1\Portal\NewController@index')->name('index');
    });
    Route::group(['prefix' => 'publicacoes', 'as' => 'publicacoes.'], function () {
        Route::get('/', 'Swagger\V1\Portal\PublicationController@index')->name('index');
    });
});
```

**Não há endpoint para A Instituição.** Não existe `v1/instituicoes` nem
`v1/a-instituicao`. O conteúdo institucional não é exposto em dados abertos —
lacuna registrada em `08-GAPS-E-DIVIDAS.md`.

## 4. Roteamento legado do portal (`v6/routes/web.php:60-63`)

| 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` |
| GET | `/portal/{slug}` | `conteudo.portal.list` | `Portal\PortalController@contentList` |

`contentList()` redireciona por `switch ($slug)`:

```php
// v6/app/Http/Controllers/Portal/PortalController.php:330-332
case 'a-cidade':    return redirect(route('a-cidade.index', $slug2));
case 'a-instituicao': return redirect(route('a-instituicao.index', $slug2));
case 'secretarias':  return redirect(route('a-instituicao-secretarias.index', $slug2));
```

O módulo também participa da **busca geral do portal**, que varre um conjunto fixo
de slugs de categoria:

```php
// v6/app/Http/Controllers/Portal/PortalController.php:251,265
$slugs = ['noticias', 'a-cidade', 'a-instituicao', 'glossario', 'publicacoes', 'legislacao', 'perguntas-frequentes'];
```

## 5. Breadcrumbs (`v6/routes/breadcrumbs.php:199-220`)

```php
// v6/routes/breadcrumbs.php:200-204
Breadcrumbs::register('a-instituicao', function ($breadcrumbs, $content) {
    $breadcrumbs->parent('home');
    $breadcrumbs->push('A Instituição', url('/#'));
    $breadcrumbs->push($content->title, route('a-instituicao.index', $content->slug));
});
```

| Registro | Categoria | Último item |
|---|---|---|
| `a-instituicao` | id 6 | `route('a-instituicao.index', $content->slug)` |
| `secretarias` | id 7 | `route('a-instituicao.index', $content->slug)` |
| `orgaos` | id 32 | `route('a-instituicao.index', $content->slug)` |

Os três registros compartilham o **mesmo** nome de rota — mais um efeito do bug
de duplicidade. O `push('A Instituição', url('/#'))` cria um link morto (o "#"
navega para a home) em vez de apontar para a listagem do módulo.

## 6. Controladores

### 6.1 `Prefecture\ContentController` (CMS)

| Método | Papel no módulo | Linha |
|---|---|---|
| `__construct` | Aplica `check.route.permission`; injeta `Content`, `CategoryContent`, `Archive`, `TypesSizes` e o `IContentRepository`. | `v6/app/Http/Controllers/Prefecture/ContentController.php:60-70` |
| `genericIndex($slug)` | `if ($slug === 'a-instituicao') return $this->returnIndex('A Instituição', $slug);` | `:91` |
| `returnIndex($title, $slug)` | `generateLog($title)`, `menuClicks($slug)`, `view("prefecture.contents.$slug.index", ['title' => $title])`. | `:114-118` |
| `genericCreate($slug)` / `genericEdit($slug, $id)` | Delegam para `genericGenerate()`. | `:139-161` |
| `genericGenerate($slug, $id)` | `if ($slug === 'a-instituicao') return $this->returnData('A Instituição', $slug, null, [], $id);` | `:161` |
| `returnData($title, $slug, $self, $extra, $id)` | Monta `categoryContent`, `content`, `gallery`, `archives`, `category`, `destaque`; escolhe `create` ou `edit`. | `:275-308` |
| `store(ContentRequest)` | Transação, slug com colisão `-<maxid+1>`, grava `contents`, move arquivos do Summernote. | `:616-700` |
| `update(ContentRequest, $id)` | Atualiza, refaz a galeria e os arquivos de destaque. | `:937` |
| `destroy($id)` / `genericDestroy($id)` | Remove o conteúdo e os `menu_items` que o referenciam. | `:1320`, `:1369` |
| `genericDeleteAll(Request)` | Exclusão em lote via repository (`destroyAll`). | `:74-82` |

### 6.2 `Prefecture\DatatableController` (CMS — grade)

```php
// v6/app/Http/Controllers/Prefecture/DatatableController.php:252-256
public function get_a_instituicao()
{
    $instituto = DB::table('a_instituicao')->get();
    return DataTables::of($this->struct_views($instituto, 'a-instituicao'))->make(true);
}
```

`struct_views()` injeta, por linha, as permissões `edit`, `delete`, `active`
(`*.restore`) e `inactivate` (`*.inactivate`):

```php
// v6/app/Http/Controllers/Prefecture/DatatableController.php:504-520
public function struct_views($data, $permission)
{
    for ($i = 0; $i < count($data); $i++) {
        $data[$i]->edit = has_access("$permission.edit");
        if ($permission === 'diario-oficial') {
            $data[$i]->delete = Sentinel::inRole('super-admin');
        } else {
            $data[$i]->delete = has_access("$permission.delete");
        }
        $data[$i]->active = has_access("$permission.restore");
        $data[$i]->inactivate = has_access("$permission.inactivate");
    }
    return $data;
}
```

### 6.3 `Portal\PrefectureController` (portal público)

| Método | Papel | Linha |
|---|---|---|
| `getIndex($slug1)` | Orquestra a página: eventos, notícias paginadas, conteúdo, vídeo, cliques, escolha da view. | `v6/app/Http/Controllers/Portal/PrefectureController.php:29-98` |
| `__construct` | Injeta `Content`, `CategoryContent` e `CategoriasSecretaria`. | `:24-28` |

## 7. Form Requests e validação

`v6/app/Http/Requests/ContentRequest.php` — `rules()` faz `switch ($slug)`:

```php
// v6/app/Http/Requests/ContentRequest.php:230-237
case 'a-instituicao':
    $rules = [
        "title" => "required|max:120",
        "category_contents_id" => "required|numeric",
        "text" => "required",
        "link" => "max:200",
        "archive_highlight" => "max:20000|mimes:jpg,jpeg,png,gif,webp",
    ];
    break;
```

| Campo | Regra | Mensagem (de `messages()`) |
|---|---|---|
| `title` | `required|max:120` | "Por favor, informe um título." / "O campo título ultrapassou o número máximo de caracteres permitidos." |
| `category_contents_id` | `required|numeric` | "Não foi possível recuperar a categoria selecionada, tente novamente." |
| `text` | `required` | "Por favor, digite algum texto." |
| `link` | `max:200` | "O campo URL ultrapassou o número máximo de caracteres permitidos." |
| `archive_highlight` | `max:20000|mimes:jpg,jpeg,png,gif,webp` | "Por favor, selecione uma imagem com tamanho menor que 20MB." |

Observações:

- `authorize()` devolve `true` incondicionalmente
  (`v6/app/Http/Requests/ContentRequest.php:19-22`) — a autorização real vem do
  middleware `check.route.permission` e do `has_access()` das views.
- `category_contents_id` é validado como numérico, mas **não** é validado
  contra a lista de categorias permitidas: um operador pode gravar o conteúdo em
  qualquer categoria (inclusive `noticias`) pela rota `POST /cms/conteudo`.
- Não há validação de unicidade do `slug`: a colisão é resolvida no `store()`.
- O `$slug` que comanda o `switch` **não vem da rota**: é derivado da categoria do
  conteúdo sendo salvo — `category_slug` da requisição ou, na ausência dele,
  `CategoryContent::find($request->get('category_contents_id'))->slug`
  (`v6/app/Http/Requests/ContentRequest.php:32-40`). Na prática, editar um
  conteúdo de A Instituição pela rota `POST /cms/conteudo` aplica o case
  `a-instituicao`; mas o `POST /cms/conteudo` com `category_contents_id` de outra
  categoria aplica o case **dela**, independentemente da tela de origem.

## 8. Permissões exigidas

| Permissão | Onde é exigida |
|---|---|
| `a-instituicao.index` | Menu lateral do CMS (`v6/app/Providers/MenuServiceProvider.php:46`) e `check.route.permission` em `GET /cms/conteudos/a-instituicao`. |
| `a-instituicao.create` | Botão "Cadastrar Conteúdo" (`v6/resources/views/prefecture/contents/a-instituicao/index.blade.php:12`). |
| `a-instituicao.delete` | Coluna de seleção múltipla e botão "Excluir selecionados" (`index.blade.php:19`, `:28-33`). |
| `a-instituicao.edit` | Botão de edição por linha (via `struct_views`). |
| `a-instituicao.inactivate` / `.inactivate` / `.restore` | Botões de ativar/inativar por linha (via `struct_views`). |

O papel `administrador` possui todas as permissões `a-instituicao.*`. Papéis
`super-admin` passam por `Sentinel::inRole('super-admin')`, que curto-circuita
tanto `has_access()` quanto o middleware.

O menu lateral do CMS só exibe a entrada se **as duas** condições forem verdadeiras:
`has_access("a-instituicao.index")` **e** `modulos.status` verdadeiro. Na base
atual, `modulos.id = 12` (`a-instituicao`) tem `status = true`; já `a-cidade`,
`orgaos` e `secretarias` têm `status = false` — suas entradas ficam ocultas
apesar de as rotas existirem.

## 9. Saídas de API / presenters

| Item | Formato |
|---|---|
| Grade do CMS | JSON do Yajra DataTables: `{data: [{value, titulo, id, galeria, slug, edit, delete, active, inactivate}], ...}`. |
| Página pública | HTML (Blade). Sem API Resource. |
| `ContentPresenter` | Presenter do model `Content` (`v6/app/Http/Model/Prefecture/Content.php:57`); não é usado neste fluxo do portal. |
| `MenuItemPresenter::getLink()` | Devolve a URL do item de menu; para a categoria 6 cai no ramo genérico `url('/' . $categoryContent->slug . $content_slug)` → `/a-instituicao/<slug>`; para as categorias 7 e 32 monta `/a-instituicao/<slug-da-categoria><slug-do-conteúdo>`. |
