# Gestão de Permissões — Views e UX

> Seis telas Blade no namespace `v2-gestaopermissoes::`, todas estendendo o
> layout do CMS. Sem Ajax, sem exportação e sem JavaScript de framework.

## 1. Inventário de views

| View | Rota | Função |
|---|---|---|
| `V2/Modules/Infraestrutura/GestaoPermissoes/Views/roles/index.blade.php` | `GET /cms/permissoes/roles` | lista de perfis com busca, ordenação, contagem de usuários e ações |
| `V2/Modules/Infraestrutura/GestaoPermissoes/Views/roles/create.blade.php` | `GET /cms/permissoes/roles/create` | cadastro de perfil + matriz inicial |
| `V2/Modules/Infraestrutura/GestaoPermissoes/Views/roles/edit.blade.php` | `GET /cms/permissoes/roles/{id}/edit` | edição de perfil + matriz preenchida |
| `V2/Modules/Infraestrutura/GestaoPermissoes/Views/permissions/index.blade.php` | `GET /cms/permissoes/permissions` | lista de usuários com papel V1 e perfil V2 |
| `V2/Modules/Infraestrutura/GestaoPermissoes/Views/permissions/create.blade.php` | `GET /cms/permissoes/permissions/create` | cadastro de usuário administrativo |
| `V2/Modules/Infraestrutura/GestaoPermissoes/Views/permissions/edit.blade.php` | `GET /cms/permissoes/permissions/{id}/edit` | edição de dados, senha e perfil V2 |

Todas as views começam com:

```blade
@extends('v2-cms::layouts.app', ['title' => 'Perfis de Acesso V2'])
```

(`V2/Modules/Infraestrutura/GestaoPermissoes/Views/roles/index.blade.php:1`)

O layout `v2-cms::layouts.app` é registrado em
`V2/Core/Providers/V2ServiceProvider.php:36-39` a partir de `V2/layouts/cms`.
As páginas usam as seções `page-breadcrumbs`, `page-actions` e `content`.

## 2. `roles/index.blade.php`

### Cabeçalho e ações

```blade
@section('page-actions')
    @v2can('GestaoPermissoes', 'write')
    <a href="{{ route('v2.gestaopermissoes.roles.create') }}" class="btn btn-primary btn-sm d-inline-flex align-items-center gap-1">
        <i class="bi bi-plus-lg" aria-hidden="true"></i>
        <span>Novo Perfil</span>
    </a>
    @endv2can
@endsection
```

(`:12-18`)

O botão só é renderizado para nível `write`. A diretiva `@v2can` é registrada
em `V2ServiceProvider.php:29-34` e compila para `if (v2_can('GestaoPermissoes', 'write'))`.

### Busca e estados vazios

- Formulário `GET` com campo `search` e botão “Filtrar”; quando há busca, exibe
  “Limpar” (`:35-50`).
- Tabela com `<thead>` ordenável: clicar em “Nome do Perfil” ou “Slug
  Identificador” reescreve a query string com `sort`/`dir` (`:77-99`).
- Estado vazio: ícone `bi-inbox`, texto “Nenhum perfil de acesso V2 encontrado.”
  (`:128-132`).
- Paginação no rodapé, preservando `search` (`:138-144`).

### Colunas

| Coluna | Conteúdo | Origem |
|---|---|---|
| Nome do Perfil | `$role->name` em negrito | banco |
| Slug Identificador | `$role->slug` em `<code>` | banco |
| Operadores Vinculados | badge com `$role->users_count` | `getAssignedUsersCount()` |
| Ações | “Editar” e “Excluir” | condicionais |

O botão “Excluir” fica `disabled` quando `users_count > 0` (`:118`) e usa
`confirm()` inline no `onsubmit` (`:116`). O backend também recusa a exclusão
(`PermissionService::deleteRole`, `:344-378`), mas a validação de UX evita o
erro.

### Alertas

`session('success')` e `session('error')` são renderizados como
`alert alert-success` / `alert alert-danger` com `data-bs-dismiss="alert"`
(`:52-75`). O mesmo padrão aparece em todas as telas.

## 3. Matriz de permissões (`roles/create` e `roles/edit`)

### Estrutura

A matriz é uma tabela com uma linha por módulo registrado:

| Coluna | Largura | Conteúdo |
|---|---|---|
| Módulo | 35% | ícone + `title` |
| Grupo | 25% | badge com `group` |
| Nível de Acesso | 40% | três rádios inline |

Código real de uma linha (`V2/Modules/Infraestrutura/GestaoPermissoes/Views/roles/create.blade.php:90-126`):

