# Menus do Site — Regras de Negocio, Acoplamentos e Armadilhas

---

## 1. Regras de negocio nao obvias

| # | Regra | Onde esta | Consequencia observavel |
|---|---|---|---|
| R1 | O `slug` do menu e derivado do `name` com `str_slug` a cada gravacao | `v6/app/Repositories/Eloquent/Prefecture/Menu/MenuRepository.php:15-19` | Renomear o menu no CMS **muda o slug** e quebra qualquer referencia externa ao slug antigo |
| R2 | Item com `menus_items_id IS NULL` e item raiz | `v6/app/Http/Model/Prefecture/MenuItem.php:91` (`subItens`) | O portal sobe ate 3 niveis (raiz + 2). Um 4o nivel criado no CMS **nao aparece** |
| R3 | Limite de 6 itens raiz por menu | `v6/app/Http/Requests/MenuItemRequest.php:33-35`, `:44-46`, `:61-63`, `:75-77` | Ao cadastrar (nao ao editar) o 7o item raiz, o formulario exige `menus_items_id` |
| R4 | Itens do tipo `0` criam um registro em `links` | `v6/app/Repositories/Eloquent/Prefecture/MenuItem/MenuItemRepository.php:24-34` | Editar o nome do item **nao** atualiza o `title`/`name` do `link` (o `links_id` e reaproveitado e o `link` nao e reescrito) |
| R5 | `sub_category_contents_id` sobrescreve `category_contents_id` | `v6/app/Repositories/Eloquent/Prefecture/MenuItem/MenuItemRepository.php:36-38` | O item aponta para a subcategoria, e a categoria "pai" fica implicita |
| R6 | `is_active = false` oculta o item **sem** remove-lo | coluna `menu_items.is_active` (default `true`) | Itens desativados somem do portal mas continuam no banco, no log e no sitemap (`SitemapController` nao filtra por `is_active`) |
| R7 | Itens inativos somem, mas o **pai** permanece no menu | `->where('is_active', true)` em cada nivel do `foreach` do portal | Um item pai sem filhos ativos continua aparecendo como link solto |
| R8 | O item pai so pode ser excluido se nao tiver filhos | `v6/app/Http/Controllers/Prefecture/MenuItemController.php:282-307` | A exclusao e negada com aviso "existem dependencias" e a tentativa fica registrada no log |
| R9 | A ordem (`order`) e **global por menu**, nao por submenu | `v6/app/Http/Controllers/Prefecture/MenuItemController.php:62-85` | Arrastar um subitem altera a numeracao global; a ordenacao exibida e sempre por `order` dentro do grupo |
| R10 | O `order` enviado pelo formulario e descartado | `v6/app/Http/Requests/MenuItemRequest.php:32` (`$this->request->remove('order')`) | O valor gravado vem do `updateOrCreate` sem `order` -> default do banco (`NOT NULL` sem default): risco de erro de integridade. Na pratica, `MenuItemRepository` nao injeta `order` |
| R11 | Excluir um menu faz *soft delete* e **nao** remove os itens | `v6/app/Http/Controllers/Prefecture/MenuController.php:166-189` | Os `menu_items` ficam orfaos, apontando para um menu inexistente na listagem (que usa a view `menus_site`, que filtra `deleted_at IS NULL`) |
| R12 | A entrada do modulo no menu lateral do CMS depende de 3 condicoes | `v6/app/Providers/MenuServiceProvider.php:45` | `has_access("$slug.index")` **e** `modulo_id` preenchido **e** `modulos.status = true` |
| R13 | A posicao no menu lateral depende de `configuration('menu_interativo')` | `v6/app/Providers/MenuServiceProvider.php:39-43` | Se a configuracao for verdadeira, ordena por `clicks DESC, id ASC` (modulo mais acessado primeiro); senao, por `id ASC` |
| R14 | `menuClicks()` e chamado no **construtor** dos controllers | `MenuController.php:39`, `MenuItemController.php:38` | Cada requisicao a qualquer acao do controller incrementa o contador. `MenuItemController` usa o slug `menu-items`, que **nao existe** em `menu_orders` -> o contador nunca sobe |
| R15 | Um item com `is_mega_menu` nao tem link proprio | `v6/resources/views/portal/layouts/modelo1/layout/nav.blade.php:126` | O `href` vira `javascript:;`; o item funciona apenas como agrupador |
| R16 | O destino de um item e decidido pelo `type` + colunas de FK | `v6/app/Http/Presenters/MenuItemPresenter.php:12-60` | Ha `if` hardcoded por id de categoria (2, 3, 7, 12, 32) e por `static_pages_id = 4` |
| R17 | Os itens do menu "Atalhos" nao podem ser desativados pela interface | `v6/resources/views/prefecture/menus-itens/show.blade.php:216` | O interruptor nao e renderizado quando `$menu->name === 'Atalhos'` |
| R18 | `menus.status_create` so influencia a **ordem** da listagem | view `menus_site` | Nao habilita/desabilita nada |

