# A Instituição — Regras, Riscos e Portabilidade

> Versão analisada: `v6`. Comparações com `v5` quando relevantes.

---

## 1. Regras de negócio não óbvias

| # | Regra | Onde está | Efeito observável |
|---|---|---|---|
| R1 | A categoria do módulo é identificada por **slug** (`a-instituicao`), nunca por id, nos controllers e na view SQL. | `v6/app/Http/Controllers/Prefecture/ContentController.php:91,161`; `v6/database/migrations/2024_11_29_100640_update_view_a_instituicao.php` | Renomear o slug da categoria sem recriar a view esvazia a grade do CMS. |
| R2 | A view SQL filtra `deleted_at IS NULL` — conteúdos em soft delete somem da grade, mas continuam na tabela `contents`. | mesma migration | Conteúdo "excluído" no CMS não aparece na listagem, mas o registro permanece. |
| R3 | A grade ordena por `created_at DESC` e numera com `row_number()`. | idem | A coluna "value" (1, 2, 3…) é recalculada a cada consulta e não serve de chave. |
| R4 | O botão "Acessar Instituto" usa o `slug` que vem da **view**, e a view só o expôs a partir de `2024_11_29_100640_update_view_a_instituicao.php`. | `v6/public/assets/js/custom/contents/a-instituicao/datatable.js` | Antes dessa migration, o botão apontava para `/a-instituicao/undefined`. |
| R5 | O slug do conteúdo é gerado por `Str::slug(title)`; em colisão vira `slug-<maxid+1>`. | `v6/app/Http/Controllers/Prefecture/ContentController.php:619,676-679` | Títulos iguais geram URLs diferentes e imprevisíveis. |
| R6 | A leitura pública **não** filtra por `status` nem por `publication_date`. | `v6/app/Http/Traits/GetInformationsPrefectureTrait.php:26-29` | Um conteúdo com `status = false` continua público. |
| R7 | `contents.clicks` é incrementado a cada visita, por um `UPDATE` avulso. | `v6/app/Http/Traits/GetInformationsPrefectureTrait.php:104-108` | Contador não atômico (perde incrementos sob concorrência) e um `UPDATE` por visita. |
| R8 | A galeria só aparece se `contents.galery = 1` **e** `archiveGallery` não for vazia. | `v6/resources/views/portal/layouts/modelo1/informacoes-instituicao/a-instituicao/show.blade.php:24-29` | Marcar "Ativar Galeria" sem anexos não exibe nada. |
| R9 | A galeria só aceita imagens (filtro por extensão) e ignora PDFs. | `v6/resources/views/portal/components/galerias/index.blade.php:23` | Anexos `.pdf` na galeria ficam invisíveis. |
| R10 | O vídeo aparece se `linksUnique->link` existir. | `v6/resources/views/portal/layouts/modelo1/informacoes-instituicao/a-instituicao/show.blade.php:35-40` | O campo `link` do formulário é o único ponto de entrada. |
| R11 | As "Últimas notícias" filtram por `contents.secretaria = $slug1` — o **slug** do conteúdo institucional, não o id da secretaria. | `v6/app/Http/Controllers/Portal/PrefectureController.php:38` | Uma página institucional só mostra notícias se o conteúdo tiver o slug de alguma secretaria — o que, no banco atual, nunca ocorre (`secretaria` é 100% NULL). |
| R12 | A entrada do menu do CMS depende de `has_access()` **e** de `modulos.status`. | `v6/app/Providers/MenuServiceProvider.php:46` | `modulos.status = false` oculta a entrada mesmo com permissão. |
| R13 | `menuClicks('a-instituicao')` incrementa `menu_orders.clicks` a cada abertura da tela de listagem do CMS. | `v6/app/Http/Controllers/Prefecture/ContentController.php:116`; `v6/app/Helpers/functions.php:865-872` | Métrica de menu contaminada por navegação do CMS. |
| R14 | A exclusão de conteúdo remove também os `menu_items` que o referenciam. | `v6/app/Http/Controllers/Prefecture/ContentController.php:1320-1367` | Evita menu quebrado apontando para conteúdo inexistente. |
| R15 | O breadcrumb empurra "A Instituição" com `url('/#')` — link morto. | `v6/routes/breadcrumbs.php:202` | O primeiro nível do breadcrumb não navega para lugar nenhum. |
| R16 | Se o layout ativo não tiver `home/index.blade.php`, o sistema **liga o modo de manutenção** e devolve 404. | `v6/app/Http/Traits/GetInformationsPrefectureTrait.php:92-102` | Trocar `configurations.layout` para `default`/`newsbit` derruba o portal inteiro. |

