# Comitê de Investimentos — Rotas e Controllers

## 1. Rotas do portal público

Definidas em `v6/routes/web.php:219-221`, dentro do grupo público (sem
middleware de autenticação):

| Método | URI | Nome da rota | Controller@action | Middleware |
|---|---|---|---|---|
| GET | `/comite-de-investimentos` | `portal.comite-de-investimentos.index` | `Portal\ComiteDeInvestimentosController@index` | — |
| GET | `/comite-de-investimentos/{slug}` | `portal.comite-de-investimentos.show` | `Portal\ComiteDeInvestimentosController@show` | — |
| GET | `/comite-de-investimentos/{slug1}/{slug2}` | `portal.comite-de-investimentos.view` | `Portal\ComiteDeInvestimentosController@view` | — |

Trecho literal de `v6/routes/web.php`:

```php
Route::get('/comite-de-investimentos', 'Portal\ComiteDeInvestimentosController@index')->name('portal.comite-de-investimentos.index');
Route::get('/comite-de-investimentos/{slug}', 'Portal\ComiteDeInvestimentosController@show')->name('portal.comite-de-investimentos.show');
Route::get('/comite-de-investimentos/{slug1}/{slug2}', 'Portal\ComiteDeInvestimentosController@view')->name('portal.comite-de-investimentos.view');
```

Semântica dos parâmetros:

| Rota | Parâmetro | Significado |
|---|---|---|
| `.show` | `{slug}` | Slug da **subcategoria** do comitê (ex.: `atas-2024`) |
| `.view` | `{slug1}` | Slug da subcategoria |
| `.view` | `{slug2}` | Slug do **documento** (linha de `contents`) |

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

O módulo não tem rotas próprias: usa o roteamento **genérico** de conteúdo e
categoria.

### 2.1 Conteúdo (atas)

| Método | URI | Nome | Controller@action | Arquivo:linha |
|---|---|---|---|---|
| GET | `/cms/conteudos/comite-de-investimentos` | `listar-conteudo` | `Prefecture\ContentController@genericIndex` | `v6/routes/basepro.php:235` |
| GET | `/cms/conteudos/cadastrar/comite-de-investimentos` | `cadastrar-conteudo` | `Prefecture\ContentController@genericCreate` | `v6/routes/basepro.php:236` |
| GET | `/cms/conteudos/editar/comite-de-investimentos/{id}` | `editar-conteudo` | `Prefecture\ContentController@genericEdit` | `v6/routes/basepro.php:237` |
| POST | `/cms/conteudo` | `conteudo.store` | `Prefecture\ContentController@store` | `v6/routes/basepro.php:174` |
| PUT | `/cms/conteudo/{id}` | `conteudo.update` | `Prefecture\ContentController@update` | `v6/routes/basepro.php:174` |
| DELETE | `/cms/conteudo/delete/{id}` | `generic.conteudo.delete` | `Prefecture\ContentController@genericDestroy` | `v6/routes/basepro.php:223` |
| POST | `/cms/ajax/conteudo/deleteAll` | `ajax.generic.content.deleteAll` | `Prefecture\ContentController@genericDeleteAll` | `v6/routes/basepro.php:141` |
| POST | `/cms/marcar-destaque` | `marcar.destaque` | `Prefecture\ContentController@setHighlights` | `v6/routes/basepro.php:346` |
| POST | `/cms/set-status` | `set.status` | `Prefecture\ContentController@setStatus` | `v6/routes/basepro.php:347` |

### 2.2 Categorias do comitê

