# Órgãos — Views e UX

> Escopo: arquivos Blade do CMS e do Portal, componentes de layout, JS/Ajax, exportações e o que o
> usuário efetivamente vê.

---

## 1. CMS — telas do operador

### 1.1 Inventário de views

| Arquivo | Papel |
|---|---|
| `v6/resources/views/prefecture/contents/orgaos/index.blade.php` | Listagem (caixa "Lista de Órgãos", botão de cadastro, botão de exclusão em lote, `<table id="tabela_orgaos">`) |
| `v6/resources/views/prefecture/contents/orgaos/create.blade.php` | Formulário de cadastro (`Form::open(['route' => ['conteudo.index'], 'files' => true])`) |
| `v6/resources/views/prefecture/contents/orgaos/edit.blade.php` | Formulário de edição (`Form::model($content, ['route' => ['conteudo.update', $content->id], 'method' => 'PUT'])`) + script de galeria |
| `v6/resources/views/prefecture/contents/orgaos/form.blade.php` | Corpo compartilhado entre create/edit (abas, campos) |

Todos estendem `admin.layouts.app` (AdminLTE 1.22 / Bootstrap 3).

### 1.2 A tela de listagem

`v6/resources/views/prefecture/contents/orgaos/index.blade.php` — estrutura real:

```blade
<div class="box-header with-border">
    <h3 class="box-title align-middle">Lista de Órgãos</h3>
    <div class="float-right">
        @if (has_access(['orgaos.create']))
            <a href="cadastrar/orgaos" class="btn btn-success font-weight-bold btn-sm" role="button"><i
                    class="fa fa-plus"></i> Cadastrar Órgão</a>
        @endif
    </div>
    @if (has_access(['orgaos.delete']))
        <div class="float-right pr-1">
            @component('prefecture.components.deleteByLoteSubmitButton', ['route' => 'ajax.generic.content.deleteAll'])
            @endcomponent
        </div>
    @endif
</div>
...
<table id="tabela_orgaos" class="table table-striped w-100">
    <thead>
        <tr>
            <th>
                @if (has_access(['orgaos.delete']))
                    @component('prefecture.components.deleteByLoteMasterCheckbox')
                    @endcomponent
                @endif
            </th>
            <th>ID</th>
            <th width="70%">Título</th>
            <th class="column-actions text-center" width="20%">Ações</th>
        </tr>
    </thead>
    <tbody>
    </tbody>
</table>
```

Pontos observáveis:

- O `<tbody>` é **vazio**; toda a grade é montada pelo JS via XHR a `/cms/datatable/orgaos`.
- O link "Cadastrar Órgão" usa uma **URL relativa** (`cadastrar/orgaos`) em vez de `route(...)`. Resolvida a
  partir de `/cms/conteudos/orgaos`, ela aponta para `/cms/conteudos/cadastrar/orgaos`, que é a rota real
  (`cadastrar-conteudo`). Funciona, mas quebra se a rota do CMS mudar de prefixo.
- A coluna "ID" exibe `value` (o `row_number()` da view), não o `id` real do conteúdo.

### 1.3 O formulário (`form.blade.php`)

Campos e regras visíveis:

| Campo | Tipo | Validação (ContentRequest) |
|---|---|---|
| `category_contents_id` | hidden, fixado em `{{ $categoryContent->id }}` | `required|numeric` |
| `slug` | input de texto, exibido só quando `$content` existe | gerado pelo servidor |
| `title` | `Form::text` com `maxlength="120"` | `required|max:120` |
| `text` | `<textarea class="summernote">` | `required` |
| `link` | `Form::text` com `maxlength="200"` | `max:2999` |
| `archive_highlight` | `<input type="file" accept="image/*">` (componente `components.inputs.image`) | `max:20000|mimes:jpg,jpeg,png,gif,webp` |
| galeria | `prefecture.contents.components.galerias.index`, só se `$content->galery == 1` | — |

Trecho real do "Link Permanente":