---

## 2. Acoplamento com outros modulos

### 2.1 Dependencias (este modulo precisa de)

| Origem | Uso | Gravidade |
|---|---|---|
| `links` (`App\Prefecture\Link`) | Destino dos itens tipo 0; `Link::menuItems()` e `Link::menuItem()` fazem `belongsToMany` com chaves erradas (`v6/app/Http/Model/Prefecture/Link.php:35`, `:62`) | Alta: uma relacao `belongsToMany` sem a segunda chave gera consulta invalida se usada |
| `static_pages` | Destino dos itens tipo 1; `StaticPage::menuItem()` | Media |
| `category_contents` + `contents` | Destino dos itens tipo 2 | Media |
| `modulos` + `menu_orders` | Visibilidade e ordem no menu lateral do CMS | Alta |
| `configurations` | `layout` (layout do portal), `menu_interativo` (ordem do menu lateral), `contact_link` (atalho do topo) | Media |
| `layouts` (tabela) | Chave `tags` (editor de layout) e `secretarias-grid` (modulo escolhido para a home do modelo5) | Media |
| `activity_log` (Spatie) | Trilha de auditoria das operacoes | Media |

### 2.2 Dependentes (outros modulos que quebram se este mudar)

| Consumidor | Condicao que quebra o funcionamento |
|---|---|
| Portal (todos os modelos) | `menu_items.menus_id = 1/2/3/4` mudar de id; `is_active` deixar de existir; `subItens`/`subItens` deixar de ser ordenados por `order` |
| `FooterServiceProvider` | a semântica de `category_contents_id IS NULL OR category_contents_id = 2` mudar |
| `CategoryContentController:709/767/969` | a FK `category_contents_id` mudar: a exclusao de uma categoria verifica `menu_items` |
| `ContentController:1334/1377` | idem para `contents_id` |
| `StaticPageController:368` | idem para `static_pages_id` |
| `SitemapController:29` | `links_id` deixar de ser preenchido para itens tipo 0 |
| `Link::menuItems()` / `menuItem()` | `links_id` mudar de nome |
| `LayoutController` + `transparencia-fiscal-grid.blade.php` | `menus.id` deixar de ser referenciavel (`layouts.secretarias-grid` guarda `menus:{id}`) |
| `MenuItemPresenter` | os ids fixos de `category_contents` (2, 3, 7, 12, 32) mudarem — a logica esta **hardcoded** |

---

## 3. Armadilhas tecnicas

### 3.1 `belongsTo` onde deveria ser `hasMany`

```php
// v6/app/Http/Model/Prefecture/Menu.php:26-29
public function menuItems()
{
    return $this->belongsTo(MenuItem::class,'menus_id','id');
}
```

Um menu tem **varios** itens. A declaracao correta seria
`hasMany(MenuItem::class, 'menus_id', 'id')`. Nenhum codigo do modulo usa essa
relacao (o portal usa `MenuItem::where('menus_id', $menu->id)`), entao o bug esta
latente — mas qualquer novo codigo que use `$menu->menuItems` vai receber um
`Model` unico (o de maior id) em vez de uma colecao.

