# A Cidade — Views e UX

> Versão analisada: `v6`. Todos os caminhos foram conferidos no filesystem.

---

## 1. Mapa de views

### 1.1 Portal público

| Caminho | Existe | Função |
|---|---|---|
| `v6/resources/views/portal/layouts/modelo1/informacoes-instituicao/acidade/show.blade.php` | ✅ | Slot-filler do layout modelo1. |
| `v6/resources/views/portal/layouts/modelo2/informacoes-instituicao/acidade/show.blade.php` | ✅ | idem, modelo2. |
| `v6/resources/views/portal/layouts/modelo3/informacoes-instituicao/acidade/show.blade.php` | ✅ | idem, modelo3. |
| `v6/resources/views/portal/layouts/modelo4/informacoes-instituicao/acidade/show.blade.php` | ✅ | idem, modelo4. |
| `v6/resources/views/portal/layouts/modelo5/informacoes-instituicao/acidade/show.blade.php` | ✅ | idem, modelo5. |
| `v6/resources/views/portal/layouts/modelo6/informacoes-instituicao/acidade/show.blade.php` | ✅ | idem, modelo6. |
| `v6/resources/views/portal/layouts/modelo{1..6}/informacoes-instituicao/components/components-acidade/index.blade.php` | ✅ | Componente com o HTML da página (6 cópias). |
| `v6/resources/views/portal/layouts/modelo{1..6}/informacoes-instituicao/galeria/index.blade.php` | ✅ | Galeria lightGallery (6 cópias). |
| `v6/resources/views/portal/layouts/newsbit/informacoes-instituicao/components/components-acidade/index.blade.php` | ✅ | Componente existe, **mas não há `show.blade.php` no newsbit**. |
| `v6/resources/views/portal/layouts/default/pages/contents/a-cidade/show.blade.php` | ✅ | **Órfã** — nenhum controller a renderiza (ver §3). |
| `v6/resources/views/portal/statics/video.blade.php` | ✅ | Player do vídeo (iframe). |
| `v6/resources/views/admin/errors/conteudo-nao-encontrado.blade.php` | ✅ | Página de erro do controller do portal. |

### 1.2 CMS (AdminLTE)

| Caminho | Função |
|---|---|
| `v6/resources/views/prefecture/contents/a-cidade/index.blade.php` | Listagem: cabeçalho, botões e `<table id="tabela_a_cidade">`. |
| `v6/resources/views/prefecture/contents/a-cidade/create.blade.php` | Formulário de cadastro (`Form::open(['route' => ['conteudo.index']])`). |
| `v6/resources/views/prefecture/contents/a-cidade/edit.blade.php` | Formulário de edição (`Form::model($content, ['route' => ['conteudo.update', $content->id], 'method' => 'PUT'])`). |
| `v6/resources/views/prefecture/contents/a-cidade/form.blade.php` | Formulário **compartilhado** entre create e edit. |
| `v6/resources/views/prefecture/contents/components/galerias/index.blade.php` | Bloco de galeria (include do form quando `galery == 1`). |
| `v6/resources/views/prefecture/contents/components/editor-de-imagem/index.blade.php` | Editor de imagem (cropper). |

## 2. Anatomia da view do portal

O par `show.blade.php` + `components-acidade/index.blade.php` usa o padrão
**Blade Component com slots**. O `show` preenche os slots; o `index` renderiza o
HTML e estende o layout do modelo.

### 2.1 `show.blade.php` (modelo1, texto real)

```blade
@component('portal.layouts.modelo1.informacoes-instituicao.components.components-acidade.index')

    @slot('breadcrumb')
        {{  Breadcrumbs::view('portal.layouts.modelo1.layout.breadcrumb',  $contentNew->categoryContents->slug, $contentNew) }}
    @endslot

    @if(isset($contentNew->archiveHighlight->path))
        @if(file_exist($contentNew->archiveHighlight->path))
            @slot('imageHighlight')
                {{ $contentNew->archiveHighlight->path}}
            @endslot
        @endif
    @endif

    @slot('title')
        {{ isset($contentNew->title ) ? $contentNew->title  : ''}}
    @endslot

    @slot('text')
        {!!  isset($contentNew->text ) ? $contentNew->text  : '' !!}
    @endslot

    @if($contentNew->galery && !$contentNew->archiveGallery->isEmpty())
        @slot('gallery')
            @include("portal.layouts.modelo1.informacoes-instituicao.galeria.index")
        @endslot
    @endif

    @if(isset($contentNew->linksUnique->link))
        @slot('video')
            @php $content = $contentNew @endphp
            @include("portal.statics.video")
        @endslot
    @endif

@endcomponent
```