```blade
<div class="mr-2">{{ url('a-instituicao/orgaos') }}/</div>

@include('components.inputs.input', [
    'size' => 4,
    'id' => 'slug',
    'data' => isset($content) ? $content->slug : old('slug'),
])

<div class="mr-2">
    <a href="{{ route('a-instituicao-orgaos.index', $content->slug) }}" target="_blank">
        Acessar <i class="fa fa-external-link fa-1x"></i>
    </a>
</div>
```

> O prefixo exibido (`/a-instituicao/orgaos`) e a rota usada no link (`a-instituicao-orgaos.index`) são
> coerentes na v6. Na v5 o prefixo era `/a-prefeitura/orgaos` (prefixo de rota antigo) — ver capítulo 8.
>
> O campo `slug` é **somente leitura visual**: o valor real é recalculado pelo servidor em
> `ContentController::store()` a partir do `title`. Digitar no campo não altera a URL gerada.

### 1.4 Scripts da tela

| Script | Origem | Função |
|---|---|---|
| `assets/js/custom/utilities/deleteAll.js` | `url()` | Exclusão em lote (`toDeleteAll[]`) via `ajax.generic.content.deleteAll` |
| `assets/js/custom/contents/orgaos/datatable.js` | `asset()` | Grade DataTable |
| `assets/plugins/summernote/summernote.min.js` + `summernote-pt-BR.js` + `summernote-image-atr.js` | `url()` | Editor rico do `text` |
| `assets/js/custom/utilities/richtext/summernote.js` | `url()` | Configuração do Summernote |
| `assets/js/custom/utilities/helpers/validacao-texto.js` | `url()` (create/edit) | Validação de texto |
| `assets/js/custom/utilities/helpers/select-image.js`, `assets/js/modulos/cropper-min.js`, `assets/js/custom/utilities/helpers/editor-de-imagem.js` | `url()` (create/edit) | Seleção/recorte da imagem de destaque |
| `assets/plugins/jquery-upload-files/jquery.uploadfile.min.js` | `url()` (edit) | Upload múltiplo da galeria (rota `ajax.upload.files`) |

### 1.5 JS da grade

`v6/public/assets/js/custom/contents/orgaos/datatable.js` (integral):

```js
let collums = [
    {
        data: 'titulo',
        render: function (data) {
            return `<div class='p-10-0'>${data}</div>`;
        }
    },
    {
        data: null,
        name: "actions",
        orderable: false,
        render: function (data) {
            return `<div class='flex justify-center vertical-middle'>`
                + createSingleButton(data.id + 'Link',`/a-instituicao/orgaos/${data.slug}`, true, 'glyphicon glyphicon-link text-primary form_link', 'Acessar orgão', '_blank')
                + createGaleryButton(`editar/orgaos/${data.id}#galeria`, data.galeria, data.edit)
                + createBTN(data.id, 'editar/orgaos/' + data.id, data.edit, data.delete)
                + `</div>`;
        }
    }
];

runDatatables('orgaos', collums, true, 'ASC', null, true);
```

O helper `runDatatables` (`v6/public/assets/js/custom/utilities/table.js:3-...`) faz:

1. `collums.unshift({data: 'value', ...})` — injeta a coluna ordinal (`value` da view SQL).
2. `collums.unshift({... createDeleteAllButton(data.id, data.delete)})` — injeta o checkbox de lote
   (porque `deleteAll = true`).
3. Cria os botões de exportação **Excel, PDF, CSV e TXT** (cliente-side, via DataTables Buttons).
4. Aponta o `ajax` para `GET /cms/datatable/orgaos` (o nome `runDatatables('orgaos', ...)` casa com
   `#tabela_orgaos` e com o endpoint).

Helpers de botão (mesmo arquivo):

| Função | Linha | Comportamento |
|---|---|---|
| `createSingleButton(id, url, permission, iconClass, tooltip, target)` | 266 | `<a target="_blank">` com tooltip; devolve string vazia se `permission === false` |
| `createGaleryButton(url, galeria, editable)` | 310 | Se `galeria` for falso, o `href` vira `#` e o botão recebe `disabled` |
| `createBTN(id, url, editable, deletable)` | 227 | Botão de editar + botão de excluir (`data-toggle="delete_btn"`) |

