# PLATAFORMA — Widgets, Tags e UX do CMS

> Continuação de `04-VIEWS-E-UX.md`. Ver também `04-VIEWS-E-UX.md`.

## 3. Sistema de widgets / tags — a tabela `layouts`

### 3.1 Estrutura da tabela

`v6/database/migrations/2022_05_30_104340_create_layouts_table.php` +
`2024_10_20_070912_add_title_to_layout_table.php` +
`2025_04_27_225303_add_column_columns_to_layout_table.php`.

| Coluna | Tipo | Papel |
|---|---|---|
| `id` | `integer` PK | — |
| `key` | `varchar(191)` | Identificador do widget/bloco (`tags`, `menu`, `destaques`, `ultimas-noticias`…) |
| `section` | `varchar(191)` | Agrupamento (`acessibilidade`, `header`, `content`, `footer`…) |
| `type` | `varchar(191)` | `section` (12 linhas), `element` (27 linhas) ou vazio (2 linhas) |
| `value` | `text` | **HTML do bloco** (para `preview` e `tags`) ou valor simples |
| `order` | `integer` | Ordem de exibição dentro da seção |
| `style` | `text` | CSS inline do elemento |
| `status` | `boolean` | Ativo |
| `path` | `varchar(191)` | Caminho do bloco no tema, ex.: `portal.layouts.modelo5.home.destaques` |
| `title` | `varchar(191)` | Título exibido (ex.: "Últimas Notícias", "Mais Lidas") |
| `columns` | `varchar(191)` | Nº de colunas da grade (grid) |
| `created_at` / `updated_at` | `timestamp(0)` | — |

**41 linhas** no banco implantado.

### 3.2 Hierarquia section → element

Exemplo real (do banco):

| id | key | section | type | order | path |
|---|---|---|---|---|---|
| 4 | `acessibilidade` | — | section | 1 | `portal.layouts.modelo5.layout.header.acessibilidade` |
| 5 | `ancoras` | acessibilidade | element | 1 | `portal.layouts.modelo5.layout.header.acessibilidade` |
| 6 | `menu` | acessibilidade | element | 2 | `portal.layouts.modelo5.layout.header.acessibilidade` |
| 7 | `header` | — | section | 1 | `portal.layouts.modelo5.layout.header` |
| 8 | `logo` | header | element | 1 | `portal.layouts.modelo5.layout.header.nav-header` |
| 9 | `menu-principal` | header | element | 2 | `portal.layouts.modelo5.layout.header.nav-header` |
| 10 | `search` | header | element | 3 | `portal.layouts.modelo5.layout.header.nav-header` |
| 11 | `content` | — | section | 1 | `portal.layouts.modelo5.layout.header` |
| 12 | `covid` | content | section | 1 | `portal.layouts.modelo5.home` |
| 13 | `vacinometro` | covid | element | 1 | `portal.layouts.modelo5.home.covid` |
| 14 | `destaque` | content | section | 2 | `portal.layouts.modelo5.home` |
| 15 | `carousel-noticias` | destaques | element | 1 | `portal.layouts.modelo5.home.destaques` |
| 16 | `noticias-destaque` | destaques | element | 2 | `portal.layouts.modelo5.home.destaques` |
| 17 | `home-news` | content | section | 3 | `portal.layouts.modelo5.home` |
| 18 | `banners` | home-news | element | 1 | `portal.layouts.modelo5.home.news.sidebar` |
| 19 | `ultimas-noticias` | home-news | element | 1 | `portal.layouts.modelo5.home.news.more-news` |
| 20 | `more-popular` | home-news | element | 2 | `portal.layouts.modelo5.home.news.more-news` |
| 21 | `transparencia` | content | section | 1 | `portal.layouts.modelo5.home.transparencia` |
| 22 | `image-transparencia-fiscal` | transparencia | element | 1 | … |
| 23 | `transparencia-fiscal` | transparencia | element | 2 | … |
| 26 | `super-banners` | content | section | 1 | `portal.layouts.modelo5.home.super-banners` |
| 29 | `footer` | — | section | 1 | `portal.layouts.modelo5.layout.footer` |
| 38 | `vereadores` | content | section | 99 | `portal.layouts.modelo5.home` |
| 41 | `sessoes` | content | section | 98 | `portal.layouts.modelo5.home` |