| Método | URI | Nome | Controller@action | Arquivo:linha |
|---|---|---|---|---|
| GET | `/cms/categorias/comite-de-investimentos` | `listar-categorias` | `Prefecture\CategoryContentController@genericIndex` | `v6/routes/basepro.php:238` |
| GET | `/cms/categorias/cadastrar/comite-de-investimentos` | `cadastrar-categorias` | `Prefecture\CategoryContentController@genericCreate` | `v6/routes/basepro.php:239` |
| GET | `/cms/categorias/editar/comite-de-investimentos/{id}` | `editar-categorias` | `Prefecture\CategoryContentController@genericEdit` | `v6/routes/basepro.php:240` |
| POST | `/cms/categoria-conteudo` | `categoria-conteudo.store` | `Prefecture\CategoryContentController@store` | `v6/routes/basepro.php:173` |
| PUT | `/cms/categoria-conteudo/{id}` | `categoria-conteudo.update` | `Prefecture\CategoryContentController@update` | `v6/routes/basepro.php:173` |
| DELETE | `/cms/categoria-conteudo/{id}` | `categoria-conteudo.destroy` | `Prefecture\CategoryContentController@destroy` | `v6/routes/basepro.php:173` |
| POST | `/cms/ajax/ajust-categorias` | `ajust.categorias` | `Prefecture\CategoryContentController@ajustOrder` | `v6/routes/basepro.php:161` |

### 2.3 DataTable

| Método | URI | Nome | Controller@action | Arquivo:linha |
|---|---|---|---|---|
| GET | `/cms/datatable/comite_de_investimentos` | `comite_de_investimentos` | `Prefecture\DatatableController@get_comite_de_investimentos` | `v6/routes/basepro.php:399` |

## 3. Controller do portal — API do método

Arquivo: `v6/app/Http/Controllers/Portal/ComiteDeInvestimentosController.php`
(namespace `App\Http\Controllers\Portal`, estende
`App\Http\Controllers\Admin\Controller`).

### `index()` — linhas 15-22

```php
$categoryFirst = CategoryContent::whereHas('categoryContents', function ($query) {
    $query->where('slug', '=', 'comite-de-investimentos');
})->orderBy('order')->first();

return redirect()->route('portal.comite-de-investimentos.show', $categoryFirst->slug);
```

Comportamento: redireciona para a **primeira** subcategoria (menor `order`). Se
não houver subcategoria, `$categoryFirst` é `null` e a chamada a `->slug` gera
**erro 500** (ver `08-GAPS-E-DIVIDAS.md`).

### `show($slug, Request $request)` — linhas 24-115

| Etapa | Linha | Comportamento |
|---|---|---|
| Resolve categoria-pai | 27 | `CategoryContent::where('slug','comite-de-investimentos')->where('category_contents_id', null)->first()->id` — **`->first()` sem verificação** gera erro 500 se a categoria-pai não existir |
| Query morta | 29-31 | `CategoryContent::whereHas('categoryContents', function ($query) {})` — closure vazia, código inerte |
| Subcategorias | 33 | `where('category_contents_id', $comite_category_id)->orderBy('order','asc')->get()` |
| Valida slug | 35-37 | Se a categoria não existir, `redirect('/publicacoes')` |
| Lista | 41-43 | `Content::where('publication_date','<=',$current_date)->select(campos)->where('category_contents_id',$category->id)->orderBy('publication_date','desc')->orderBy('id','desc')->paginate(12)` |
| Anos | 45 | `DB::select("select extract(year from publication_date) as ano from contents WHERE category_contents_id = {$category->id} AND deleted_at IS NULL GROUP BY ano order by ano desc")` |
| Meses | 57 | Só é calculada **se** `$request->ano` estiver preenchido; SQL com `{$_GET['ano']}` interpolado |
| Filtros | 63-92 | `when($request->ano)`, `when($request->mes)`, `when(empty($request->mes))` (corta `publication_date <= now`), `when($request->texto)` com `whereRaw(translate ilike)` |
| AJAX | 100-113 | `Response::json(view("portal.layouts.{layout}.publicacoes.list")->render())` |
| Render | 108-112 | `view("portal.layouts.{layout}.comite-de-investimentos.show", compact(variáveis))` |

Filtro de texto (linhas 82-89):