> O botão "Acessar órgão" é renderizado com `permission = true` fixo — **não** depende de
> `has_access()`, porque é um link público para o portal.
> O botão de galeria e o de editar dependem de `data.edit` (= `has_access('orgaos.edit')`);
> o de excluir depende de `data.delete` (= `has_access('orgaos.delete')`).

### 1.6 Exportações

| Formato | Onde é gerado | Observação |
|---|---|---|
| Excel | Botão DataTables `extend: 'excel'` | `exportOptions: { columns: ':visible:not(.column-actions)' }`; título `Prefeitura de <cidade> - <dd/mm/aaaa>` |
| PDF | Botão DataTables `extend: 'pdf'` | Landscape, A4, larguras de coluna calculadas por JS (`doc.content[1].table.widths`) |
| CSV | Botão DataTables `extend: 'csv'` | Arquivo `dados_exportados_<yyyy-mm-dd>` |
| TXT | Botão DataTables com `action` customizada | Usa `dt.buttons.exportData()` |

Como a grade já materializou **todas** as linhas em PHP antes da paginação do Yajra, a exportação inclui
a lista completa — o que, para órgãos, é o comportamento desejado (poucos registros).

### 1.7 Superfície de dados abertos

`GET /api/v1/orgaos` devolve JSON puro (envelope `data` do Laravel Resource) com as chaves `titulo`,
`descricao`, `interacoes`, `slug` e `arquivos`. Não há Blade nem JS envolvido. Contrato completo em
`09-API-E-DADOS-ABERTOS.md`.

## 2. Portal — o que o cidadão vê

### 2.1 Estrutura de templates

```
portal/layouts/modeloN/informacoes-instituicao/a-instituicao/show.blade.php
   └─ @component('portal.layouts.modeloN.informacoes-instituicao.components.components-a-prefeitura.index',
                 ['sidenews' => ..., 'eventsList' => ...])
        ├─ @slot('breadcrumb')  → Breadcrumbs::view('portal.layouts.modeloN.layout.breadcrumb',
        │                                 $contentNew->categoryContents->slug, $contentNew)
        ├─ @slot('title')       → $contentNew->title
        ├─ @slot('imageHighlight') → $contentNew->archiveHighlight->path (se file_exist)
        ├─ @slot('text')        → $contentNew->text (HTML, sem escape)
        ├─ @slot('gallery')     → @include('portal.components.galerias.index',
        │                                ['archives' => $contentNew->archiveGallery])
        ├─ @slot('noticias')    → só se isset($news_secretary) && count != 0  (NUNCA no ramo órgãos)
        ├─ @slot('video')       → @include('portal.statics.video') quando há linksUnique->link
        └─ @section('side-bar') → @include("portal.layouts.modeloN.layout.sidebar.layout")
```

Views por modelo (todas existentes):

| Modelo | Página do órgão | Componente | Include da lista de notícias |
|---|---|---|---|
| `modelo1` | `v6/resources/views/portal/layouts/modelo1/informacoes-instituicao/a-instituicao/show.blade.php` | `.../modelo1/informacoes-instituicao/components/components-a-prefeitura/index.blade.php` | `portal.layouts.modelo1.layout.sidebar.orgaosNews` (linha 97) |
| `modelo2` | `.../modelo2/...` | `.../modelo2/...` | **`portal.layouts.modelo1.layout.sidebar.orgaosNews`** (linha 97) — **include do modelo1** |
| `modelo3` | `.../modelo3/...` | `.../modelo3/...` | `portal.layouts.modelo3.layout.sidebar.orgaosNews` (linha 100) |
| `modelo4` | `.../modelo4/...` | `.../modelo4/...` | `portal.layouts.modelo4.layout.sidebar.orgaosNews` (linha 92) |
| `modelo5` | `.../modelo5/...` | `.../modelo5/...` | `portal.layouts.modelo5.layout.sidebar.orgaosNews` (linha 97) |
| `modelo6` | `.../modelo6/...` | `.../modelo6/...` | `portal.layouts.modelo6.layout.sidebar.orgaosNews` (linha 95) |