Contrato dos slots:

| Slot | Preenchido quando | Origem |
|---|---|---|
| `breadcrumb` | sempre | `Breads::view(...)` com `categoryContents->slug` |
| `imageHighlight` | `archiveHighlight->path` existe **e** o arquivo existe no disco | `archives.highlight = 1` |
| `title` | sempre | `contents.title` |
| `text` | sempre | `contents.text` (HTML cru, `{!! !!}`) |
| `gallery` | `contents.galery` **e** `archiveGallery` não vazia | `archives.highlight IS NULL` |
| `video` | `linksUnique->link` preenchido | `links.link` |

> `file_exist($contentNew->archiveHighlight->path)` — o path é **relativo**
> (`storage/content/a-cidade/...`) e é testado contra o **cwd**, não contra
> `public_path()`. Na prática o teste quase sempre falha e a imagem de
> destaque **nunca** aparece. Bug registrado em `08-GAPS-E-DIVIDAS.md`.

### 2.2 `components-acidade/index.blade.php` (modelo1)

Estrutura: `@extends('portal.layouts.modelo1.layout.layout')`; seção
`breadcrumb` com `{{ $breadcrumb }}`; seção `content` com o container
`<div id="a-cidade-estatic" class="col-xl-9 col-lg-9 col-md-8 col-sm-12">` que,
quando `$text` não é vazio, renderiza o título, a barra de cor, os **botões de
compartilhamento**, a imagem de destaque (`getLargFile($imageHighlight)`), o
`<p class="text-secondary text-content"> {{ $text }}</p>`, os slots `gallery` e
`video` — e, quando `$text` é vazio, o bloco "CONTEÚDO NÃO ENCONTRADO." com
ícone. Fecha com a seção `side-bar`, que inclui
`portal.layouts.modelo1.layout.sidebar.layout`.

### 2.3 Diferenças entre os 6 modelos

Os 6 `show.blade.php` são **quase idênticos** — a diferença é o nome do layout
e o include da galeria. Duas divergências relevantes:

| Modelo | Breadcrumb | Observação |
|---|---|---|
| modelo1, modelo2, modelo4, modelo5, modelo6 | `Breadcrumbs::view('...layout.breadcrumb', $contentNew->categoryContents->slug, $contentNew)` | Resolvido **pela categoria do conteúdo**. |
| **modelo3** | `Breadcrumbs::view('portal.layouts.modelo3.layout.breadcrumb', 'a-cidade', $contentNew)` | Literal `'a-cidade'` — o breadcrumb **sempre** diz "A Cidade", mesmo que o conteúdo seja de outra categoria. |

Os 6 `components-acidade/index.blade.php` divergem bastante entre si (116, 61,
111, 18 e 14 linhas de diff contra o modelo1). Diferenças observáveis: o
`modelo1` e o `modelo2` usam `<h4 class="text-uppercase txt-color-2 font-or">`
(o `modelo2` acrescenta a classe `image-summernote`) e `getLargFile()` para a
imagem; o `modelo3` usa `<h4 class="p-title ...">` e `url($imageHighlight)`
puro com `max-height: 600px`; o `modelo5` usa
`<p class="h4 text-uppercase txt-color-2 font-or">` e **não** renderiza a
imagem de destaque. Todos usam o bloco `addtoany`. O `modelo6` é o mais
próximo do `modelo1` (14 linhas de diff).

### 2.4 Galeria

> **Bug do modelo3 (verificado):**
> `v6/resources/views/portal/layouts/modelo3/informacoes-instituicao/acidade/show.blade.php:24`
> inclui `portal.layouts.modelo3.informacoes-prefeitura.galeria.index` — diretório
> que **não existe** na v6 (o diretório pai foi renomeado para
> `informacoes-instituicao`). O include só é alcançado quando o conteúdo tem
> `galery = 1` **e** `archiveGallery` não vazia; nesse caso a página **quebra**
> com "View [...] not found". Os outros cinco layouts usam o caminho correto.
> Na v5 o include era coerente — a regressão é da v6.

