# Secretarias — Rotas e Controladores

> Tabelas de rota conferidas com `php artisan route:list` no container `v6_6f22e1bd-app-1`.
> Middleware omitido na coluna "Middleware" = apenas `web` (+ `check.site.manutention` no portal).

---

## 1. Rotas do CMS (`v6/routes/basepro.php`, prefixo `/cms`)

### 1.1 CRUD de secretarias (genérico)

| Método | URI | Nome | Controller@ação | Middleware |
|---|---|---|---|---|
| GET | `/cms/conteudos/secretarias` | `listar-conteudo` | `Prefecture\ContentController@genericIndex` | `web, check.route.permission` |
| GET | `/cms/conteudos/cadastrar/secretarias` | `cadastrar-conteudo` | `Prefecture\ContentController@genericCreate` | `web, check.route.permission` |
| GET | `/cms/conteudos/editar/secretarias/{id}` | `editar-conteudo` | `Prefecture\ContentController@genericEdit` | `web, check.route.permission` |
| POST | `/cms/conteudos` | — | `Prefecture\ContentController@store` | `web, check.route.permission` |
| PUT/PATCH | `/cms/conteudos/{id}` | — | `Prefecture\ContentController@update` | `web, check.route.permission` |
| DELETE | `/cms/conteudo/delete/{id}` | `generic.conteudo.delete` | `Prefecture\ContentController@genericDestroy` | `web, check.route.permission` |
| POST | `/cms/conteudo/deleteAll` | `ajax.generic.content.deleteAll` | `Prefecture\ContentController@genericDeleteAll` | `web, check.route.permission` |

As rotas acima são **genéricas por slug** (`v6/routes/basepro.php:235-237`). O dispatch
para a view do módulo acontece em `ContentController@genericIndex()`:

```php
if ($slug === 'secretarias') return $this->returnIndex('Secretarias', $slug);   // :96
```

e em `genericGenerate()`:

```php
if ($slug === 'secretarias') return $this->returnData('Secretarias', $slug, null, ['types_sizes' => $this->typesSizes->all()], $id);  // :168
```

`returnData()` monta `$contentData` (destaque, galeria, `categoryContent`, `types_sizes`) e
resolve a view `prefecture.contents.secretarias.{create,edit}`.

### 1.2 Departamentos (`categorias_secretarias`)

| Método | URI | Nome | Controller@ação | Middleware |
|---|---|---|---|---|
| GET | `/cms/conteudo/secretarias/secretarias-categorias` | `secretarias-categorias.index` | `CategorySecretaryController@index` | `web, check.route.permission` |
| GET | `/cms/conteudo/secretarias/secretarias-categorias/create` | `secretarias-categorias.create` | `@create` | idem |
| POST | `/cms/conteudo/secretarias/secretarias-categorias` | `secretarias-categorias.store` | `@store` | idem |
| GET | `/cms/conteudo/secretarias/secretarias-categorias/{secretarias_categoria}` | `secretarias-categorias.show` | `@show` | idem |
| GET | `/cms/conteudo/secretarias/secretarias-categorias/{secretarias_categoria}/edit` | `secretarias-categorias.edit` | `@edit` | idem |
| PUT/PATCH | `/cms/conteudo/secretarias/secretarias-categorias/{secretarias_categoria}` | `secretarias-categorias.update` | `@update` | idem |
| DELETE | `/cms/conteudo/secretarias/secretarias-categorias/{secretarias_categoria}` | `secretarias-categorias.destroy` | `@destroy` | idem |
| POST | `/cms/secretarias/secretarias-categorias` | `secretarias-categorias.destroyAll` | `@destroyAll` | idem |

Registro: `v6/routes/basepro.php:366-367`.

### 1.3 Páginas de secretaria (`paginas_secretarias`)