> **Divergência real:** o `modelo2` inclui o arquivo do `modelo1`. Não existe
> Não existe um `orgaosNews.blade.php` em `v6/resources/views/portal/layouts/modelo2/layout/sidebar/` — confirmado por
> listagem do diretório. Consequência: no `modelo2`, a lista de notícias do órgão aparece com o CSS do
> `modelo1` (larguras fixas de 350x100 px, classes `margin-bottom`/`flex` redefinidas no include).
>
> O `ModulosServiceProvider` registra composers para
> `portal.layouts.modeloN.layout.sidebar.orgaosNews` (N = 1..6), mas a view do `modelo2` **não existe** —
> esse composer nunca dispara. Na prática, o include efetivo é o do `modelo1`, que **é** composed, e por
> isso `$modulos` chega ao include renderizado (a variável é usada na primeira linha do arquivo,
> `@if(isset($modulos['noticias']) && $modulos['noticias'])`).
>
> `default` e `newsbit` **não possuem** `informacoes-instituicao/a-instituicao/show.blade.php`; como
> `getTemplateActived()` só aceita layouts com `home/index.blade.php`, esses dois layouts são inalcançáveis
> para este módulo.

### 2.2 A lista de notícias do órgão

`v6/resources/views/portal/layouts/modelo1/layout/sidebar/orgaosNews.blade.php`:

```blade
@if(isset($modulos['noticias']) && $modulos['noticias'])
    ...
    <div class="container">
        <p class="mt-5 h4 text-uppercase font-or txt-color-2 "> Notícias de {{ $title }}</p>
        <i class="border-title d-block bg-color-1 mb-3"></i>
        @if (getStatusDisplayNews()['displayNews'] == 0)
            @if (isset($sidenews) && isset($sidenews[0]))
                <ul class="list-unstyled more-news row">
                    @foreach ($sidenews as $noticia)
                        ...
                        <a href="{{ route('noticia.show', $noticia->slug) }}" ...>
                            <img src="{{ getMinFile(file_exist_portal($destaque)) }}" ...>
                            <p class="badge ...">{{ date('d.m.Y', strtotime($noticia->publication_date)) }}</p>
                            <p title="{{ $noticia->title }}" class="h6 mb-1 w-50">{{ str_limit($noticia->title, 20) }}</p>
                            <span ...>@php echo($noticia->categoryContents->name) @endphp</span>
                        </a>
                    @endforeach
                </ul>
            @else
                <p>Nenhuma notícia cadastrada</p>
            @endif
        @else
            <div class="my-3">
                <img src="{{ url(getImageNewsDisabled()) }}" alt="Aviso" style="width: 100%">
            </div>
        @endif
    </div>
@endif
```

Regras de exibição:

| Condição | Resultado |
|---|---|
| `modulos['noticias']` falso | O bloco inteiro **não é renderizado** (nem título, nem aviso) |
| `modulos['noticias']` verdadeiro e `getStatusDisplayNews()['displayNews'] != 0` | Exibe a imagem de "notícias desabilitadas" (`getImageNewsDisabled()`) no lugar da lista |
| `displayNews == 0` e `$sidenews` vazio | Exibe "Nenhuma notícia cadastrada" |
| `displayNews == 0` e `$sidenews` com itens | Lista todas as notícias vinculadas ao órgão |

> **Sem paginação:** o `@foreach` percorre **toda** a coleção `$sidenews` (que já vem sem `limit` do SQL).
> Não há `->links()`, `paginate()` nem corte em Blade. Ver capítulo 5.
>
> `getStatusDisplayNews()` (`v6/app/Helpers/functions.php:594-604`) lê as configurações `noticias` e
> `ultimas_noticias`.