## 2. Acoplamentos com outros módulos

| Módulo | Tipo de acoplamento | Onde |
|---|---|---|
| `../secretarias/` | **O mesmo controller e a mesma view** servem a página institucional e a página de secretaria, decididas pela categoria do conteúdo. | `v6/app/Http/Controllers/Portal/PrefectureController.php:52-75` |
| `../orgaos/` | Idem, com `sidenews = $contentNew->orgaos`. | `v6/app/Http/Controllers/Portal/PrefectureController.php:77-92` |
| `../noticias/` | Bloco "Últimas notícias" e "Notícias de &lt;título&gt;". | `v6/resources/views/portal/components/secretarias/noticias.blade.php`; `v6/resources/views/portal/layouts/modelo1/layout/sidebar/orgaosNews.blade.php` |
| `../agenda-de-eventos/` | `$eventsList` na lateral. | `v6/app/Http/Controllers/Portal/PrefectureController.php:31-36` |
| `../menus/` | `MenuItemPresenter` decide a URL final do item de menu. | `v6/app/Http/Presenters/MenuItemPresenter.php:22-45` |
| `../a-cidade/` | Mesmo `ContentController`, mesmo `ContentRequest`, e — por bug — mesmo formulário Blade. | `v6/resources/views/prefecture/contents/a-instituicao/form.blade.php:21,30` |
| `../busca/` | A busca do portal varre um array fixo de slugs que inclui `a-instituicao`. | `v6/app/Http/Controllers/Portal/PortalController.php:251,265` |
| Core do CMS | `modulos`, `resources`, `menu_orders` guardam o slug do módulo. | `v6/database/seeds/MenuOrderSeeder.php:76-87`; `v6/database/seeds/ResourcesTableSeeder.php:89-105` |

## 3. Riscos de performance (os 6 riscos da plataforma, filtrados para este módulo)

| # | Risco | Aplica a A Instituição? | Evidência |
|---|---|---|---|
| R-P1 | **A grade do CMS carrega a view SQL inteira no PHP** (`DB::table(...)->get()`), e o Yajra fatia em memória, sem `LIMIT` no SQL. | **Sim, integralmente.** `DB::table('a_instituicao')->get()` materializa todas as linhas e o DataTable pagina no cliente (`serverSide` não é configurado). | `v6/app/Http/Controllers/Prefecture/DatatableController.php:252-256`; `v6/public/assets/js/custom/utilities/table.js:170-186` |
| R-P2 | **Portal: a mesma lista é lida duas vezes, uma paginada e outra não**; o `hasMany` sem limite é cortado em Blade. | **Parcialmente.** A listagem principal (`news_secretary`) é paginada no servidor (`paginate(6)`). Mas o ramo `secretarias`/`orgaos` carrega `$contentNew->secretarias` / `$contentNew->orgaos` (hasMany **sem limite no SQL**) e corta em Blade com `->sortByDesc('created_at')->splice(0, 12)`. No ramo institucional (categoria 6), as relações não são carregadas — o que é uma economia acidental. | `v6/app/Http/Controllers/Portal/PrefectureController.php:38,53-54,79-80`; `v6/app/Http/Model/Prefecture/Content.php:72-86` |
| R-P3 | **Legislação: dupla paginação** (servidor + cliente). | **Não.** Não se aplica. | — |
| R-P4 | **`POST legislacao/filtro`** carrega a subcategoria inteira sem limite. | **Não.** Não se aplica. | — |
| R-P5 | **Selects do CMS carregam a view inteira só para popular um `<select>`** (ex.: `DB::table('secretarias')->get(['id','titulo as name'])`). | **Não diretamente** — o formulário de A Instituição não tem selects de secretarias. Mas o mesmo padrão aparece no `ContentController` para `noticias` (`getContentByCategoryWith('secretarias', ...)`) e em `secretarias`, o que evidencia o risco ao ser copiado. | `v6/app/Http/Controllers/Prefecture/ContentController.php:186-188` |
| R-P6 | **Eventos: `eventsList` usa `->limit(5)->get()`** (limite no SQL — o caso correto). | **Sim, parcialmente.** O `->limit(5)` só é aplicado nos ramos `secretarias` e `orgaos`. No ramo institucional (categoria 6), `$eventsList` é construído com `orderBy` e **sem `limit`**; como o controller não o repassa à view (`compact('contentNew','news_secretary','title','linkVideo')`), a agenda de eventos **não aparece** na página institucional — a query é feita e descartada. | `v6/app/Http/Controllers/Portal/PrefectureController.php:31-36,57-60,82-86,92-97` |

