# Módulo Menus (`Infraestrutura/Menus`)

> Contrato de leitura do módulo (ADR-004/ADR-014) — mantenha sincronizado com o código.

## Propósito

Os **menus do site** são dado editável no painel: uma árvore de até **3 níveis** por menu, com
destino validado (rota viva da área Portal ou URL `http(s)`) e rótulo nas quatro locales. O layout
do portal **não importa** este módulo: consome a porta `Core\Contracts\Navigation`, registrada por
este `ModuleProvider` (ADR-013/ADR-016). Nada de HTML/CSS/classes no banco (ADR-017).

- **Grupo:** `Infraestrutura` (organizacional — não entra no token, ADR-014).
- **Área:** CMS.
- **Ativação:** dado em `core_module_discovery_settings`; `php artisan core:module:activate Menus [--disable]` (ADR-034).
- **Cache:** `navigation.{menu}.{locale}`, invalidado **na escrita** (mesmo request).

## Estrutura

```
src/modules/Infraestrutura/Menus/
├── ModuleProvider.php
├── MODULE.md
├── Database/
│   ├── Migrations/2026_10_24_000000_create_menus_tables.php
│   ├── Migrations/2026_10_24_000100_seed_principal_menu.php
│   └── Models/{Menu,MenuItem}.php
├── Logic/
│   ├── MenusNavigation.php              # implementa Core\Contracts\Navigation
│   ├── Actions/{SaveMenu,SaveMenuItem,DeleteMenuItem,MoveMenuItem,ToggleMenuItem}.php
│   ├── Controllers/Cms/*.php
│   ├── DTOs/{MenuData,MenuItemData}.php
│   ├── Requests/{SaveMenuRequest,SaveMenuItemRequest}.php
│   └── Support/{MenusCache,MenusTarget,MenusTree}.php
├── Routes/cms.php
├── Layout/Views/cms/{index,menu-form,menus-items,menu-item-form}.blade.php
├── Layout/Views/cms/partials/{menu-tree,menu-item-fields}.blade.php
├── Lang/{pt_BR,en,es,fr}/
└── Test/{MenusCmsTest,MenusNavigationTest}.php
```

## Token canônico

**`menus`** — rotas `menus.cms.*`, classes `Menus*`/`Menu*`, tabelas `menus.menus`/`menus.menu_items`,
chaves no namespace `menus`. Verificação: `tools/name-check.sh --grep menus`.

## Dependências

- **Core:** `Core\Contracts\Navigation`, `Core\Contracts\Telemetry`, `Core\Access\Permission`,
  `Core\Internal\ModuleDiscovery\ModuleDiscoveryService` (ativação), `Core\Enums\PlatformArea`
  (domínio do portal), `Core\Localization\Locales`.
- **Layout:** `Layout\Design\IconSet` (só nomes Lucide vendorizados — zero request, ADR-002).
- **Services:** nenhum.
- **Outros módulos:** nenhum import.
- **Tabelas:** `menus.menus`, `menus.menu_items` (propriedade deste módulo).

## Rotas

| Nome | Método | URI | Controller | Acesso |
|---|---|---|---|---|
| `menus.cms.index` | GET | `/menus` | `Cms\MenuIndexController` | `menus.ver` |
| `menus.cms.store` | POST | `/menus` | `Cms\MenuStoreController` | `menus.editar` |
| `menus.cms.edit` | GET | `/menus/{menu}/editar` | `Cms\MenuEditController` | `menus.ver` |
| `menus.cms.update` | PUT | `/menus/{menu}` | `Cms\MenuUpdateController` | `menus.editar` |
| `menus.cms.items` | GET | `/menus/{menu}/itens` | `Cms\MenuItemIndexController` | `menus.ver` |
| `menus.cms.items.store` | POST | `/menus/{menu}/itens` | `Cms\MenuItemStoreController` | `menus.editar` |
| `menus.cms.items.edit` | GET | `/menus/{menu}/itens/{item}/editar` | `Cms\MenuItemEditController` | `menus.ver` |
| `menus.cms.items.update` | PUT | `/menus/{menu}/itens/{item}` | `Cms\MenuItemUpdateController` | `menus.editar` |
| `menus.cms.items.toggle` | POST | `/menus/{menu}/itens/{item}/ativo` | `Cms\MenuItemToggleController` | `menus.editar` |
| `menus.cms.items.move` | POST | `/menus/{menu}/itens/{item}/mover/{direction}` | `Cms\MenuItemMoveController` | `menus.editar` |
| `menus.cms.items.destroy` | DELETE | `/menus/{menu}/itens/{item}` | `Cms\MenuItemDestroyController` | `menus.editar` |