`v6/resources/views/portal/layouts/modelo1/informacoes-instituicao/galeria/index.blade.php` (replicado em `modelo2`..`modelo6`):

```blade
<div class="card p-3 news-details mb-3 mt-5 ">
    <h6 title="Imagens" class="text-secondary mb-4">Galeria de fotos</h6>
    <ul class="list-unstyled text-center" id="lightgallery">
        @foreach ($contentNew->archiveGallery as $archive)
            <li class="d-inline-block" data-thumb="{{ url(getMinFile($archive->path)) }}"
                        data-src="{{ url(getLargFile($archive->path)) }}">
                ...
            </li>
        @endforeach
    </ul>
</div>
```

Dependências: `assets/vendor/portal/css/lightgallery.css`,
`layouts/modelo1/css/gallery.css`,
`assets/plugins/lightgallery/picturefill.min.js`,
`assets/vendor/portal/js/lightgallery-all.min.min.js`. A inicialização é:

```js
$(document).ready(function() {
    $('#lightgallery').lightGallery({ getCaptionFromTitleOrAlt: false });
});
```

Não há filtro de tipo: um `archive` com `extension = 'pdf'` vira um `<li>` de
imagem quebrada.

### 2.5 Player de vídeo

`v6/resources/views/portal/statics/video.blade.php`:

```blade
<div class=" card p-3 my-4">
    <div class="embed-responsive embed-responsive-16by9">
        <iframe class="embed-responsive-item iframe-video w-100" src="{{ $linkVideo }}" allowfullscreen></iframe>
    </div>
</div>
```

`$linkVideo` já vem **pronto** do controller (`embedVideo()`), que converte a
URL original em uma URL de embed:

| Origem | URL gerada |
|---|---|
| `youtube.com/watch?v=ID` / `youtu.be/ID` | `https://www.youtube.com/embed/ID` |
| `vimeo.com/ID` | `https://player.vimeo.com/video/ID` |
| `facebook.com/...` | `https://www.facebook.com/plugins/video.php?href=<urlencode>&show_text=false` |
| `instagram.com/...` | URL pública normalizada + `/embed/` |

### 2.6 Página de erro

`v6/resources/views/admin/errors/conteudo-nao-encontrado.blade.php`:

```blade
@extends('portal.layouts.modelo2.layout.layout')
@section('content')
    <div class="col-xl-12 col-lg-12 col-md-12 col-sm-12">
        <div class="text-center  p-4 ">
            <h4 style="color: #ef6464;">CONTEÚDO NÃO ENCONTRADO.</h4>
            <div class="mt-5 text-secondary">
                <i class="fas fa-exclamation-triangle fa-9x"></i>
            </div>
        </div>
    </div>
@endsection
```

Duas observações: o layout é **fixo no modelo2**, independentemente do layout
ativo (um visitante no modelo5 que erre a URL vê o cabeçalho do modelo2); e a
resposta HTTP é **200** — o status de erro não é enviado, o que é ruim para SEO
e para monitoramento.

## 3. O layout `default` — view órfã e módulo inalcançável

**Verificado:**

| Item | Resultado |
|---|---|
| `v6/resources/views/portal/layouts/default/pages/contents/a-cidade/show.blade.php` | **Existe** (55 linhas) |
| `default/informacoes-instituicao/acidade/show.blade.php` (sob `v6/resources/views/portal/layouts/`) | **NÃO existe** |
| `default/home/index.blade.php` (mesmo prefixo) | **NÃO existe** (o `default` tem `pages/index.blade.php`) |
| `newsbit/home/index.blade.php` (mesmo prefixo) | **NÃO existe** |
| `home/index.blade.php` em `modelo1..modelo6` | **Existe** nos 6 |

Consequências:

1. O `AboutController` **nunca** renderiza a view do layout `default` — ele
   procura `portal.layouts.default.informacoes-instituicao.acidade.show`, que
   não existe.
2. Mas o `default` nem chega lá: `getTemplateActived()` aborta com 404 antes,
   porque `portal.layouts.default.home.index` não existe — e, como efeito
   colateral, **liga a manutenção automática do portal**
   (`configurations.pagina_em_manutencao = 1`).