| Método | URI | Nome | Controller@ação | Middleware |
|---|---|---|---|---|
| GET | `/cms/conteudo/secretarias/secretarias-paginas` | `secretarias-paginas.index` | `PagesSecretaryController@index` | `web, check.route.permission` |
| GET | `/cms/conteudo/secretarias/secretarias-paginas/create` | `secretarias-paginas.create` | `@create` | idem |
| POST | `/cms/conteudo/secretarias/secretarias-paginas` | `secretarias-paginas.store` | `@store` | idem |
| GET | `/cms/conteudo/secretarias/secretarias-paginas/{secretarias_pagina}` | `secretarias-paginas.show` | `@show` | idem |
| GET | `/cms/conteudo/secretarias/secretarias-paginas/{secretarias_pagina}/edit` | `secretarias-paginas.edit` | `@edit` | idem |
| PUT/PATCH | `/cms/conteudo/secretarias/secretarias-paginas/{secretarias_pagina}` | `secretarias-paginas.update` | `@update` | idem |
| DELETE | `/cms/conteudo/secretarias/secretarias-paginas/{secretarias_pagina}` | `secretarias-paginas.destroy` | `@destroy` | idem |
| POST | `/cms/secretarias/secretarias-paginas` | `secretarias-paginas.destroyAll` | `@destroyAll` | idem |
| DELETE | `/cms/secretarias_paginas/arquivo/{id}` | *(sem nome)* | `@deleteArchive` | idem |

Registro: `v6/routes/basepro.php:368-370`.

### 1.4 Menu das secretarias

| Método | URI | Nome | Controller@ação | Middleware |
|---|---|---|---|---|
| GET | `/cms/conteudo/secretarias/menus/{id}` | `secretaria-menus.index` | `MenuSecretaryController@index` | `web, check.route.permission` |
| GET | `/cms/get_categorias/secretarias/{id}` | *(sem nome)* | `MenuSecretaryController@returnSecretarias` | idem |
| POST | `/cms/order_categorias/secretarias` | *(sem nome)* | `MenuSecretaryController@orderCategory` | idem |

Registro: `v6/routes/basepro.php:371-373`.

### 1.5 Grades (DataTable)

| Método | URI | Nome | Controller@ação | Consulta |
|---|---|---|---|---|
| GET | `/cms/datatable/secretarias` | `datatable.secretarias` | `DatatableController@get_secretarias` (:198) | `DB::table('secretarias')->get()` |
| GET | `/cms/datatable/secretarias_categorias` | `datatable.` *(nome vazio)* | `@get_secretaria_categorias` (:185) | `DB::table('secretarias_categorias')->get()` |
| GET | `/cms/datatable/secretarias_paginas` | `datatable.` *(nome vazio)* | `@get_secretaria_paginas` (:191) | `DB::table('secretarias_paginas')->get()` |

⚠ As duas últimas rotas **não têm `->name()`** — o nome gerado é literalmente `datatable.`.
Elas não são protegidas pelo nome, mas o middleware `check.route.permission` continua
aplicado (veja `CheckRoutePermission::openRoutes`, que libera `datatable`).

## 2. Rotas do Portal (`v6/routes/web.php`)

| Método | URI | Nome | Controller@ação | Middleware |
|---|---|---|---|---|
| ANY | `/a-instituicao/{slug}` | `a-instituicao.index` | `Portal\PrefectureController@getIndex` | `web, check.site.manutention` |
| ANY | `/a-instituicao/secretarias/{slug}` | `a-instituicao.index` **(duplicado!)** | `Portal\PrefectureController@getIndex` | idem |
| ANY | `/a-instituicao/secretarias/paginas/{slug}/{id}` | `secretarias-pages.index` | `Portal\SecretariaController@show_pages_secretary` | idem |
| GET | `/portal/{slug1}/{slug2}` | `conteudo.portal.show` | `Portal\PortalController@contentList` | idem |

Registro: `v6/routes/web.php:78-82` e `:61`.

### 2.1 Bug de nome duplicado (verificado em runtime)

`v6/routes/web.php:79-80` registra **duas** rotas com o mesmo nome `a-instituicao.index`:

```php
Route::any('/{slug}', 'Portal\PrefectureController@getIndex')->name('a-instituicao.index');
Route::any('secretarias/{slug}', 'Portal\PrefectureController@getIndex')->name('a-instituicao.index');
```

O Laravel resolve o nome para a **última** definição. Consequências:

| Chamada | URL gerada | Correto? |
|---|---|---|
| `route('a-instituicao.index', 'saude')` | `/a-instituicao/secretarias/saude` | ⚠ só funciona por acidente (a secretaria) |
| `route('a-instituicao.index', 'a-instituicao')` | `/a-instituicao/secretarias/a-instituicao` | ❌ quebra a página "A Instituição" |
| `route('a-instituicao.index', $contentNew->slug)` em `v6/routes/breadcrumbs.php:205` | idem | ❌ |

O breadcrumb de "A Instituição" e de "Secretarias" (`v6/routes/breadcrumbs.php:202-212`)
usa `route('a-instituicao.index', $content->slug)` — com o nome duplicado, o breadcrumb
de "A Instituição" aponta para `/a-instituicao/secretarias/{slug}`.

### 2.2 Bug de rota inexistente (verificado em runtime)

`v6/app/Http/Controllers/Portal/PortalController.php:332`:

```php
case 'secretarias': return redirect(route('a-instituicao-secretarias.index', $slug2));
```

A rota `a-instituicao-secretarias.index` **não existe na v6** (existe na v5 como
`a-prefeitura-secretarias.index`, `v5/routes/web.php:71`). Resultado:
`/portal/secretarias/{slug}` → `InvalidArgumentException: Route [a-instituicao-secretarias.index] not defined`
→ **HTTP 500**. O `catch (\Throwable $th) {}` no fim de `contentList()` engole a exceção
e devolve uma resposta vazia (status 200 sem corpo) — o que torna o diagnóstico confuso.

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

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

Registro: `v6/routes/api.php:18-20` (grupo `prefix => v1`, `as => v1.`, sub-grupo
`prefix => secretarias`, `as => secretarias.`).

Parâmetros aceitos (`SecretariaRequest`): `titulo` (string), `descricao` (string),
`limite` (int, default documentado 100).

## 4. Controladores — detalhe de comportamento

### 4.1 `CategorySecretaryController`

| Método | Comportamento |
|---|---|
| `__construct` | injeta `ICategorySecretaryRepository`; `$this->title = 'Departamentos'`; `$this->secretarias = DB::table('secretarias')->get(['id','titulo as name'])`; aplica `check.route.permission` |
| `index()` | view `prefecture.contents.secretarias.categoria.index` (só o título; a grade é AJAX) |
| `show($id)` | mesma view com `$categoria = $id` |
| `create()` | view `...categoria.create` + `$secretarias` (drop-down) |
| `store()` | `repository->upInsert($request)`; flash de sucesso/erro; `redirect()->route('secretarias-categorias.index')` |
| `edit($id)` | view `...categoria.create` + `data` = `repository->getModel()->where('id',$id)->first()` |
| `update($request,$id)` | `repository->upInsert($request, $id)` |
| `destroy($id)` | `repository->getModel()->destroy($id)` → JSON `{success}` 200 ou `{erro}` 500 |
| `destroyAll($request)` | `repository->destroyAll($request->toDelete)` → JSON |

### 4.2 `PagesSecretaryController`

| Método | Comportamento |
|---|---|
| `__construct` | injeta `IPagesSecretaryRepository`; `$this->archive = new Archive()`; `$this->secretarias = new CategoriasSecretaria()`; `check.route.permission` |
| `index()` | view `prefecture.contents.secretarias.paginas.index` |
| `create()` | view `...paginas.create` + `categorias` = `CategoriasSecretaria::all()` (**todos** os departamentos, sem filtro por secretaria) |
| `store()` | `upInsert` → `$result->id`; `insertArchive()` por arquivo; `insertImage()` para o destaque |
| `edit($id)` | `repository->getModel()->where('id',$id)->with('highlight','archives')->first()` |
| `update($request,$id)` | `upInsert($request,$id)`; **`insertArchive()` NÃO remove os arquivos anteriores** → anexos duplicam a cada edição |
| `destroy($id)` | `destroy()` → JSON |
| `destroyAll($request)` | `repository->destroyAll($request->toDelete)` → JSON |
| `insertImage($file,$id)` | grava em `content/secretarias_paginas/{id}/destaque`; **apaga** os archives com `highlight = true` do mesmo `reference_id` antes de criar o novo |
| `insertArchive($file,$id,$description)` | grava em `content/secretarias_paginas/{id}/arquivos`; `highlight = false`; `title` = descrição ou nome do arquivo |
| `deleteArchive($id)` | `archive->destroy($id)` → JSON |