As rotas vivem na área CMS (domínio próprio, `/cms` no domínio base redireciona — ADR-005/037).
`scopeBindings()` garante que o item da URL pertence ao menu da URL.

## Dados e regras

- `menus.menus`: `slug` único **case-insensitive** (`lower(slug)`), `nome`, `descricao`.
- `menus.menu_items`: `menu_id` (cascata), `parent_id` (auto-FK **composta** `(parent_id, menu_id)`
  → pai do mesmo menu; `NO ACTION` bloqueia excluir pai com filhos), `position >= 1`, `type`
  (`route|url|page`), `target jsonb` com a chave do tipo, `label jsonb` por locale (`pt_BR`
  obrigatório; as demais exigidas pelo editor), `icon` Lucide opcional, `active`,
  `opens_new_tab`.
- **Árvore:** máximo de 3 níveis, ciclo proibido, pai de outro menu proibido, exclusão de pai com
  filhos recusada (422). Irmãos são renumerados `1..n` na ordem `position`, `id`.
- **`page`** existe no schema, mas o editor só aceita `route`/`url` até o P5.
- **Dado de referência:** a migração idempotente semeia o menu `principal` com as rotas atuais do
  portal (mesmo conteúdo de `config/layout.php`); sem esse dado, o port usa o fallback de
  `config('layout.portal.nav')` **sem cache** (o dado novo aparece no próximo request).

## Leitura pública (porta)

`MenusNavigation` resolve:

- módulo inativo → `[]` + `menus.inactive` (nunca 500);
- alvo inválido (rota ausente/de outra área/com parâmetro, URL não `http(s)`, `page`) → item
  ignorado + `menus.invalid`;
- menu `principal` sem linha/itens → fallback de configuração + `menus.fallback`;
- rótulo no locale vigente com fallback `pt_BR`; nunca HTML.

## Permissões

`menus.{ver|editar}` declaradas em `ModuleProvider::config()` e aplicadas com
`Permission::authorize()` (negação por padrão, ADR-033). Negação responde 403; item inexistente ou
de outro menu responde 404.

## Eventos e observabilidade

- `menus.save` (menu criado/atualizado), `menus.item_save`, `menus.item_delete`,
  `menus.item_toggle`, `menus.item_move` — spans de mesmo nome via `Core\Contracts\Telemetry`.
- `menus.inactive`, `menus.invalid`, `menus.fallback` — leitura pública (nível `warning`), sem PII.
- A mutação entra na trilha de auditoria pelo `GlobalAuditSubscriber` (ADR-035).

## Layout

- `cms/index`, `cms/menu-form`, `cms/items` e `cms/item-form` usam **um único** `<x-layout::cms>`;
  o módulo preenche apenas o conteúdo (o `<h1>` é do shell).
- O header do portal (`src/layout/Portal/partials/header.blade.php`) consome
  `Core\Contracts\Navigation` e renderiza `<nav aria-label>/<ul>/<li>`, `aria-current="page"`,
  `<details>` (submenu sem JS) e ícones `<x-ui::icon>`.
- Nenhum `<style>`/`style=`/`<script>` inline; 4 locales em `Lang/`.

## Comandos

```bash
php artisan core:module:activate Menus
php artisan core:module:activate Menus --disable
php artisan migrate
```

## Convenções

- [x] `declare(strict_types=1)`; DI; sem traits/abstract/generics.
- [x] Migração idempotente; `down()` renomeia para `backup_*` (ADR-017).
- [x] 4 locales com paridade; nenhuma string visível hardcoded.
- [x] Sem JS obrigatório; alvos e foco do shell preservados.