```blade
<tr>
    <td>
        <i class="{{ mapModuleIcon($config['icon'] ?? '') }} text-primary me-2"></i>
        <strong>{{ $config['title'] ?? $moduleKey }}</strong>
    </td>
    <td>
        <span class="badge bg-secondary">{{ $config['group'] ?? 'OUTROS' }}</span>
    </td>
    <td>
        <div class="d-flex justify-content-around align-items-center">
            <div class="form-check form-check-inline text-danger fw-bold">
                <input class="form-check-input" type="radio" name="permissions[{{ $moduleKey }}]" id="perm_{{ $moduleKey }}_none" value="none" checked>
                <label class="form-check-label" for="perm_{{ $moduleKey }}_none">
                    <i class="bi bi-ban"></i> Sem Acesso
                </label>
            </div>
            <div class="form-check form-check-inline text-warning fw-bold">
                <input class="form-check-input" type="radio" name="permissions[{{ $moduleKey }}]" id="perm_{{ $moduleKey }}_read" value="read">
                <label class="form-check-label" for="perm_{{ $moduleKey }}_read">
                    <i class="bi bi-eye-fill"></i> Leitura
                </label>
            </div>
            <div class="form-check form-check-inline text-success fw-bold">
                <input class="form-check-input" type="radio" name="permissions[{{ $moduleKey }}]" id="perm_{{ $moduleKey }}_write" value="write">
                <label class="form-check-label" for="perm_{{ $moduleKey }}_write">
                    <i class="bi bi-pencil-fill"></i> Leitura &amp; Escrita
                </label>
            </div>
        </div>
    </td>
</tr>
```

### Diferença entre create e edit

- `create`: todos os rádios começam em `none`; `checked` fixo no `value="none"`
  (`:104`).
- `edit`: o valor vem de `$resolvedPermissions[$moduleKey]`
  (`V2/Modules/Infraestrutura/GestaoPermissoes/Views/roles/edit.blade.php:88-92,102-118`), que o controller calculou com
  `resolvePermission()`. Isso faz um perfil antigo com slug legado aparecer no
  nível correto.
- Se `permissions_v2` for nulo, `edit` cai no fallback
  `$role->permissions_v2 ?: []` e todos ficam em `none`.

### Módulos restritos

O controller remove `restricted` da lista antes de renderizar
(`GestaoPermissoesRolesController.php:96-100,181-185`), portanto a matriz nunca
mostra `SupportConfigs`, `MigracaoLegada`, `StructuralLandingEditor` ou
`DemonstrativosLayouts`. Consequência: não é possível conceder esses módulos a
um perfil comum pela UI.

### Ícones

As views definem uma função global `mapModuleIcon()` dentro de `@php`
(`V2/Modules/Infraestrutura/GestaoPermissoes/Views/roles/create.blade.php:8-27`) que converte ícones FontAwesome legados em
Bootstrap Icons. Duas views declaram a mesma função protegida por
`function_exists`, então a primeira carregada define o mapa para as demais.

## 4. Gerador de slug

A tela de criação tem um script inline que deriva o slug do nome
(`V2/Modules/Infraestrutura/GestaoPermissoes/Views/roles/create.blade.php:159-186`):

```javascript
nameInput.addEventListener('input', function() {
    if (slugInput.dataset.edited === 'true') return;

    let name = this.value;
    let slug = name.toString().toLowerCase()
        .normalize('NFD').replace(/[\u0300-\u036f]/g, '')
        .replace(/[^a-z0-9 -]/g, '')
        .replace(/\s+/g, '-')
        .replace(/-+/g, '-');

    slugInput.value = slug;
});

slugInput.addEventListener('change', function() {
    this.dataset.edited = 'true';
});
```

Comportamento:

- Digitar o nome preenche o slug.
- Editar o slug manualmente marca `dataset.edited` e impede sobrescrita.
- Não há validação de slug no cliente; o servidor só exige `string|max:255` e
  unicidade. Espaços, acentos e maiúsculas são aceitos se enviados por POST
  direto.

## 5. `permissions/index.blade.php`

### Colunas

| Coluna | Conteúdo |
|---|---|
| Nome | `$user->name` |
| E-mail | `$user->email` |
| Papel (V1) | badge “Suporte Técnico”, “Super Admin” ou “Operador” |
| Perfil (V2) | badge “Total (Admin)”, nome do perfil ou “Nenhum” |
| Ações | botão “Perfil V2” |

### Regras de exibição