3. A view `default/pages/contents/a-cidade/show.blade.php` é **código morto**:
   além de estar no caminho errado, ela espera a variável `$content` (e não
   `$contentNew`), usa `$null`, `$content->archives->where(...)` e o include
   `portal.layouts.default.pages.elements.gallery` — incompatível com as
   variáveis entregues pelo controller.

Verificação de reachability:

```bash
find v6/resources/views/portal -path "*informacoes-instituicao/acidade/show.blade.php"
# → modelo1, modelo2, modelo3, modelo4, modelo5, modelo6
```

## 4. CMS — o que o operador vê

### 4.1 Tela de listagem

`v6/resources/views/prefecture/contents/a-cidade/index.blade.php`:

- Título "Lista de A Cidade".
- Botão **"Cadastrar Conteúdo"** (`cadastrar/a-cidade`), exibido apenas se
  `has_access(['a-cidade.create'])`.
- Botão de **exclusão em lote**, exibido se `has_access(['noticias.delete'])` —
  **permissão errada**: a tela de A Cidade exige a permissão de *Notícias*.
- Cabeçalho da tabela com 4 colunas: (checkbox de seleção), `ID`, `Título`
  (largura 75%), `Ações`.
- Carrega `assets/js/custom/utilities/deleteAll.js` e
  `assets/js/custom/contents/acidade/datatable.js`.

### 4.2 Formulário (create e edit)

`v6/resources/views/prefecture/contents/a-cidade/form.blade.php`:

| Campo | Tipo | Observação |
|---|---|---|
| `category_contents_id` | hidden | `$categoryContent->id` (4). |
| `slug` | texto | Exibido **somente na edição**; mostra o prefixo `{{ url('a-cidade') }}/` ao lado e um link "Acessar" para `route('a-cidade.index', $content->slug)`. Na criação o slug é gerado pelo servidor. |
| `title` | texto, `maxlength=120` | — |
| `text` | textarea `summernote` | Editor rico; imagens upadas para `summernote-files/temporarios/` e movidas para `summernote-files/content/<slug>/<id>/` no `store()`. |
| `link` | texto, `maxlength=200` | "Video (Youtube)"; placeholder sugere `https://www.youtube.com/watch?v=YbJOTdZBX1g` ou o ID puro. |
| `archive_highlight` | imagem | Componente `components.inputs.image`. |
| `galery` | checkbox | "Ativar Galeria"; habilita a aba de galeria. |

Scripts carregados: `summernote.min.js`, `summernote-pt-BR.js`,
`summernote-image-atr.js`, `assets/js/custom/utilities/richtext/summernote.js`.
No `create.blade.php` há um script inline que troca o rótulo do botão para
"Salvar e ir para galeria" quando o checkbox está desmarcado — lógica
**invertida** (o texto aparece quando a galeria **está** desligada).

### 4.3 Grade (DataTable)

`v6/public/assets/js/custom/contents/acidade/datatable.js`:

```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-cidade/${data.slug}`, true, 'glyphicon glyphicon-link text-primary form_link', 'Acessar A cidade', '_blank')
                + createBTN(data.id, 'editar/a-cidade/' + data.id, data.edit, data.delete)
                + `</div>`;
        }
    }
];

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

O helper `runDatatables()` (`v6/public/assets/js/custom/utilities/table.js:3`)
monta a chamada AJAX:

```js
ajax: {
    // ...
    url: `/cms/datatable/${url}`,
```

e **acrescenta** uma coluna `value` (o `row_number()` da view SQL) e, como
`deleteAll = true`, uma coluna de seleção múltipla.

### 4.4 Exportações

`runDatatables()` habilita **quatro** botões de exportação, gerados **no
cliente** (DataTables Buttons + `pdfmake`/`jsPDF`):

| Botão | Tecnologia | Colunas |
|---|---|---|
| "Excel" | `extend: 'excel'` | `:visible:not(.column-actions)` |
| "PDF" | `extend: 'pdf'`, `orientation: 'landscape'`, `pageSize: 'A4'` | idem |
| "CSV" | `extend: 'csv'`, arquivo `dados_exportados_<data>.csv` | idem |
| "TXT" | `action` custom (separador `\t`, cabeçalho sublinhado) | idem |

O título do arquivo Excel/PDF é `Prefeitura de <cidade> - <data em pt-BR>`,
onde a cidade vem de `.user-panel .info p` do cabeçalho do CMS.

## 5. JS/Ajax — resumo dos endpoints

