# Órgãos — Rotas e Controladores

> Escopo: mapa completo de rotas (CMS, Portal, API), controllers, Form Requests, Resources e permissões.
> Todas as rotas abaixo foram lidas diretamente dos arquivos de rotas do repositório.

---

## 1. CMS — `/cms/...`

O grupo de prefixo `cms` está em `v6/routes/basepro.php:34`:

```php
Route::group(['prefix' => 'cms', 'before' => 'csrf'], function () {
```

### 1.1 Tabela de rotas

| Método | URI | Nome da rota | Controller@action | Permissão efetiva |
|---|---|---|---|---|
| GET | `/cms/conteudos/orgaos` | `listar-conteudo` | `Prefecture\ContentController@genericIndex` | `orgaos.index` |
| GET | `/cms/conteudos/cadastrar/orgaos` | `cadastrar-conteudo` | `Prefecture\ContentController@genericCreate` | `orgaos.create` |
| GET | `/cms/conteudos/editar/orgaos/{id}` | `editar-conteudo` | `Prefecture\ContentController@genericEdit` | `orgaos.edit` |
| GET | `/cms/datatable/orgaos` | `datatable.orgaos` | `Prefecture\DatatableController@get_orgaos` | nenhuma (rota aberta `datatable`) |
| POST | `/cms/conteudo` | `conteudo.store` | `Prefecture\ContentController@store` | nenhuma (`store` é rota de recurso "aberta") |
| PUT | `/cms/conteudo/{id}` | `conteudo.update` | `Prefecture\ContentController@update` | nenhuma (`update` é rota de recurso "aberta") |
| DELETE | `/cms/conteudo/{id}` | `conteudo.destroy` | `Prefecture\ContentController@destroy` | nenhuma (`destroy` é rota de recurso "aberta") |
| DELETE | `/cms/conteudo/delete/{id}` | `generic.conteudo.delete` | `Prefecture\ContentController@genericDestroy` | nenhuma (o caso especial do middleware é inalcançável) |
| POST | `/cms/conteudo/deleteAll` | `ajax.generic.content.deleteAll` | `Prefecture\ContentController@genericDeleteAll` | `ajax` é rota aberta |
| GET | `/cms/modulos` | `modulos.index` | `Admin\ModulosController@index` | `check.logged` |
| POST | `/cms/modulos` | `modulos.update` | `Admin\ModulosController@update` | `check.logged` (alterna `modulos.status`) |

Trechos reais que definem essas rotas:

```php
// v6/routes/basepro.php:235-237
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:174
Route::resource('conteudo', 'Prefecture\ContentController');

// v6/routes/basepro.php:416 (dentro do grupo ['prefix' => 'datatable', 'as' => 'datatable.'])
Route::get('/orgaos', 'Prefecture\DatatableController@get_orgaos')->name('orgaos');

// v6/routes/basepro.php:223
Route::delete('conteudo/delete/{id}', 'Prefecture\ContentController@genericDestroy')->name('generic.conteudo.delete');

// v6/routes/basepro.php:141
Route::post('conteudo/deleteAll', 'Prefecture\ContentController@genericDeleteAll')->name('ajax.generic.content.deleteAll');
```

### 1.2 `Prefecture\ContentController`

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

| Método | Linha | Papel no módulo Órgãos |
|---|---|---|
| `__construct` | 53-64 | Aplica `$this->middleware('check.route.permission')`; injeta `Content` e `IContentRepository` |
| `genericIndex` | 91-125 | `if ($slug === 'orgaos') return $this->returnIndex('Órgãos', $slug);` (linha 95) |
| `returnIndex` | 121-126 | `generateLog($title)`; `menuClicks($slug)`; `return view("prefecture.contents.$slug.index", ['title' => $title]);` |
| `genericCreate` / `genericEdit` | 140-161 | Delegam para `genericGenerate($slug, $id)` |
| `genericGenerate` | 163-... | `if ($slug === 'orgaos') return $this->returnData('Órgãos', $slug, null, [], $id);` (linha 165) |
| `returnData` | 275-... | Monta `$categoryContent` (`CategoryContent::where('slug', $slug)->first()`), `$content`, `$gallery`, `$archives`, `$category`, `$destaque` |
| `store` | 616-... | Gera `slug`, resolve colisão, grava `contents`, move imagens do Summernote, cria `Link` de vídeo, registra log |
| `destroy` | 1320-1361 | Remove `menu_items` que apontam para o conteúdo + soft delete |
| `genericDestroy` | 1369-1403 | Idem, redireciona para `categoria-conteudo.show` |
| `genericDeleteAll` | 70-76 | Exclusão em lote via `IContentRepository::destroyAll` |

Trecho do `store()` que grava os campos do conteúdo:

```php
// v6/app/Http/Controllers/Prefecture/ContentController.php:638-669
$contentData = [
    'clicks' => '0',
    'title' => $request->get('title'),
    'linha_fina' => $request->filled('linha_fina') ? trim($request->get('linha_fina')) : null,
    'text' => $request->get('text'),
    'highlights' => $request->get('destaque'),
    'carousel' => $request->get('carousel'),
    'order' => $request->get('order'),
    'category_contents_id' => $category_contents_id,
    'secretaria' => $request->get('secretaria'),
    'publication_date' => $publication_date,
    'author' => $request->get('author'),
    'source' => $request->get('source'),
    'status' => $request->get('status'),
    'galery' => $request->get('galery'),
    'isBlank' => $request->get('isBlank'),
    'icon' => $request->get('icon'),
    'referencia' => $request->filled('referencia') ? trim($request->get('referencia')) : null,
    'orgaos_id' => $request->orgaos != "0" ? $request->orgaos : null,
    'councilmen_id' => $request->councilmen_id != "0" && $request->councilmen_id ? $request->councilmen_id : null,
    'position_id' => $request->get('position_id'),
    'layout' => $request->get('layout'),
];

$verificaSlug = Content::where('slug', str_slug($request->get('title')))->get();
$maxid = Content::max('id') + 1;
if (count($verificaSlug) > 0) $slug = $slug . "-" . $maxid;

$contentData += ["slug" => $slug];
```

> **Armadilha:** `store()` referencia `$request->orgaos` e `$request->councilmen_id` **sem `->has()`/default**.
> No formulário de órgãos esses campos não existem; o acesso a um atributo inexistente no `Request`
> devolve `null`, e `null != "0"` é `true` no PHP (comparação frouxa com string), de modo que
> `orgaos_id` pode receber `null` — comportamento benigno aqui, mas frágil. Ver capítulo 5.

### 1.3 `Prefecture\DatatableController@get_orgaos`

`v6/app/Http/Controllers/Prefecture/DatatableController.php:258-262`

```php
public function get_orgaos()
{
    $orgaos = DB::table("a_orgaos")->get();
    return DataTables::of($this->struct_views($orgaos, 'orgaos'))->make(true);
}
```

E o injetor de flags de permissão (`struct_views`, linhas 504-520):

```php
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;
}
```

Resultado JSON por linha: `value`, `titulo`, `id`, `galeria`, `slug`, `edit`, `delete`, `active`, `inactivate`.

### 1.4 Form Request — `App\Http\Requests\ContentRequest`

`v6/app/Http/Requests/ContentRequest.php`

- `authorize()` (linha 19-22) retorna **`true`**: a autorização é feita exclusivamente pelo middleware.
- `rules()` (linha 29) resolve o `slug` da categoria a partir de `category_slug` (request) ou de
  `category_contents_id` (banco) e então aplica o `switch ($slug)`.
- Caso `orgaos` (linhas 205-214), **compartilhado com `a-cidade`**:

```php
case 'orgaos':
case 'a-cidade':
    $rules = [
        "title" => "required|max:120",
        "category_contents_id" => "required|numeric",
        "text" => "required",
        "link" => "max:2999",
        "archive_highlight" => "max:20000|mimes:jpg,jpeg,png,gif,webp",
    ];
    break;
```

| Campo | Regra | Observação |
|---|---|---|
| `title` | `required|max:120` | obrigatório, ≤ 120 caracteres |
| `category_contents_id` | `required|numeric` | id da categoria (32) |
| `text` | `required` | corpo do órgão (HTML do Summernote) |
| `link` | `max:2999` | link do vídeo YouTube (o formulário limita a 200) |
| `archive_highlight` | `max:20000|mimes:jpg,jpeg,png,gif,webp` | imagem de destaque; 20000 = KB (5 MB é o limite do JS) |

> O bloco de `archive` (linhas 37-56) valida, **antes** do `switch`, que os arquivos enviados em
> `archive[]` tenham extensão `rar, zip, doc, docx, pdf, odt, xls, xlsx, xlsb, xslm, ppt, pptx` e
> aborta com `back()->withErrors(...)`.

### 1.5 Permissões do CMS

| Recurso | Permissões (papel `administrador`) |
|---|---|
| `resources.slug = 'orgaos'` (`modulo_id = 17`) | `orgaos.index`, `orgaos.create`, `orgaos.edit`, `orgaos.delete`, `orgaos.inactive`, `orgaos.inactivate`, `orgaos.restore`, `orgaos.show` |

O predicado `has_access()` (`v6/app/Library/Helper/appHelpers.php:22-42`) delega a
`cartalyst/sentinel` (`Sentinel::inRole('super-admin')` sempre retorna `true`).

O menu lateral do CMS exibe a entrada "Órgãos" apenas se `has_access("orgaos.index")` **e**
`modulos.status` forem verdadeiros (`v6/app/Providers/MenuServiceProvider.php`, laço sobre `menu_orders`).
Como `modulos.status` de `orgaos` é `false` no banco atual, **a entrada do menu não aparece**, embora a
rota e a permissão existam.

