# Obras — Views SQL e Permissões (complemento)

> Complemento de `02-MODELAGEM.md` (v5). As seções 7 e 11 daquele arquivo
> foram movidas para cá para manter todos os arquivos dentro do limite de
> 400 linhas.

---

## 1. Views SQL

Duas views criadas por migration, confirmadas com
`SELECT viewname, definition FROM pg_views WHERE viewname ILIKE '%obra%'`.

### 1.1 `listagem_obras`

```sql
CREATE OR REPLACE VIEW listagem_obras AS
SELECT
    w.id AS id,
    row_number() OVER (ORDER BY w.created_at DESC) AS value,
    w.title AS titulo,
    wc.name AS categoria,
    ws.name AS situacao,
    w.value AS total,
    w.value_paid AS valor_pago
FROM works w
INNER JOIN work_categories wc ON wc.id = w.work_category_id
INNER JOIN work_statuses ws ON ws.id = w.status_id
WHERE w.deleted_at IS NULL
ORDER BY w.created_at DESC;
```

| Coluna da view | Origem | Uso |
|---|---|---|
| `id` | `works.id` | ação de editar/excluir na linha do DataTable |
| `value` | `row_number() OVER (ORDER BY created_at DESC)` | **numeração sequencial** exibida como coluna "ID" no DataTable (`runDatatables` faz `unshift` de uma coluna `data: 'value'`) |
| `titulo` | `works.title` | coluna "Título" |
| `categoria` | `work_categories.name` | coluna "Categoria" |
| `situacao` | `work_statuses.name` | coluna "Situação" |
| `total` | `works.value` | coluna "Valor Total" |
| `valor_pago` | `works.value_paid` | coluna "Valor Pago" |

**O que ela abre:** é a fonte de dados de
`GET /cms/datatable/listagem_obras`, consumido por
`v5/public/assets/js/custom/contents/obras/datatable.js`. É a **única** listagem de
obras do CMS — o controller do CMS (`WorkController@index`) **não** consulta a
tabela; ele apenas devolve a view Blade e o JS faz o AJAX.

**Consequências do `INNER JOIN`:**

1. Obras com `work_category_id` ou `status_id` **nulos** (o que a FK
   `ON DELETE SET NULL` permite) **desaparecem** da listagem do CMS.
2. Categorias/situações com `deleted_at` preenchido **continuam** aparecendo
   (a view não filtra `wc.deleted_at`/`ws.deleted_at`), enquanto a obra
   continua listada.

**Consequência do `row_number()`:** o "ID" exibido é a **posição na
ordenação**, não o `id` real da obra. A ordenação é por `created_at DESC`, e
obras com o mesmo `created_at` recebem numeração arbitrária.

### 1.2 `listagem_categorias_obras`

```sql
CREATE OR REPLACE VIEW listagem_categorias_obras AS
SELECT
    wc.id AS id,
    row_number() OVER (ORDER BY wc.created_at DESC) AS value,
    wc.name AS nome
FROM work_categories wc
WHERE wc.deleted_at IS NULL
ORDER BY wc.created_at DESC;
```

**O que ela abre:** `GET /cms/datatable/listagem_categorias_obras`, consumido
por `v5/public/assets/js/custom/contents/obras/categorias-datatable.js`. Só a coluna
`nome` é exibida; a coluna `value` (row_number) aparece como "ID".

### 1.3 Views SQL ausentes

- **Não há view para `work_archives`** — a listagem de arquivos da obra é
  montada em Eloquent (`WorkController@edit`).
- **Não há view para `work_statuses`** — as situações são carregadas com
  `WorkStatus::query()->select(DB::raw("id, name"))->get()`.
- **Não há view no portal** — o portal usa Eloquent direto sobre `works`.

---

## 2. Permissões derivadas da estrutura

O resource `obras` (id 13) na tabela `resources` é a origem das permissões. A
combinação **resource.slug + "." + permission.slug** é o que o CMS grava em
`roles.permissions` (JSON) e o que o `CheckRoutePermission` verifica. As
permissões disponíveis na tabela `permissions` são: `index`, `create`, `edit`,
`delete`, `inactive`, `inactivate`, `restore`, `show`.