**Observação:** todas as 39 linhas com `path` apontam para **modelo5**. Ou
seja, o configurador de layout do CMS só sabe editar o **modelo5**, mesmo
existindo 6 modelos.

### 3.3 Os dois blocos de HTML editável

| `key` | Tamanho do `value` | Conteúdo |
|---|---|---|
| `preview` | **77.329** bytes | HTML completo do preview do layout (o editor visual) |
| `tags` | **76.302** bytes | HTML das tags/blocos editáveis |

O resto das 39 linhas tem `value` vazio (0 bytes).

### 3.4 Editor de layout (CMS)

`v6/app/Http/Controllers/Prefecture/LayoutController.php`:

| Método | Ação | Efeito no banco |
|---|---|---|
| `edit()` | Carrega o formulário | Lê `preview` + `tags` (valor) e os `element` com `style` não-nulo |
| `save_layout(Request)` | Salva | `UPDATE layouts SET value` para `preview` e `tags`; atualiza `title`, `columns` e `style` conforme o prefixo `input-` do request |
| `updateModuleStatus(Request)` | Troca o módulo do grid | `updateOrInsert` em `layouts` com `key = 'secretarias-grid'` e `value = "modulo[:subId]"` |
| `orderLayout(Request)` | Reordena | Atualiza `order`, `status` e `section` de cada elemento |

Trecho real (`save_layout`, linhas 66-90):

```php
Layout::where(['key' => 'preview'])->update(['value' => $request->html]);
Layout::where(['key' => 'tags'])->update(['value' => $request->htmlTags]);

$inputs = collect($request->all())->filter(function ($value, $key) {
    return str_contains($key, 'input-');
});

foreach ($inputs as $key => $value) {
    if ($key === 'input-title-ultimas-noticias') {
        Layout::where('key', 'ultimas-noticias')->update(['title' => $value]);
    } …
    } else {
        Layout::where(['key' => str_replace('input-', '', $key)])->update(['style' => $value]);
    }
}
```

**Armadilha:** o `else` final faz `str_replace('input-', '', $key)` e escreve
`style` na linha cujo `key` seja o nome do campo. Se o campo não existir na
tabela `layouts`, o `update()` **não affected rows** e é silenciosamente
ignorado — falha sem feedback.

A view do editor é `v6/resources/views/prefecture/configurations/modelo5/edit.blade.php`
(que carrega `previewValue` e `tagsValue`).

### 3.5 Efeito do layout no conteúdo — o caso `carousel-noticias`

`v6/app/Http/Traits/NewsTrait.php:176-187`:

```php
if ($currentLayout == 'modelo5') {
    $layout_carousel_destaque = Layout::where('key', 'carousel-noticias')->value('section');
    if ($layout_carousel_destaque == 'tags') {
        $contents = Content::whereIn('category_contents_id', $subcategories_ids)
            ->whereNull('deleted_at')
            ->where('highlights', true)
            ->orderBy('publication_date', 'desc')
            ->paginate($paginate);
        return $contents;
    }
}
```

Ou seja: **o widget `carousel-noticias` decide se o carrossel mostra os
"destaques" ou os "mais recentes"**, e essa decisão vem do campo `section`
da tabela `layouts`. Configurar o layout muda o conteúdo exibido — não só a
aparência. Isso é um requisito de equivalência funcional na v7.

Também `v6/app/Helpers/functions.php:1091-1128` (`lastNewsForLayout()`)
calcula o número de notícias por modelo (`modelo5` → 8, `modelo6` → 4), para
evitar exibição duplicada entre o carrossel e a listagem.