```blade
@if($user->email === config('v2.support.email'))
    <span class="badge bg-danger d-inline-flex align-items-center gap-1"><i class="bi bi-wrench-adjustable"></i> Suporte Técnico</span>
@elseif($user->inRole('super-admin'))
    <span class="badge bg-warning text-dark d-inline-flex align-items-center gap-1"><i class="bi bi-shield-fill-check"></i> Super Admin</span>
@else
    <span class="badge bg-secondary d-inline-flex align-items-center gap-1"><i class="bi bi-person-fill"></i> Operador</span>
@endif
```

(`V2/Modules/Infraestrutura/GestaoPermissoes/Views/permissions/index.blade.php:70-80`)

Para suporte e super-admin, a coluna “Perfil (V2)” mostra “Total (Admin)” e a
ação é um botão desabilitado “Acesso Total”
(`:81-101`). Para os demais, aparece o link de edição protegido por `@v2can`.

Observação: a UI **esconde** a edição de suporte/super-admin, mas o endpoint
`POST /cms/permissoes/permissions/{id}` não recusa esses alvos. A proteção é
apenas visual (ver `08-GAPS-E-DIVIDAS.md`).

### Busca e paginação

- Campo `search` por nome ou e-mail (`:33-53`).
- Paginação de 15 com `appends(['search' => $search])` (`:121-127`).
- Sem ordenação por coluna: a consulta fixa `orderBy('name')`.

## 6. Formulários de usuário

### `permissions/create.blade.php`

Campos:

| Campo | Tipo | Obrigatório | Regra |
|---|---|---|---|
| `name` | text | sim | `required|string|max:255` |
| `email` | email | sim | `required|email|unique:users,email` |
| `role_id` | select (Sentinel V1) | sim | `required|integer` |
| `v2_role_id` | select (perfis V2) | não | `nullable|exists:v2_roles,id` |
| `password` | password | sim | `required|min:6|confirmed` |
| `password_confirmation` | password | sim | `confirmed` |

O select V1 é populado com
`Sentinel::getRoleRepository()->where('slug', '!=', 'super-admin')`
(`GestaoPermissoesPermissionsController.php:188`). O select V2 usa
`getAllRoles()` (ordem alfabética).

### `permissions/edit.blade.php`

Campos: `name`, `email`, `password` opcional, `password_confirmation` e
`v2_role_id`. O texto de ajuda explica a herança:

> “O usuário herdará todos os privilégios de leitura e escrita correspondentes
> ao perfil selecionado.”

(`V2/Modules/Infraestrutura/GestaoPermissoes/Views/permissions/edit.blade.php:96`)

Não há campo para trocar o papel Sentinel V1 na edição. O papel V1 só é
definido na criação.

## 7. UX de erro e bloqueio

Quando o nível é insuficiente:

- **Middleware**: redirect para o dashboard com
  `session('error')` (`V2PermissionMiddleware.php:97-99`), ou `403` textual para
  AJAX/JSON (`:94`).
- **Controller**: `abort(403, 'Acesso negado. Apenas super-administradores podem gerenciar perfis de acesso.')`
  (`GestaoPermissoesRolesController.php:91-92`); as demais ações usam mensagens
  equivalentes.
- **View**: se o usuário chegou à tela sem `write` (por exemplo, link direto em
  cache), o formulário é substituído por um cartão com ícone de cadeado e a
  mensagem “Você não tem permissão de escrita neste módulo.”
  (`V2/Modules/Infraestrutura/GestaoPermissoes/Views/roles/create.blade.php:147-153`).

O texto “Apenas super-administradores” é impreciso: qualquer perfil com
`write` em `GestaoPermissoes` pode executar as ações.

## 8. Acessibilidade e assets

- Labels explícitos em todos os inputs (`for`/`id`).
- `aria-label` nas tabelas e botões de fechar alerta.
- Ícones decorativos usam `aria-hidden="true"`.
- Não há CDN no módulo: Bootstrap 5 e Bootstrap Icons vêm do layout do CMS.
- Não há `@push('styles')`; apenas um `@push('scripts')` na tela de criação de
  perfil.
- Não há gráficos, upload, download, PDF, Excel ou CSV.

## 9. O que o operador vê versus o que o sistema faz

| Situação | UI | Efeito real |
|---|---|---|
| Perfil `read` | lista perfis/usuários; vê mensagem de bloqueio nas telas de formulário | GETs passam; POSTs são barrados pelo middleware |
| Perfil `write` | vê botões e matriz editável | CRUD liberado |
| Suporte | vê também módulos do grupo `Support` | bypass total |
| `super-admin` | vê tudo | bypass total |
| Módulo desativado | rota responde redirect/404 antes da tela | `module.active` |