### 3.2 `MenuItem` sem `SoftDeletes`

`v6/app/Http/Model/Prefecture/MenuItem.php:18` tem `//use SoftDeletes;` comentado,
mas a tabela tem `deleted_at` (criada pelo `softDeletes()` da migration original).
Consequencias:

- `$menuItem->delete()` faz **DELETE fisico** — a exclusao nao e reversivel.
- Consultas a `menu_items` nao filtram `deleted_at IS NULL` (portanto, se algum
  registro antigo tiver `deleted_at` preenchido, ele **continua aparecendo** no
  portal, porque nenhuma query do modulo filtra por `deleted_at`).
- A `use Illuminate\Database\Eloquent\SoftDeletes;` continua no topo do arquivo.

### 3.3 Rotas quebradas

| Rota | Problema |
|---|---|
| `menu-item.index` (`GET /cms/menu-item`) | `MenuItemController` nao tem `index()` -> erro 500 |
| `menu-item.create` (`GET /cms/menu-item/create`) | `MenuItemController` nao tem `create()` -> erro 500 |
| `menu-item-by-menu` (`GET /cms/itens-do-menu/{menuID}/`) | Aponta para `MenuItemController@createByMenu`, metodo inexistente -> erro 500 |
| `menu-item.toggle` (`POST /menu-item/{id}/toggle`) | Registrada em `v6/routes/web.php` (rotas **publicas**), sem `check.route.permission` e sem `check.logged` |

### 3.4 SQL e logica hardcoded

| Local | Hardcode |
|---|---|
| `v6/app/Http/Presenters/MenuItemPresenter.php:38-56` | `category_contents_id` = 2 (legislacao), 3 (publicacoes), 7 e 32 (a-instituicao), 12 (acesso-rapido); `slug` do pai = `conselhos`; `static_pages_id` = 4 |
| `v6/app/Providers/MenuServiceProvider.php:428` | `menus_id = 1` (menu principal) |
| `v6/app/Providers/MenuServiceProvider.php:454` | `menus_id = 3` (acesso rapido) |
| `v6/app/Providers/MenuServiceProvider.php:471` | `menus_id = 4` (atalhos) |
| `v6/app/Providers/FooterServiceProvider.php:65` | `menus_id = 1` e `category_contents_id = 2` |
| `v6/app/Http/Controllers/Prefecture/MenuItemController.php:132` | `menus_id = 1..4` assumidos pelo operator (nenhum enforco no banco) |
| `v6/resources/views/portal/layouts/modelo1/layout/header.blade.php:47-56` | nomes de atalho `Telefones`, `Ouvidoria`, `Contato`, `Acessibilidade` |
| `v6/database/migrations/2023_04_13_151009_create_view_to_menus.php` | view `menus_site` em SQL cru |

### 3.5 N+1 e queries dentro de loops

| Local | Problema |
|---|---|
| `v6/resources/views/prefecture/menus-itens/show.blade.php:186-360` | 3 `foreach` aninhados, cada um com uma query `MenuItem::where('menus_items_id', $pai->id)` -> 1 + N + M queries |
| `v6/app/Http/Controllers/Prefecture/MenuItemController.php:62-85` (`ajustOrder`) | 1 `find` + 1 `save` por item, sem transacao |
| `v6/app/Http/Controllers/Prefecture/MenuItemController.php:275-317` (`destroy`) | `MenuItem::where('menus_items_id', $id)->get()` carrega todos os filhos so para contar |
| `v6/app/Http/Controllers/Prefecture/MenuItemController.php:149-241` (`edit`) | 8+ queries para reconstruir o formulario |
| `v6/app/Http/Controllers/Prefecture/MenuItemController.php:49-56` | `pluck('name','id')` completo so para contar |
| `v6/app/Providers/MenuServiceProvider.php:24-427` | 4 queries de `InformationsRequest` + 1 de `CategoryContent` **por requisicao**, mesmo em paginas que nao sao de LAI |
| Portal (todas as paginas) | 4 queries de menu por requisicao, sem cache |