---

## 4. CMS — AdminLTE 1.22

### 4.1 Layout base

`v6/resources/views/admin/layouts/app.blade.php`:

```blade
@extends('adminlte::page')
@section('title', 'PREFEITURAV4')
```

Assets carregados (todos com `HTML::script`, facade `laravelcollective/html`):

- **AngularJS 1.x** (`angular.min.js`, `angular-animate`, `angular-aria`,
  `angular-messages`, `angular-route`) — herdado de `jadjoubran/laravel-angular`.
- **DataTables** (`datatables.min.js`) + botões (`jszip`, `pdfmake`, `vfs_fonts`).
- `icheck.js`, `bootstrap-confirmation.min.js`, `jquery.mask.min.js`.
- `sweetalert2.min.js`, `toastr.min.js`.
- Utilitários próprios: `assets/js/custom/utilities/helpers/prefecture.js`,
  `table.js`, `exec.js`, `functions.js`, `datatables-buttons-config.js`.

### 4.2 Configuração do AdminLTE

`v6/config/adminlte.php`:

```php
'title' => 'PREFEITURAV4',
'title_prefix' => 'P',
'title_postfix' => 'V',
'logo' => '<b>PREFEITURA</b>V4',
'logo_mini' => '<b>P</b>V',
```

### 4.3 Menu lateral dinâmico

`v6/app/Providers/MenuServiceProvider.php` escuta
`JeroenNoten\LaravelAdminLte\Events\BuildingMenu` e monta a barra lateral
a partir de `menu_orders` (45 linhas) + `modulos`:

```php
$menus = MenuOrder::with('modulo')->orderBy('clicks', 'DESC')->orderBy('id', 'ASC')->get();

foreach ($menus as $menu) {
    if (has_access("$menu->slug.index") && isset($menu->modulo->status) && $menu->modulo->status) {
        switch ($menu->slug) {
            case 'noticias':
                $event->menu->add(['text' => 'Notícias', 'url' => '/cms/conteudos/noticias', 'icon' => 'newspaper-o']);
                break;
            …
```

**Três condições** para um item aparecer: (a) o usuário tem a permissão
`<slug>.index`; (b) o módulo tem `status` verdadeiro; (c) o item existe em
`menu_orders`.

O slug do `menu_order` **não é igual** ao slug do módulo em todos os casos —
ex.: `menu_orders.slug = 'enquete'` aponta para `modulos.slug = 'enquetes'`;
`'formulario-dinamico'` → `'formularios-dinamicos'`; `'lai'` → `'solicitacoes-lai'`.
Essa correspondência é feita pela migration
`v6/database/migrations/2026_09_01_100000_fix_menu_orders_missing_modulo_id.php:27`
(`DB::table('modulos')->where('slug', $menuOrder->slug)->first()`) — **mas ela
busca pelo slug exato**, logo os casos divergentes acima só funcionam porque
foram corrigidos manualmente. Fragilidade a mapear.

### 4.4 Dashboard

- View: `v6/resources/views/admin/layouts/dashboard.blade.php` (legado:
  `dashboard-legacy.blade.php`).
- Widgets: 16 classes em `v6/app/Services/Dashboard/Widgets/` —
  `ConteudosVisitadosWidget`, `DiarioWidget`, `EventosWidget`, `GaDevicesWidget`,
  `GaGeoWidget`, `GaLineWidget`, `GaOverviewWidget`, `GaTopPagesWidget`,
  `LaiWidget`, `LegislacoesWidget`, `LicitacoesWidget`, `NoticiasWidget`,
  `OuvidoriaWidget`, `PublicacoesWidget`, `RevisoesWidget`, `UsuariosWidget`.
- Persistência do grid por usuário: `dashboard_widget_instances` (16 linhas),
  model `v6/app/Http/Model/Admin/DashboardWidgetInstance.php`.
- Registry: `v6/app/Services/Dashboard/WidgetRegistry.php` + `AbstractWidget`
  + interface `WidgetInterface`.