### 3.1 Riscos específicos deste módulo

| Risco | Descrição | Impacto |
|---|---|---|
| C-1 | `DB::table('a_instituicao')->get()` + paginação no cliente | A grade cresce linearmente em memória e em tempo de resposta conforme o número de páginas institucionais. |
| C-2 | A view `a_instituicao` **não tem índice próprio**; ela faz `JOIN contents × category_contents` e `ORDER BY c.created_at DESC` sobre `row_number()`. Cada `get()` reordena tudo. | Custo de CPU do PostgreSQL a cada carregamento da grade. |
| C-3 | `updateClicks()` faz `SELECT` + `UPDATE` em duas queries por visita. | 2 round-trips por visita ao banco, em uma página de alto tráfego. |
| C-4 | `$news_secretary->links()` gera o paginador; mas a coleção já foi materializada com `with('categoryContents','archives')` — eager loading de `archives` para 6 itens é razoável. | OK. |
| C-5 | `Content::with('archiveHighlight','archiveGallery', ...)` na página institucional carrega **todas** as mídias da galeria em memória só para checar `isEmpty()` e renderizar. | Custo proporcional ao tamanho da galeria. |
| C-6 | `getTemplateActived()` faz `Configuration::where('key','layout')->first()` a cada requisição (sem cache) e `View::exists()` no disco. | 1 query extra por visita. |

## 4. Armadilhas verificadas

### 4.1 `Content::secretarias()` liga pelo SLUG, não pelo id

```php
// v6/app/Http/Model/Prefecture/Content.php:72-75
public function secretarias()
{
    return $this->hasMany(Content::class, 'secretaria', 'slug');
}
```

`localKey = 'slug'`: a notícia é ligada à secretaria pelo **slug textual** do
conteúdo, não pelo `id`. Se o slug da secretaria mudar, o vínculo se rompe em
silêncio (nenhum erro, nenhum log). No banco atual a coluna `contents.secretaria`
está 100% NULL, o que significa que o caminho nunca foi exercitado em produção.

### 4.2 `ContentRequest::authorize()` devolve `true`

```php
// v6/app/Http/Requests/ContentRequest.php:19-22
public function authorize()
{
    return true;
}
```

A Form Request **não** autoriza nada. Quem protege o módulo é:

1. o middleware `check.route.permission`, aplicado no construtor do
   `ContentController` (`v6/app/Http/Controllers/Prefecture/ContentController.php:64`);
2. o `has_access()` dentro das views (botões) e do `struct_views()` (colunas).

Consequência: qualquer rota do CMS que use `ContentRequest` sem o middleware
herda a mesma fragilidade. Ao migrar, a autorização deve ir para uma **Policy**.

### 4.3 `MenuItemPresenter` acoplado a IDs de categoria

```php
// v6/app/Http/Presenters/MenuItemPresenter.php:30-38
} elseif ($this->categoryContent->id == 7) {
    return url('a-instituicao/' . $this->categoryContent->slug . $content_slug);
} elseif ($this->categoryContent->id == 32) {
    return url('a-instituicao/' . $this->categoryContent->slug . $content_slug);
} elseif ($this->categoryContent->id == 12) {
    return url('acesso-rapido/' . $this->categoryContent->slug . $content_slug);
}
```

A URL do item de menu é decidida por **id numérico** de `category_contents`
(7 = Secretarias, 32 = Órgãos, 12 = Perguntas Frequentes). A categoria do módulo
(id 6) cai no ramo genérico `url('/' . $categoryContent->slug . $content_slug)`.

Impactos:

- recriar a base ou reordenar as categorias quebra os links do menu;
- o mesmo problema existe no `getShortLink()` (`v6/app/Http/Presenters/MenuItemPresenter.php:71-77`);
- o `id == 7` (Secretarias) e o `id == 32` (Órgãos) produzem `/a-instituicao/<slug-da-categoria><slug-do-conteúdo>` — para a categoria 6 isso seria `/a-instituicao/a-instituicao/...`, o que explica por que o ramo genérico existe.

### 4.4 `has_access()` vs. middleware — duas camadas divergentes

| Camada | Onde | Granularidade |
|---|---|---|
| `has_access()` | Views e `struct_views()` | Decide **exibição** de botões e habilitação de ações por linha. |
| `check.route.permission` | `ContentController::__construct` | Decide **acesso à rota**, comparando o nome da rota com as permissões do Sentinel. |

Divergências reais:

- `has_access(['a-instituicao.create'])` habilita o botão "Cadastrar Conteúdo", mas
  a rota `GET /cms/conteudos/cadastrar/a-instituicao` tem nome `cadastrar-conteudo`
  — o middleware compara com o nome da rota, não com o slug. O operador pode ver o
  botão e tomar 403 ao clicar.
- `has_access()` retorna `true` para `super-admin` sem consultar permissões
  (`v6/app/Library/Helper/appHelpers.php:33-35`); o middleware faz o mesmo atalho.
- `has_access()` retorna `false` para usuário não logado
  (`v6/app/Library/Helper/appHelpers.php:25-27`).

### 4.5 `getTemplateActived()` torna `default` e `newsbit` inalcançáveis

```php
// v6/app/Http/Traits/GetInformationsPrefectureTrait.php:92-102
public function getTemplateActived()
{
    $templateActived = Configuration::where('key', 'layout')->first()->value;

    if (!View::exists('portal.layouts.' . $templateActived . '.home.index')) {
        DB::table('configurations')->where('key', 'pagina_em_manutencao')->update(['value' => 1]);
        abort(404, 'view_inexistente');
    }

    return $templateActived;
}
```

Só `modelo1..modelo6` possuem `portal/layouts/modeloN/home/index.blade.php`.
Consequências para este módulo:

- as views de A Instituição em `default` e `newsbit` são **órfãs**;
- `v6/resources/views/portal/layouts/default/pages/contents/a-prefeitura/show.blade.php`
  ainda usa o **nome antigo** (`a-prefeitura`) e nunca é alcançada pelo controller;
- a validação é feita contra a *home* do layout, não contra a view que o módulo
  precisa — um layout pode ter `home/index.blade.php` e não ter
  `informacoes-instituicao/a-instituicao/show.blade.php`, o que produziria um
  `ViewException` em vez de um 404 limpo.

### 4.6 Outras armadilhas

| Armadilha | Evidência |
|---|---|
| `Route::` dentro de controller/view (acoplamento de controller a named route) | `v6/app/Http/Controllers/Portal/PortalController.php:331-332`; `v6/routes/breadcrumbs.php:203`; `v6/resources/views/prefecture/contents/a-instituicao/form.blade.php:30` |
| Nome de rota duplicado (ver `03-ROTAS-E-CONTROLADORES.md` §1.1) | `v6/routes/web.php:79-80` |
| Referência a nome de rota inexistente (`a-instituicao-secretarias.index`) | `v6/app/Http/Controllers/Portal/PortalController.php:332` |
| SQL hardcoded dentro de migration (`DB::statement` com SQL completo) | `v6/database/migrations/2024_11_29_100640_update_view_a_instituicao.php` |
| `Str::slug` + sufixo manual para colisão (não há índice único) | `v6/app/Http/Controllers/Prefecture/ContentController.php:676-679` |
| `count($data)` em laço sobre coleção materializada | `v6/app/Http/Controllers/Prefecture/DatatableController.php:504-520` |
| `session()` dentro de scopes do model (`scopeByCategoryContentNews`, `scopeByLimit`) — estado de requisição em scope | `v6/app/Http/Model/Prefecture/Content.php:216-224,245-252` |
| `$_GET` lido diretamente no controller do portal | `v6/app/Http/Controllers/Portal/PortalController.php:319` |
| HTML renderizado sem escape (`{!! $contentNew->text !!}`) — risco de XSS Stored se o texto não for sanitizado no `store()` | `v6/resources/views/portal/layouts/modelo1/informacoes-instituicao/a-instituicao/show.blade.php:22` |
| `Cache::rememberForever('modulos')` sem invalidação ao alterar `modulos.status` | `v6/app/Providers/ModulosServiceProvider.php:22-25` |
| `row_number()` como "índice" — sem chave estável para a grade | view `a_instituicao` |

## 5. Código copiado entre v5 e v6