```php
->when($request->texto, function ($query, $reqTexto) {
    $query->where(function ($query) use ($reqTexto) {
        $newTXT = clean_slug($reqTexto);
        $query->whereRaw("translate(lower(title), ' ''àáâãäéèëêíìïîóòõöôúùüûçÇ', '--aaaaaeeeeiiiiooooouuuucc') ilike '%" . $newTXT . "%'")
            ->orWhereRaw("translate(lower(text), ' ''àáâãäéèëêíìïîóòõöôúùüûçÇ', '--aaaaaeeeeiiiiooooouuuucc') ilike '%" . $newTXT . "%'");
    });
})
```

O texto passa por `clean_slug()` (`v6/app/Helpers/functions.php:823`), que
remove acentuação e caracteres especiais, **mas não escapa aspas simples** —
veja `05-REGRAS-E-PORTABILIDADE.md`.

### `view($slug1, $slug2)` — linhas 117-133

| Etapa | Comportamento |
|---|---|
| Busca | `Content::where('slug', $slug2)->first()` — **não** filtra por categoria nem por `deleted_at` |
| Corte | Se `publication_date > now()`, redireciona para o index do comitê |
| Anexos | `Archive::where('contents_id',$content->id)->where('highlight', null)->get()` |
| PDF | `Archive::where('contents_id',$content->id)->where('view_pdf', true)->first()` |
| Cliques | `$content->update(['clicks' => $content->clicks + 1])` |
| Render | `view("portal.layouts.{layout}.comite-de-investimentos.view", compact(variáveis))` |

## 4. Controller CMS — `ContentController`

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

| Método | Linhas | Ramo do comitê |
|---|---|---|
| `genericIndex($slug)` | 99-118 | Linha 113: `if ($slug === 'comite-de-investimentos') return $this->returnIndex('Comitê de Investimentos', $slug);` |
| `returnIndex($title, $slug, $alt_click)` | 120-126 | Chama `generateLog($title)` e `menuClicks($slug)`; renderiza `prefecture.contents.{slug}.index` |
| `genericCreate($slug)` | 144-146 | `genericGenerate($slug)` |
| `genericEdit($slug, $id)` | 157-160 | `genericGenerate($slug, $id)` |
| `genericGenerate($slug, $id)` | 162-263 | Linhas 226-234: ramo `comite-de-investimentos` monta `categoryContent` (pai) e `categoryContentSelect` (filhas) |
| `returnData()` | 275-312 | Monta `title`, `categoryContent`, `isModel5`, e em modo edição `content`, `archives`, `gallery`, `category`, `destaque` |
| `store(ContentRequest)` | 616+ | Grava `contents` e anexos; gera `slug` a partir do título; trata `category_slug == 'transparencia-fiscal'` (não se aplica ao comitê) |
| `update(ContentRequest, $id)` | 937+ | Atualização |
| `genericDestroy($id)` | 1369+ | Exclusão lógica |

Ramo do comitê em `genericGenerate` (linhas 226-234):

```php
if ($slug === 'comite-de-investimentos') {
    return $this->returnData('Comitê de Investimentos', $slug, null, [
        'categoryContent' => $this->categoryContent->where('slug', 'comite-de-investimentos')->whereNull('category_contents_id')->first(),
        'categoryContentSelect' => $this->categoryContent->whereHas('categoryContents', function ($query) {
            return $query->where('slug', 'comite-de-investimentos');
        })->get()
    ], $id);
}
```

## 5. Controller CMS — `CategoryContentController`

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

| Método | Linha | Ramo do comitê |
|---|---|---|
| `genericCreate` | 275-289 | Título "Categorias de Comitê de Investimento"; busca a categoria-pai e as filhas ordenadas por `order` |
| `genericEdit` | 388-399 | Título "Categorias de Comitês de Investimentos"; `case 'comite-de-investimentos'` |