### 4.5 Acessibilidade no CMS

O dashboard expõe, em `v6/resources/views/admin/layouts/dashboard.blade.php:62,70`:

```html
<select class="form-control dashboard-widget-period" data-widget-period aria-label="Período">
<select class="widget-size form-control input-sm hidden" data-widget-size aria-label="Tamanho do widget">
```

A raiz do portal tem `barra-privacidade` (barra de privacidade LGPD) e o
componente `v6/resources/views/components/weather-box.blade.php` (que consome
`getActualLayout()`).

---

## 5. JavaScript e assets

- `v6/public/assets/` — `components`, `css`, `fontes`, `functions`, `images`,
  `jquery`, `js`, `landing-covid`, `plugins`, `vendor`.
- **2.026** arquivos `.js` servidos estaticamente (sem pipeline de build
  obrigatório; existe `v6/webpack.mix.js` e `v6/package.json`).
- `v6/resources/views/portal/layouts/modelo*/noticias/show.blade.php:39` (6
  cópias, uma por modelo) contém um `dd()` comentado dentro de `{{-- --}}`.
- A facade `HTML::` (pacote `laravelcollective/html`, **abandonado**) é usada em
  **37** Blades — é ponto de atrito na migração (Laravel 11 já traz o
  `<x-...>` e `Illuminate\Support\HtmlString`).

---

## 6. Exportações (Excel/PDF/CSV)

| Tipo | Onde | Como |
|---|---|---|
| Excel (XLSX) | `v6/app/Exports/` (8 classes) | Maatwebsite Excel `FromView` → renderiza `resources/views/exports/*.blade.php` |
| Excel (portal) | Rotas `publicacoes/export`, `legislacao/export`, `emendas-parlamentares/export`, `viap/export` | Controller → `Excel::download(...)` |
| PDF (diário) | `v6/app/Services/Pdf/PdfConverter.php` | Imagick (PDF→JPEG) + FPDI/TCPDF (montagem) |
| PDF (carta de serviços) | `Prefecture\ServiceLetter\ServiceLetterPdfController` | `barryvdh/laravel-dompdf` |
| PDF (FPDF) | `v6/app/Services/FPdfService.php` | FPDF puro |

**Não há fila**: `grep ShouldQueue` em `v6/app` → **0 ocorrências**. Todos os
PDFs e exportações são gerados **sincronamente dentro da requisição HTTP**.
Para a v7 isso é impracticável nos volumes do diário oficial — ver
`06-MIGRACAO-MODERNA.md`.

---

## 7. Referências

| Item | Caminho |
|---|---|
| Escolha do tema (trait) | `v6/app/Http/Traits/GetInformationsPrefectureTrait.php` |
| Escolha do tema (duplicado) | `v6/app/Http/Controllers/Admin/Controller.php` |
| Função global de layout | `v6/app/Helpers/functions.php` |
| Editor de layout (CMS) | `v6/app/Http/Controllers/Prefecture/LayoutController.php` |
| View do editor | `v6/resources/views/prefecture/configurations/modelo5/edit.blade.php` |
| Model de layout | `v6/app/Http/Model/Prefecture/Layout.php` |
| Migration da tabela `layouts` | `v6/database/migrations/2022_05_30_104340_create_layouts_table.php` |
| Seed da config `layout` | `v6/database/seeds/ConfigurationsTableSeeder.php` |
| Menu lateral do AdminLTE | `v6/app/Providers/MenuServiceProvider.php` |
| Config do AdminLTE | `v6/config/adminlte.php` |
| Layout base do CMS | `v6/resources/views/admin/layouts/app.blade.php` |
| Dashboard do CMS | `v6/resources/views/admin/layouts/dashboard.blade.php` |
| Widgets do dashboard | `v6/app/Services/Dashboard/Widgets/` |
| influencing o carrossel | `v6/app/Http/Traits/NewsTrait.php` |