| Trecho | v5 | v6 | Observação |
|---|---|---|---|
| Controller do portal | `v5/app/Http/Controllers/Portal/PrefectureController.php:84,95` | `v6/app/Http/Controllers/Portal/PrefectureController.php:76,92` | Praticamente idêntico; só o nome da view mudou (`informacoes-prefeitura` → `informacoes-instituicao`). |
| Componente de view | `v5/resources/views/portal/layouts/modelo1/informacoes-prefeitura/a-prefeitura/show.blade.php` | `v6/resources/views/portal/layouts/modelo1/informacoes-instituicao/a-instituicao/show.blade.php` | Cópia com renomeação de diretório. |
| Grade do CMS | `v5/app/Http/Controllers/Prefecture/DatatableController.php:279-283` | `v6/app/Http/Controllers/Prefecture/DatatableController.php:252-256` | Mesma estrutura, tabela trocada. |
| JS da grade | `v5/public/assets/js/custom/contents/aprefeitura/datatable.js` | `v6/public/assets/js/custom/contents/a-instituicao/datatable.js` | URL do botão ajustada de `/a-prefeitura/` para `/a-instituicao/`; a v5 protegia com `if (data.slug)` e a v6 **não** protege. |
| `MenuItemPresenter` | `v5/app/Http/Presenters/MenuItemPresenter.php:31-34` | `v6/app/Http/Presenters/MenuItemPresenter.php:30-33` | Mesmos IDs de categoria, apenas o prefixo de URL mudou. |
| `ContentRequest` | `v5/app/Http/Requests/ContentRequest.php:213-219` | `v6/app/Http/Requests/ContentRequest.php:230-237` | Regras idênticas, `case` renomeado. |
| Menu do CMS | `v5/app/Providers/MenuServiceProvider.php:150-152` | `v6/app/Providers/MenuServiceProvider.php:183-185` | Texto "A Prefeitura" → "A Instituição"; ícone `institution` mantido. |
| Busca do portal | `v5/app/Http/Controllers/Portal/PortalController.php:247,255` | `v6/app/Http/Controllers/Portal/PortalController.php:251,265` | Slug `a-prefeitura` → `a-instituicao` no array fixo. |
| `FilterSearchAllSite` | `v5/app/Http/Filter/FilterSearchAllSite.php:29` | `v6/app/Http/Filter/FilterSearchAllSite.php:29` | Idem renomeação de slug. |
| Função auxiliar de busca | `v5/app/Helpers/functions.php:443-447` (agrupa em "A Prefeitura" quando `category_contents_id == 6`) | `v6/app/Helpers/functions.php:472-473` (agrupa em `a-instituicao` quando `category_contents_id == 6`) | O id 6 aparece **hardcoded** no agrupamento da busca. |

## 6. Onde a v6 divergiu da v5 (e por quê, se descobrível)

| Divergência | Detalhe | Hipótese |
|---|---|---|
| Renomeação do módulo | "A Prefeitura" → "A Instituição" | Ampliar o escopo do módulo para também o institucional (secretarias, órgãos) que passaram a ser servidos pelo mesmo controller. |
| Novo diretório de views | `informacoes-prefeitura` → `informacoes-instituicao` | Acompanhar o renomeamento. |
| Coluna `slug` na view | adicionada em 2024-11-29 na v6; em 2026-03-09 na v5 | Necessidade do botão "Acessar Instituto" apontar para a página real. |
| Rota de secretarias com nome duplicado | v5: `a-prefeitura-secretarias.index`; v6: `a-instituicao.index` (duplicado) | Renomeação mecânica de `a-prefeitura-` → `a-instituicao-` que **não** foi aplicada à segunda rota, quebrando o nome `a-instituicao-secretarias.index` ainda referenciado no `PortalController`. |
| Perda da proteção `if (data.slug)` no JS | v5 protegia o botão contra `slug` vazio | Regressão: com `slug` NULL o botão vira `/a-instituicao/undefined`. |
| Formulário com URL de A Cidade | idêntico em `v6/resources/views/prefecture/contents/a-cidade/form.blade.php:21,30` | Copy/paste não revisado. |
| `modulos.status` | tabela `modulos` (2024-11-01) introduziu a segunda chave de exibição do menu do CMS | `a-instituicao` ficou `true`; `a-cidade`, `orgaos` e `secretarias` ficaram `false`. |

## 7. Regras de equivalência funcional (o que NÃO pode mudar na v7)

1. A URL pública canônica é `/a-instituicao/{slug}`.
2. O `slug` do conteúdo é único e legível; a colisão precisa de resolução
   determinística.
3. O título é limitado a 120 caracteres; o texto é obrigatório; o vídeo é
   limitado a 200 caracteres; a imagem de destaque aceita `jpg, jpeg, png, gif, webp`
   até 20 MB.
4. A galeria só aparece com `galery = 1` e só mostra imagens.
5. O contador `clicks` continua sendo exibido/auditado.
6. A entrada de menu do CMS é controlada por permissão **e** por status do módulo.
7. A exclusão de conteúdo limpa os itens de menu que o referenciam.
8. A listagem do CMS mantém as colunas `#`, `ID`, `Título`, `Ações`, com a coluna
   "Ações" fora das exportações.
9. A navegação do portal mantém a coluna lateral padrão do tema e a agenda de
   eventos.