## 2. Portal — público

### 2.1 Tabela de rotas

| Método | URI | Nome da rota | Controller@action | Middleware |
|---|---|---|---|---|
| ANY | `/a-instituicao/orgaos/{slug}` | `a-instituicao-orgaos.index` | `Portal\PrefectureController@getIndex` | `check.site.manutention` |
| ANY | `/a-instituicao/{slug}` | `a-instituicao.index` | `Portal\PrefectureController@getIndex` | `check.site.manutention` |
| ANY | `/estrutura-organizacional` | `estrutura-organizacional.index` | `Portal\OrganizationalStructureController@index` | `check.site.manutention` |

Trecho real (`v6/routes/web.php:78-83`):

```php
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');
});
```

### 2.2 `Portal\PrefectureController@getIndex`

`v6/app/Http/Controllers/Portal/PrefectureController.php:30-99`. O ramo específico de órgãos (linhas 71-88):

```php
if ($category->slug == 'orgaos') {
    $date = date('Y-m-d H:i:s');
    $contentNew->orgaos = $contentNew->orgaos
        ->where("publication_date", "<=", $date);
    $eventsList = $eventsList
        ->where('orgaos_id', $contentNew->id)
        ->limit(5)
        ->get();

    return view('portal.layouts.' . $this->getTemplateActived() . '.informacoes-instituicao.a-instituicao.show', [
        'contentNew' => $contentNew,
        'title' => $title,
        'linkVideo' => $linkVideo,
        'sidenews' => $contentNew->orgaos,
        'eventsList' => $eventsList,
        'model' => $this->getTemplateActived()
    ]);
}
```

Observações:

- O ramo de órgãos **não** entrega `news_secretary` à view; logo o slot `@slot('noticias')` das views
  `a-instituicao/show.blade.php` não é renderizado (a condição `isset($news_secretary) && $news_secretary->count() != 0`
  falha).
- `$news_secretary` ainda é calculado (linha 38) com `->paginate(6)` e `where('secretaria', $slug1)` —
  para um slug de órgão essa consulta retorna vazio ( wasted query, ver capítulo 5).
- `$contentNew->orgaos` é um `hasMany` **sem limite no SQL**; o filtro de `publication_date` é aplicado
  sobre a Coleção já materializada.
- `eventsList` é limitado a 5 **no SQL** (`->limit(5)->get()`) — este é o caminho correto.

### 2.3 Breadcrumbs

`v6/routes/breadcrumbs.php:214-221`

```php
// Home > A Prefeitura > Órgãos >
Breadcrumbs::register('orgaos', function ($breadcrumbs, $content) {
    $breadcrumbs->parent('home');
    $breadcrumbs->push('A Instituição', url('/#'));
    $breadcrumbs->push('Órgãos', url('/#'));
    $breadcrumbs->push($content->title, route('a-instituicao.index', $content->slug));
});
```

> O último push usa `a-instituicao.index` (prefixo genérico), não `a-instituicao-orgaos.index`.
> Como ambas apontam para `PrefectureController@getIndex`, a URL gerada é `/a-instituicao/<slug>` — que
> **não** é a rota do órgão (`/a-instituicao/orgaos/<slug>`). O breadcrumb, portanto, aponta para a URL
> errada. Ver capítulo 8.

### 2.4 `OrganizationalStructureController` — NÃO é o módulo Órgãos

`v6/app/Http/Controllers/Portal/OrganizationalStructureController.php`:

```php
public function index()
{
    $contents = $this->getStaticPageData('estrutura-organizacional');

    return view('portal.layouts.'.$this->getTemplateActived().'.transparencia-fiscal.estrutura-organizacional',
        compact('contents'));
}
```

`getStaticPageData()` (`v6/app/Http/Traits/GetInformationsPrefectureTrait.php:60-63`) lê a tabela
`static_pages` pelo slug `estrutura-organizacional`. **A rota `/estrutura-organizacional` é uma página
estática e não tem relação com a categoria 32 nem com a view `a_orgaos`.**

## 3. Resumo das superfícies

| Superfície | Prefixo | Autenticação | Autorização | Leitura de dados |
|---|---|---|---|---|
| CMS | `/cms` | Sentinel (login) | `has_access` + `CheckRoutePermission` | View SQL `a_orgaos` (grade) / Eloquent (formulário) |
| Portal | `/` (público) | Nenhuma | Nenhuma | Eloquent `Content` |
| API | `/api/v1` | Nenhuma | Nenhuma | Eloquent `Content` + `archives` |

> A superfície de dados abertos (rota, `OrgaoController`, base compartilhada, `OrgaoRequest`,
> `OrgaoResource` e contrato JSON) está detalhada em `09-API-E-DADOS-ABERTOS.md`.