### 4.3 `MenuSecretaryController`

| Método | Comportamento |
|---|---|
| `__construct` | `$this->secretarias = DB::table('secretarias')->get(['id','titulo as name'])`; `check.route.permission` |
| `index($id)` | view `prefecture.contents.secretarias.menu.index` com `secretaria` (view SQL) e `categorias` = `CategoriasSecretaria::with('contentsCategory')->where('secretarias_id',$id)->get()` |
| `returnSecretarias($id)` | JSON: `CategoriasSecretaria::with('contentsCategory')->where('secretarias_id',$id)->orderBy('order','ASC')->get()` |
| `orderCategory($request)` | reescreve `order` de 1..N na ordem de `$request->ids`; devolve a **string** `'true'` ou `'false'` (não JSON, não booleano) |

### 4.4 `Portal\PrefectureController@getIndex`

Ver fluxograma completo em `01-ARQUITETURA.md` §2. Trechos decisivos:

```php
$news_secretary = $this->content->with('categoryContents', 'archives')
    ->where('secretaria', $slug1)->orderBy('id', 'DESC')->paginate(6);   // :38
$contentNew = $this->getContentDataWith($slug1);
$title = $contentNew->title;
$linkVideo = isset($contentNew->linksUnique) ? embedVideo($contentNew->linksUnique->link) : null;

if (empty($contentNew) and !isset($contentNew)) {
    return view("admin.errors.conteudo-nao-encontrado");
}

$this->updateClicks($contentNew->id);

$category = $this->categoryContent->find($contentNew->category_contents_id);

if ($category->slug == 'secretarias') {                                   // :52
    $date = date('Y-m-d H:i:s');
    $contentNew->secretarias = $contentNew->secretarias
        ->where("publication_date", "<=", $date);
    $eventsList = $eventsList->where('secretarias_id', $contentNew->id)->limit(5)->get();
    $menu = $this->menu_secretaria->where('secretarias_id', $contentNew->id)->get();
    return view('portal.components.secretarias.show', [...]);
}
```

### 4.5 `Portal\SecretariaController@show_pages_secretary`

```php
public function show_pages_secretary($slug, $id)
{
    $secretaria = $this->content->where('slug', $slug)->first();
    return view(
        'portal.components.secretarias.paginas.index',
        [
            'content' => $this->model->find($id),
            'secretaria' => $secretaria,
            'model' => $this->getTemplateActived(),
            'menus' => $this->categorias->where('secretarias_id', $secretaria->id)->get()
        ]
    );
}
```

⚠ Sem validação de existência: `find($id)` vazio gera **erro 500** na view
(`$content->title` sobre null). Também não verifica se a página pertence à
secretaria informada no `{slug}`.

### 4.6 `Swagger\V1\Portal\SecretariaController@index`

```php
public function index(SecretariaRequest $request)
{
    $title = $request->get('titulo');
    $description = $request->get('descricao');

    $contents = $this->getContent();

    if($title) {
        $contents->whereRaw($this->caseInsensitiveAccentFreeSearch($title, 'title'));
    }

    if($description) {
        $contents->whereRaw($this->caseInsensitiveAccentFreeSearch($description, 'text'));
    }

    $contents = $contents->get();

    return SecretariaResource::collection($contents);
}
```

⚠ **Bug confirmado:** os dois `whereRaw()` são chamados **sem reatribuir** o resultado
(`$contents->whereRaw(...)` em vez de `$contents = $contents->whereRaw(...)`).
Em Eloquent, `whereRaw()` retorna uma **nova** query builder; o original fica
inalterado. Logo os filtros `titulo`/`descricao` **nunca são aplicados** — a API
ignora esses parâmetros. (O mesmo padrão existe no controller de Órgãos —
ver `../orgaos/`.)

## 5. Form Requests — o que validam