### 2.3 A agenda de eventos

Renderizada dentro do componente `components-a-prefeitura/index.blade.php` (bloco "Agenda de eventos"),
com `route('agenda-eventos.show', [$event->categoryCalendarEvents->name, $event->slug])`. A lista vem de
`$eventsList`, limitado a 5 eventos com `status = '1'`, `final_date >= now()` e
`orgaos_id = <id do órgão>` — o único ponto do módulo em que o limite é aplicado no SQL.

### 2.4 A galeria

`v6/resources/views/portal/components/galerias/index.blade.php` filtra as extensões de imagem
(`jpg, jpeg, png, gif, bmp, tiff, svg, webp, ico, heic, raw`) sobre `$archives` e monta uma galeria
lightbox (`id="lightgallery"`). É alimentado por `$contentNew->archiveGallery`
(`hasMany(Archive::class,'contents_id','id')->byHighLight(null)->orderBy('created_at','desc')`) e só é
incluído quando `$contentNew->galery` é verdadeiro.

### 2.5 O menu do portal

O menu é injetado por `MenuServiceProvider` como `$menuHeader` nas views `portal.layouts.modeloN.layout.*`.
A navegação principal (`modelo1..modelo5` -> `layout/nav.blade.php`; `modelo6` -> `layout/header/nav.blade.php`)
itera `$menuHeader->where('is_active', true)` e monta até 3 níveis via `subItens`.

O link de cada órgão é calculado por `MenuItemPresenter::getLink()`
(`v6/app/Http/Presenters/MenuItemPresenter.php`), que tem um caso **hardcoded**:

```php
} 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);
}
```

Com `category_contents_id = 32` e `content->slug = 'procon'`, o link gerado é
`/a-instituicao/orgaos/procon` — que casa com a rota `a-instituicao-orgaos.index`.

> O item agrupador "Órgãos" (id 16) tem `type = 0` (link externo, `links_id = 30`), logo seu link vem de
> `links.link` e **não** do presenter. É ele que abre o dropdown com os órgãos filhos.
>
> **Não existe `pub-nav.blade.php` para o módulo Órgãos.** O padrão `pub-nav` (navegação de subcategoria
> por área) existe em `default/pages/elements` (publicacoes), `modelo1..modelo6/atas-e-pontos`,
> `modelo1..modelo6/comite-de-investimentos`, `modelo3/4/5/6/publicacoes` e `modelo3/4/requerimentos` —
> **não** para `orgaos`. Os órgãos usam o menu principal (`layout/nav.blade.php`) como canal de navegação.

## 3. Fluxo de telas do operador

```
 /cms/modulos ──(liga/desliga o módulo 'orgaos')──┐
                                                   ▼
 /cms/conteudos/orgaos  ──── GET /cms/datatable/orgaos ──── grade
        │  [Cadastrar Órgão]                          │
        ▼                                             ▼
 /cms/conteudos/cadastrar/orgaos              editar/orgaos/{id}
        │  (POST /cms/conteudo)                          │ (PUT /cms/conteudo/{id})
        ▼                                                ▼
 /cms/conteudos/orgaos  ◄────────────── (redirect) ─────┘
        │
        └─ [Acessar órgão] ──► /a-instituicao/orgaos/{slug}  (portal público, nova aba)
```

## 4. Resumo de UX por perfil

| Perfil | O que vê |
|---|---|
| Operador do CMS (com `orgaos.index`) | Lista de órgãos; botões condicionais por permissão; url do portal; galeria; exclusão em lote |
| Operador do CMS (sem `orgaos.index`) | Menu lateral do CMS oculta a entrada "Órgãos" (pois `modulos.status` é `false`); as rotas continuam acessíveis por URL |
| Cidadão | Página do órgão: título, destaque, texto, galeria, vídeo, agenda de eventos, notícias vinculadas. Sem listagem geral de órgãos e sem paginação da lista de notícias |
| Consumidor da API | JSON com `titulo`, `descricao`, `interacoes`, `slug`, `arquivos` |