Ambos registram log de atividade com a mensagem fixa
"Acessou criação de categoria de Requerimentos" — **texto copiado** de outro
módulo.

## 6. DataTable — `DatatableController`

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

```php
public function get_comite_de_investimentos()
{
    $comite = DB::table('comite_de_investimentos')->get();
    return DataTables::of($this->struct_views($comite, 'comite_de_investimentos'))->make(true);
}
```

`struct_views($data, $permission)` (linha 504) injeta `edit`, `delete`, `active`
e `inactivate` em cada linha conforme `has_access('comite_de_investimentos.*')`.

## 7. Form Requests e validação

O módulo **não tem Form Request próprio**. A validação vem de
`v6/app/Http/Requests/ContentRequest.php`, que resolve o slug do conteúdo e
aplica um `switch`:

- Não existe `case 'comite-de-investimentos'` — o comitê cai no **`default`**
  (linha 169 do arquivo):

```php
default:
    $rules = [
        "title" => "required|max:120",
        "category_contents_id" => "required|numeric",
        "archive.*.0" => "mimes:jpg,jpeg,png,bmp,gif,rar,zip,doc,docx,pdf,odt,xls,xlsx,xlsb,xslm,ppt,pptx,ods,wav,mp3,wma,mp4,mpg4,mkv,flv",
        "archive_highlight" => "max:20000|mimes:jpg,jpeg,png,gif,webp",
    ];
```

Consequências:

| Campo do formulário | Regra aplicada | Observação |
|---|---|---|
| `title` | required, max 120 | O formulário do comitê informa "Máximo de 150 caracteres" e seta `maxlen=150` — divergência |
| `category_contents_id` | required, numeric | Correto |
| `publication_date` | **não validado** | O formulário marca como obrigatório, mas a regra não existe |
| `text` | **não validado** | Idem |
| `slug` | **não validado** | Só `noticias` e `publicacoes` exigem `slug` no update |
| `archive.*` | mimes |tipos aceitos na galeria |

## 8. Repositórios

| Repositório | Caminho | Uso |
|---|---|---|
| `ContentRepositoryDefaultImpl` | `v6/app/Repositories/Eloquent/Prefecture/Content/ContentRepositoryDefaultImpl.php` | Gravação e exclusão de `contents` |
| `IContentRepository` | `v6/app/Repositories/Contracts/IContentRepository.php` | Contrato |
| Ligação | `v6/app/Providers/RepositoryServiceProvider.php:85` | `$this->app->bind(IContentRepository::class, ContentRepositoryDefaultImpl::class)` |

Não há repositório específico do comitê nem contrato dedicado.

## 9. Permissões exigidas

| Contexto | Permissão | Origem |
|---|---|---|
| Ver o menu do CMS | `comite-de-investimentos.index` + `modulos.status = true` | `v6/app/Providers/MenuServiceProvider.php:46` |
| Acessar o index do CMS | `comite-de-investimentos.index` | `has_access` / `check.route.permission` |
| Ver botões de cadastro/exclusão | `comite-de-investimentos.create`, `comite-de-investimentos.delete` | `v6/resources/views/prefecture/contents/comite-de-investimentos/index.blade.php` |
| Editar/excluir via DataTable | `comite-de-investimentos.edit`, `comite-de-investimentos.delete` | `struct_views` |
| Super-admin | Passa por tudo | `v6/app/Library/Helper/appHelpers.php:33` |

O guard é o **Sentinel** (`cartalyst/sentinel`); `super-admin` contorna todas as
verificações.

## 10. API Resources / presenters

Não há API Resource. O controller portal devolve **HTML renderizado** em JSON
(`Response::json(view(publicacoes.list)->render()))`) — ou seja, o "endpoint AJAX" do portal
devolve um fragmento HTML, não JSON estruturado. O CMS usa
`yajra/laravel-datatables-oracle`, que serializa a coleção com as flags injetadas
por `struct_views`.