| Request | Campo | Regra | Mensagem |
|---|---|---|---|
| `ContentRequest` (case `secretarias`) | `title` | `required|max:120` | padrão Laravel |
| | `category_contents_id` | `required|numeric` | padrão |
| | `text` | `required` | padrão |
| | `link` | `max:2999` | padrão |
| | `archive_highlight` | `max:20000\|mimes:jpg,jpeg,png,gif,webp` | padrão |
| | `width`, `height` | `required|numeric` **quando `type_size_id == 4`** (Personalizado) | padrão |
| `SecretariasCategoriasRequest` | `name` | `required` | "O nome do departamento é obrigatório." |
| | `secretarias_id` | `[SelectRule, required]` | "A secretaria é obrigatória." |
| `SecretariasPaginasRequest` | `title` | `required` | ⚠ **não há mensagem** — `messages()` define `'name.required'` (chave errada) |
| | `categoria_id` | `[SelectRule, required]` | ⚠ idem |
| | `text` | `required` | ⚠ idem |
| `SecretariaRequest` (API) | `titulo` | `nullable|string` | — |
| | `descricao` | `nullable|string` | — |

⚠ `authorize()` retorna `true` em **todos** os requests deste módulo: a autorização é
inteiramente delegada ao middleware `check.route.permission` (CMS) ou à ausência de
middleware (API pública).

## 6. Permissões

### 6.1 Como o CMS autoriza

`v6/app/Http/Middleware/CheckRoutePermission.php`:

1. Se não logado → `redirect()->route('login')`.
2. Se logado e **não** `super-admin` → `hasAccess()`:
   - `$access` = nome da rota atual; `$accessSegment` = segmento da URL + sufixo
     (`cadastrar`→`.create`, `editar`→`.edit`, senão `.index`).
   - `hasAnyAccess([$access, $accessSegment])` no Sentinel.
3. Rotas cujo nome começa com `datatable`, `ajax`, `dashboard`, `error`, `login`,
   `logout` são **abertas** (`openRoutes`).

Derivação para este módulo:

| URL | Segmento derivado | Permissão exigida |
|---|---|---|
| `/cms/conteudos/secretarias` | `secretarias.index` | `secretarias.index` |
| `/cms/conteudos/cadastrar/secretarias` | `secretarias.create` | `secretarias.create` |
| `/cms/conteudos/editar/secretarias/1` | `secretarias.edit` | `secretarias.edit` |
| `/cms/conteudo/secretarias/secretarias-categorias` | `secretarias-categorias.index` | `secretarias-categorias.index` |
| `/cms/conteudo/secretarias/secretarias-paginas` | `secretarias-paginas.index` | `secretarias-paginas.index` |
| `/cms/conteudo/secretarias/menus/1` | `secretaria-menus.index` | `secretaria-menus.index` |

### 6.2 Permissões cadastradas

`resources` (tabela de permissões do Sentinel):

| id | slug | name | modulo_id |
|---|---|---|---|
| 15 | `secretarias` | Secretarias | 18 |
| 16 | `secretarias-categorias` | Departamentos das Secretarias | 18 |
| 17 | `secretarias-paginas` | Páginas internas de Secretarias | 18 |

A role **Administrador** (`roles.id = 2`) possui `*.index/.create/.edit/.delete/.show/.inactive/.inactivate/.restore`
para `secretarias`, `secretarias-categorias` e `secretarias-paginas`.

A tela de menu (`secretaria-menus.index`) é a **única** rota do módulo cujo nome de
permissão **não** corresponde a um `resources.slug` — quem tiver `secretarias.index`
mas não `secretaria-menus.index` **não consegue abrir a tela de menu** pelo CMS.

### 6.3 Autorização no portal e na API

Portal e API **não exigem autenticação**. A única restrição do portal é
`check.site.manutention` (modo manutenção do site).

## 7. Saída da API (formato)

`SecretariaResource` (`v6/app/Http/Resources/Swagger/V1/Portal/SecretariaResource.php`):

```php
return [
    "titulo" => data_get($this, "title"),
    "descricao" => $this->when($description != "", $description),
    "interacoes" => data_get($this, "clicks"),
    "slug" => data_get($this, "slug"),
    "arquivos" => ArchiveResource::collection($this->whenLoaded("archives"))
];
```

`descricao` é o `text` **sem HTML** (`strip_tags(trim(...))`).
`ArchiveResource` devolve `titulo`, `descricao`, `tipo` (MIME) e `url`
(`url($path)` — sem prefixo `/storage`).

Envelope: `{"data": [ ... ]}` (JsonResource padrão do Laravel 5.5).