### 3.6 Codigo duplicado

| Duplicacao | Onde |
|---|---|
| Calculo da URL do item | `v6/app/Http/Presenters/MenuItemPresenter.php` **e** Blade em `v6/resources/views/prefecture/menus-itens/show.blade.php:203-212` (com regras diferentes) |
| Estrutura da arvore de itens | `v6/resources/views/prefecture/menus-itens/show.blade.php` (3 niveis) e `v6/resources/views/portal/layouts/modelo1/layout/nav.blade.php` (2 subniveis) e `v6/resources/views/portal/layouts/modelo5/layout/header/nav-header/menu-principal.blade.php` (2 subniveis) |
| Listagem de menus | `prefecture/menus/index.blade.php` (DataTable) e `menus-itens/index.blade.php` (orfa, com colunas erradas) |
| `MenuItemController@edit` | logica quase identica em `v5/app/Http/Controllers/Prefecture/MenuItemController.php` (v6 reescreveu com `switch` explicito) |

### 3.7 Outras armadilhas

| Item | Detalhe |
|---|---|
| `Repository::upInsert` | captura `\Throwable` e chama `dd($th)` — **dump + die** em producao; o controller nunca ve a excecao e o `catch (\Exception)` do controller nunca dispara |
| `Repository::upInsert` | `DB::transaction` envolve apenas `structureUpInsert`; o `updateOrCreate` do model fica **fora** da transacao |
| `Repository::upInsert` | `updateOrCreate(['id' => $id], $data)` com `$id = null` cria um registro novo; com `$id` preenchido, atualiza — o `id` do request nao e validado contra a existencia do registro |
| `Repository::destroy` | `DB::transaction` retorna a **query builder** (nao executada); o `delete()` e chamado fora da transacao |
| Contratos vazios | `IMenuRepository` e `IMenuItemRepository` nao declaram metodo algum — nao ha contrato verificavel |
| `MenuItemRequest::rules()` | consulta o banco **durante** a validacao (contagem de itens) |
| `MenuItemRequest::rules()` | `Route::getCurrentRoute()->getName()` — a regra depende do nome da rota, nao do metodo HTTP |
| `MenuController@store` | sem validacao: um POST sem `name` viola `menus.name NOT NULL` |
| `MenuController@destroy` | `Menu::find($id)` pode devolver `null`; `null->delete()` gera erro 500 capturado pelo `catch (\Exception)` |
| `MenuItemController@show` | `Menu::find($menuID)` pode ser `null`; a view acessa `$menu->name` -> erro 500 |
| `MenuItemController@toggle` | `findOrFail` sem try/catch; fora do CMS, sem autenticacao |
| Importacao inexistente | `use App\Http\Presenters\SectionPresenter;` em `v6/app/Http/Model/Prefecture/MenuItem.php:5` — a classe nao existe no repositorio |
| `link` inexistente | `v6/resources/views/prefecture/menus-itens/form.blade.php` usa `$menuItem->link`; a coluna nao existe em `menu_items` na v6 |
| `menu_items.order` | `NOT NULL` sem default no banco, mas o seeder `AlterColumnTableMenuItemsSeeder` a torna nullable — divergencia entre migration e seed |
| `status_create` de `menus` | coluna booleana gravada como string `'1'` pelos seeds |

---

## 4. Riscos de seguranca