| Endpoint | Quem chama | Evento |
|---|---|---|
| `GET /cms/datatable/a_cidade` | `runDatatables()` | `DataTable` draw / ordenação / paginação |
| `POST /cms/ajax/generic/content/deleteAll` | `deleteAll.js` | clique em "Excluir selecionados" |
| `DELETE /cms/conteudo/delete/{id}` | botão de exclusão por linha | clique |
| `POST /cms/galeria/` e `GET /cms/galeria/{column?}/{id?}` | `ArchiveController@saveArchivesFromGalleryOrDriver` / `...@getArchivesFromGalleryOrDriver` | upload e listagem de anexos da galeria |
| `POST /cms/ajax-delete-files` (`ajax.delete.files`) | `edit.blade.php` (função `deleteHighlightArchive`) | remover a imagem de destaque |
| `POST /cms/conteudo` | `Form::open(['route' => ['conteudo.index']])` | submit do formulário |

Não há **polling** nem requisição periódica no módulo.

## 6. UX — o que o usuário final vê

| Elemento | Comportamento |
|---|---|
| URL | `/a-cidade/{slug}` — indexável, sem parâmetro de query. |
| Título | `contents.title` em `<h4 class="text-uppercase txt-color-2 font-or">`. |
| Corpo | HTML do Summernote renderizado cru (`{!! !!}`) — sem sanitização na view. |
| Imagem de destaque | Deveria aparecer abaixo do título; na prática não aparece (ver §2.1). |
| Galeria | Grade de miniaturas com lightGallery, se `galery = 1` — e no modelo3 a página quebra (ver §2.4). |
| Vídeo | `iframe` responsivo 16:9, se houver `linksUnique->link`. |
| Compartilhamento | WhatsApp, Facebook, copiar link, e-mail, imprimir (addtoany). |
| Menu lateral e breadcrumb | Include do sidebar do modelo (banners, notícias) e breadcrumb `Home › A Cidade › {título}`. |
| Erro de slug | Página "CONTEÚDO NÃO ENCONTRADO." com layout do modelo2, **HTTP 200**. |
| Contador de cliques | Incremental a cada visita, sem debounce nem sessão. |

## 7. UX — o que o operador vê

Telas: **listagem** em `/cms/conteudos/a-cidade` (não aparece no menu lateral —
`modulos.status = false`); **cadastro** em `/cms/conteudos/cadastrar/a-cidade`;
**edição** em `/cms/conteudos/editar/a-cidade/{id}` (mostra o link permanente e
o botão "Acessar"); **exclusão** por linha ou em lote (soft delete; remove também
os `menu_items` que apontavam para o conteúdo).

## 8. Inventário de assets

| Tipo | Caminho |
|---|---|
| JS da grade | `v6/public/assets/js/custom/contents/acidade/datatable.js` |
| JS de tabela | `v6/public/assets/js/custom/utilities/table.js` |
| JS de exclusão em lote | `v6/public/assets/js/custom/utilities/deleteAll.js` |
| JS do editor de texto e de imagem | `v6/public/assets/plugins/summernote/summernote.min.js` (+ `summernote-pt-BR.js`, `summernote-image-atr.js`); `v6/public/assets/js/modulos/cropper-min.js`; `assets/js/custom/utilities/helpers/editor-de-imagem.js` |
| CSS e JS da galeria | `v6/public/assets/vendor/portal/css/lightgallery.css`, `layouts/modelo1/css/gallery.css`; `v6/public/assets/vendor/portal/js/lightgallery-all.min.js` |
| Imagem padrão | `v6/public/storage/img/exemplos/sem-imagem-min.png` (usada no `edit.blade.php`) |

## 9. Replicação — o custo da replicação

| Artefato | Cópias | Linhas por cópia (aprox.) |
|---|---|---|
| `informacoes-instituicao/acidade/show.blade.php` | 6 | 36 |
| `informacoes-instituicao/components/components-acidade/index.blade.php` | 7 (inclui `newsbit`) | 18–63 |
| `informacoes-instituicao/galeria/index.blade.php` | 6 | 34 |

Mais de 250 linhas de Blade replicadas apenas por causa do prefixo do layout.
Cada correção no módulo precisa ser aplicada 6 vezes — e já divergiu entre os
modelos (ver §2.3 e o bug do modelo3 em §2.4).