| Chave de permissão | Usada em |
|---|---|
| `obras.index` | `MenuServiceProvider` (exibe o item de menu), `CheckRoutePermission` nas rotas `obras.index` e `categorias-obras.index` |
| `obras.create` | botão "Criar Obra"/"Categorias de Obras" em `prefecture/works/index.blade.php` |
| `obras.edit` | botão de editar por linha (`struct_views`) e rota `obras.edit` |
| `obras.delete` | botão de excluir por linha (`struct_views`) e rota `obras.destroy` |
| `obras.restore` / `obras.inactivate` | expostos no JSON do DataTable (`active`/`inactivate`), sem uso pela UI de obras |

Além disso, `resolve_permission_aliases()`
(`v5/app/Library/Helper/appHelpers.php:44`) remapeia
`obra-paralisada` → `obras`, `obras-paralisadas` → `obras` e
`categorias-obras` → `obras`, de modo que **as rotas de categorias de obra
herdam as permissões do módulo de obras**.

> **Estado atual do banco:** nenhum perfil em `roles` possui a chave `obras.*`
> (`SELECT id, slug FROM roles WHERE permissions::text ILIKE '%obras%'` → 0
> linhas). Na prática, **apenas `super-admin`** acessa o módulo — o
> `CheckRoutePermission` libera `super-admin` antes de qualquer verificação.

### 2.1 Como a tela de papel monta as permissões

`v5/resources/views/admin/role/form.blade.php` gera a chave
`{resource.slug}.{permission.slug}` para cada combinação
resource × permission e a grava no JSON `roles.permissions`. É exatamente o
formato que `has_access()` e `CheckRoutePermission` esperam — a reconstrução
deve preservar esse formato de chave para não invalidar perfis já
configurados.

### 2.2 Menu e contador de cliques

`menu_orders.slug = 'obras'` (id 33) é o item que o
`MenuServiceProvider` transforma no submenu "Obras" (com "Obras" e "Obras
Paralisadas"). Com `Configuration('menu_interativo') = 1`, a ordenação do menu
é `ORDER BY clicks DESC, id ASC` — ou seja, **o uso do módulo altera a ordem
dos itens do menu lateral**.

### 2.3 Matriz de verificação por rota

| Rota | Segmento derivado | Permissão exigida |
|---|---|---|
| `cms/obras` | `obras.index` | `obras.index` |
| `cms/obras/create` | `obras.create` | `obras.create` |
| `cms/obras/{id}/edit` | `obras.edit` | `obras.edit` |
| `cms/obras` (POST) | rota `obras.store` | `obras.create` (via alias do segmento) |
| `cms/obras/{id}` (PUT) | rota `obras.update` | `obras.edit` |
| `cms/obras/{id}` (DELETE) | rota `obras.destroy` | `obras.delete` |
| `cms/categorias-obras` | `categorias-obras.index` → alias `obras.index` | `obras.index` |
| `cms/categorias-obras/{id}` (DELETE) | `categorias-obras.destroy` → alias `obras.delete` | `obras.delete` |
| `cms/datatable/listagem_obras` | nome `datatable.` (não nomeado) | **`datatable` está em `openRoutes`** → liberado para qualquer usuário logado |
| `cms/galerias-obras` | nome `galeria-obras.` (não nomeado) | derivado do segmento da URL |
| `cms/obras/get-geolocation-data` | `obras.getGeolocationData` | derivado do segmento da URL |

> **Achado relevante:** as rotas de DataTable ficam dentro de `openRoutes`
> (que inclui `datatable`), logo **qualquer usuário logado** pode ler o JSON
> completo das obras e categorias — inclusive quando não tem `obras.index`.
> As rotas de galeria, por terem nome malformado (`galeria-obras.`), caem no
> caminho de derivação por segmento (`hasAccessSegment()`), que produz
> slugs como `galerias-obras.index` — não coincidentes com nenhuma permissão
> cadastrada, o que na prática **nega** o acesso a não-`super-admin`.