| Risco | Evidencia | Impacto |
|---|---|---|
| Qualquer visitante pode ativar/desativar itens de menu | rota `POST /menu-item/{id}/toggle` em `v6/routes/web.php:232`, dentro do grupo publico com apenas `check.site.manutention` | Um cidadao pode esconder um item do menu principal (ou bring-lo de volta) |
| `store`, `update` e `destroy` sao "rotas abertas" | `v6/app/Http/Middleware/CheckRoutePermission.php:47-95` (`$resourceRoutes`) | Qualquer usuario logado (sem permissao `menu.edit`/`menu.delete`) pode criar, alterar e excluir itens de menu |
| O corpo do toggle e ignorado | `v6/app/Http/Controllers/Prefecture/MenuItemController.php:319-325` | Nao ha validacao de entrada; o id do path e o alvo |
| CSRF na rota de toggle | a rota esta em `v6/routes/web.php`, cujo grupo usa o middleware `web` (CSRF do Laravel); o JS envia o header `X-CSRF-TOKEN` | O token protege o POST, mas o endpoint continua publico para quem tenha um token valido |
| Logica de URL duplicada no tooltip do CMS | o destino e reimplementado em Blade (`v6/resources/views/prefecture/menus-itens/show.blade.php:203-212`) | Risco de manutencao: o link exibido no CMS pode divergir do link efetivo no portal |

---

## 5. Onde a v6 divergiu da v5

| Area | v5 | v6 | Motivo aparente |
|---|---|---|---|
| `MenuItem::$fillable` | inclui `link` | inclui `is_mega_menu` | `link` foi abandonado como coluna; `is_mega_menu` e novo |
| `MenuItem::isFather()` / `getIsFatherAttribute()` | `is_null($this->attributes['menu_items_id'])` — **coluna inexistente** (a coluna real e `menus_items_id`) | `is_null($this->attributes['menus_items_id'] ?? null)` | Correcao de bug |
| `MenuItemController@store` / `@update` | nao existia `is_mega_menu` | faz `$request->merge(['is_mega_menu' => $request->has('is_mega_menu') ? 1 : 0])` | Nova feature de mega menu |
| `MenuItemController@edit` | logica de selecao de categoria com `else` aninhado e variaveis duplicadas | `switch` por `type` + guarda `if ($categorySelectedObj)` | Correcao de bug: na v5, `CategoryContent::find()` podia devolver `null` e o codigo acessava `->category_contents_id` |
| `MenuItemPresenter::getLink()` | `acesso**s**-rapido/`; `a-prefeitura/` | `acesso-rapido/`; `a-instituicao/` | Adequacao aos slugs novos do portal |
| `MenuItemPresenter` (tipo 2) | checava `!is_null($this->contents_id) && $this->contents_id !== ''` | usa apenas `isset($this->content->slug) && !empty($this->content->slug)` | Simplificacao |
| `MenuItemPresenter::getShortLink()` | includia `conselhos` com `category_contents_id` | removeu o caso `conselhos` (so `getLink` mantem) | Inconsistencia entre os dois metodos |
| `MenuController` | identico | identico | — |
| `MenuItemController` (resto) | identico | identico | — |
| Views `prefecture/menus/**` | identicas | identicas | — |
| Views `prefecture/menus-itens/**` | sem `is_mega_menu`; ja com o interruptor de `is_active`; usam `optional()` | com `is_mega_menu` e badge; **removem os `optional()`** | Mega menu; regressao de robustez no Blade |
| Migrations | `2025_06_02_114951_add_is_active_to_menus_table.php` | `2025_06_04_160404_add_is_active_to_menus_table.php` (mesmo conteudo, nomeData diferente) | Renomeada ao portar |
| View `menus_site` | identica | identica | — |
| `modulo_id` em `menu_orders` | ausente | `2024_11_04_094157_add_modulo_id_to_menu_order_table.php` + `2026_09_01_100000_fix_menu_orders_missing_modulo_id.php` | Vinculo com a tabela `modulos` |
| Menu "Tags" | `AddMenuTagsToProductionSeeder.php` + `AddMenuTagsWidgetSeeder.php` + view `portal/layouts/modelo5/layout/menus/menu_tags.blade.php` | **ausentes** | Substituidos pelo seletor de modulo do editor de layout (`UpdateModuloSelectOptions.php`) |

> Detalhe do "Tags" em `09-TAGS-DE-MENU.md`; comparativo completo em
> `08-GAPS-E-DIVIDAS.md`.
