# Temas e Cores — Gaps, Dívidas e Comparativo v5 → v6

> **Slug do módulo:** `temas-e-cores` (tabela `modulos`, id 2)

## 1. Sobreposição de tabela e controller

O módulo `temas-e-cores` **não tem tabela, model nem controller próprios**. Ele divide:

| Recurso compartilhado | Colegas |
|---|---|
| Tabela `configurations` + `category_configurations` | `../configuracoes-da-instituicao/`, `../lgpd/`, `../outras-configuracoes/` |
| `v6/app/Http/Controllers/Prefecture/ConfigurationController.php` | idem |
| `v6/app/Http/Requests/ConfigurationRequest.php` | idem |
| Model `App\Prefecture\Configuration` | idem |
| Rotas `edit.configuracoes.by.category` / `update.configurations.by.category` | idem |

A separação entre os quatro módulos é feita por **três eixos independentes**:

1. **Categoria** — `category_configurations.slug` (`site`, `temas`, `barra-privacidade`, `outros`).
2. **Recurso** — `resources.slug` (`configuracoes`, `tema-do-portal`, `template-do-portal`, `barra-privacidade`, `outros`) com `resources.modulo_id` apontando para `modulos.id`.
3. **Módulo** — `modulos.slug` (`configuracoes-da-instituicao`, `temas-e-cores`, `lgpd`, `outras-configuracoes`), que controla a **exibição** do item no menu do CMS.

O `case 'temas'` do controller é o **único** que mapeia a rota `temas` para o recurso
`tema-do-portal` (para `configuracoes` o mapeamento é para `site`; `outros` e
`barra-privacidade` usam o próprio slug).

### 1.1 A parte realmente própria do módulo

Tudo abaixo é exclusivo de `temas-e-cores` e **não tem relação com a tabela `configurations`**:

| Peça | Caminho |
|---|---|
| Tabela `layouts` | `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`) |
| Model `Layout` | `v6/app/Http/Model/Prefecture/Layout.php` |
| Controller | `v6/app/Http/Controllers/Prefecture/LayoutController.php` |
| Presenter | `v6/app/Http/Presenters/LayoutPresenter.php` |
| View | `v6/resources/views/prefecture/configurations/modelo5/edit.blade.php` |
| Rotas | `v6/routes/basepro.php:358-363` |

> **Não existe FK, chave estrangeira nem vínculo lógico entre `layouts` e `configurations`.**
> A ligação é apenas indireta: o usuário escolhe `modelo5` na chave `layout` de
> `configurations`, e a chave `layout` faz o portal renderizar as views do modelo 5, que por
> sua vez são controladas pela tabela `layouts`. Apagar a configuração `layout` **não**
> apaga os blocos, e vice-versa.

## 2. Achados verificados (aplicáveis a este módulo)

### 2.1 Composer `$modulos` ausente no CMS — menu "Temas & Cores" não aparece

`v6/app/Providers/ModulosServiceProvider.php` registra composers **apenas** para
`portal.layouts.modelo1..6.home.index`, `portal.layouts.modelo1..6.layout.sidebar.layout`,
`portal.layouts.modelo1..6.layout.sidebar.orgaosNews`, `prefecture.contents.noticias.form`,
`admin.layouts.dashboard` e `admin.layouts.dashboard-legacy`.

O item de menu em `v6/resources/views/vendor/adminlte/page.blade.php` usa
`$modulos['temas-e-cores']`:

```blade
@if (has_access(['tema-do-portal.index']) && $modulos['temas-e-cores'])
    <li>
        <a href="{{ route('edit.configuracoes.by.category', 'temas') }}">
            <h4 class="ml-0">
                <i class="fa  fa-paint-brush pr-2"></i>
                Temas & Cores
            </h4>
        </a>
    </li>
@endif
```

Verificado empiricamente no container: os eventos `composing: admin.layouts.app` e
`composing: adminlte::page` chegam **sem** a variável `modulos` (só `admin.layouts.dashboard`
e `admin.layouts.dashboard-legacy`, que estendem `admin.layouts.app`, a recebem). Ao registrar
manualmente um composer em `admin.layouts.app`, `adminlte::page` passa a receber `modulos`.

**Efeito:** em todas as telas do CMS **exceto** o dashboard, os itens do menu
"Configurações", "Temas & Cores", "LGPD" e "Outras Configurações" **não são exibidos** —
a tela de temas só é alcançável por URL direta (`/cms/configuracoes/editar/temas`).

### 2.2 Permissão assimétrica (leitura × gravação)

| Operação | Permissão exigida |
|---|---|
| Ler a tela (`editByCategory`) | `has_access(['tema-do-portal.edit'])` |
| Gravar (`updateByCategory`) | `has_access(['configuracoes.edit'])` — **a mesma para os 4 módulos** |

Um usuário com permissão apenas de leitura de `tema-do-portal` **não** consegue gravar; um
usuário com `configuracoes.edit` grava em qualquer categoria, inclusive `temas`.

### 2.3 Divergência entre menu e controller

O menu exige `tema-do-portal.index`; o controller exige `tema-do-portal.edit`. São recursos
diferentes no `resources` (`tema-do-portal` é o mesmo recurso, mas o verbo difere), e o
guard real da tela é o `edit`.

### 2.4 Sem FK para cores — texto livre

Não existe `theme_id`, `palette_id` nem qualquer coluna de referência para cores. As chaves
`cor_1`..`cor_4` são `varchar(191)`/`text` validadas apenas por `required|max:50|min:3`
(via `notnull`/`max_input`/`min_input` na própria linha de `configurations`). **Qualquer
string** de 3 a 50 caracteres é aceita (ex.: `red`, `rgb(1,2,3)`, `qualquer-texto`).

A lista `$temas_lte` do controller é o **único** lugar que define as skins válidas do CMS
(`blue`, `black`, `purple`, `green`, `red`, `yellow`, `blue-light`, `black-light`,
`green-light`, `red-light`, `yellow-light`) — e ela **não** é usada na validação, apenas
para montar o `<select>`.

### 2.5 `configurations.key` sem índice único

`configurations` tem apenas o índice da PK. `updateByCategory` usa
`Configuration::where('key',$key)->first()` — chaves duplicadas farão **apenas a primeira**
ser gravada. O seed usa `updateOrCreate(['key' => ...])`, que também silenciosamente
ignora duplicatas.

### 2.6 N+1 no Form Request

`ConfigurationRequest::rules()` e `ConfigurationRequest::messages()` executam **uma query por
campo enviado** na tabela `configurations`. A tela de temas envia ~7 campos → ~14 queries
por gravação, além das queries do controller.

### 2.7 View SQL `configuracoes` sem filtro por categoria

`DatatableController@get_configuracoes` (`v6/app/Http/Controllers/Prefecture/DatatableController.php:384`)
lê a view `configuracoes`, que expõe **todas** as configurações de **todas** as categorias
(87 ativas). O datatable do CMS lista, portanto, também as 9 chaves de `temas`.

### 2.8 `echo` dentro do controller

`updateByCategory` executa `echo "JSON file created successfully..."` (≈ linhas 263-265)
quando a categoria é `site`. No container `output_buffering => 0`, o texto é emitido antes do
redirecionamento (cabeçalhos já enviados). **Não afeta a categoria `temas`**, mas o endpoint
é o mesmo para as 4 categorias.

### 2.9 `config.json` obrigatório para o boot

`v6/config/app.php:6`, `v6/config/mail.php:3` e `v6/config/recaptcha.php:3` leem
`base_path()."/config.json"` e acessam `$data->config[0..2]`. Quem reescreve esse arquivo é a
tela de configurações da instituição (`../configuracoes-da-instituicao/`), **não** a de temas —
mas uma configuração inválida de e-mail/recaptcha derruba a aplicação inteira.

## 3. Bugs e fragilidades específicos deste módulo

| # | Achado | Evidência | Impacto |
|---|---|---|---|
| B1 | Rotas do modelo 5 sem `check.logged` e sem `has_access()` no controller | `v6/routes/basepro.php:358-363` | Qualquer visitante pode chamar `save.layout`, `orderLayout`, `uploadFile`, `downloadFile`, `edit.layout` e `update-module-status`. |
| B2 | `layout` só é validado se for o primeiro campo enviado | `ConfigurationController@updateByCategory` | POST com `cor_1` antes de `layout` grava template inválido → 404 no portal. |
| B3 | `cor_cms` não é validado contra `$temas_lte` | `ConfigurationRequest` | Valor inexistente → `<link>` para CSS 404 → AdminLTE sem estilo. |
| B4 | `->first()->value` sem guarda em 4 arquivos | `head.blade.php`, `default/master.blade.php`, `vendor/adminlte/page.blade.php`, `Admin/Controller.php:39` | Falha fatal (`Attempt to read property on null`) se a chave não existir. |
| B5 | Checkboxes de `background` renderizados dentro do bloco de `background` | `form.blade.php` | Se `background` não vier na coleção, `background_fixed` e `background_repeat` não aparecem. |
| B6 | `background` ignora o redimensionamento | `if ($key != 'imagem-acesso-rapido' && $key !== 'background')` | Upload de imagem muito pesada é armazenada em tamanho original. |
| B7 | Duplo `save()` do arquivo de upload | `$image->save(storage_path(...)); $image->save($directory . '/' . $name);` | Mesmo caminho gravado duas vezes (custo de CPU). |
| B8 | `$fillable` do model `Layout` com typo e incompleto | `v6/app/Http/Model/Prefecture/Layout.php` (`'kay'`, sem `title`/`style`/`columns`) | `fill()`/`create()` perderiam esses campos (hoje contornado pelo uso de `update()`). |
| B9 | `Layout` sem `protected $table` | idem | Tabela inferida do nome da classe — funciona, mas implícito. |
| B10 | `secretarias-grid` inexistente no banco de referência | `LayoutController@updateModuleStatus` | Criado sob demanda; `edit()` cai no default `'transparencia'`. |
| B11 | Validação de `cor_cms` ausente no `FormRequest` e dependência do array do controller | idem | Ver B3. |
| B12 | `has_access('modelo-personalizavel.*')` — string com wildcard | `select_layout.blade.php` | `has_access()` com string chama `hasAccess([$access])`; o curinga funciona por convenção do Sentinel, mas não é validado em lugar nenhum. |
| B13 | `cor_cms` consultado 3× por render do AdminLTE | `vendor/adminlte/page.blade.php:5,58,66` | 3 queries por página do CMS. |
| B14 | `head.blade.php` e `default/master.blade.php` fazem 4 queries por página | idem | 8 queries por página do portal para as cores. |
| B15 | Bloco de `<body>` com plano de fundo duplicado em 6 templates | `modelo{1..6}/layout/layout.blade.php` | Correção precisa ser replicada 6×. |
| B16 | `url('/storage/{{ ... }}')` com barra fixa | idem | Quebra se o valor persistido vier com barra inicial. |

## 4. Funcionalidades incompletas ou mortas

| Item | Situação |
|---|---|
| `v6/app/Http/Views/Composers/ConfigurationsComposer.php` | **Código morto**: não é registrado em nenhum provider (grep em `v6/app/Providers/` não retorna nada). |
| `cache('modulos')` | `ModulosServiceProvider` usa `Cache::rememberForever('modulos', ...)`, mas o composer que o consumiria (`admin.layouts.app`) não é registrado — ver 2.1. |
| `category_configurations` slug `layout` (id 5) | Categoria existe e está **vazia** (0 configurações) — confundível com o template do portal. |
| Coluna `placeholder` de `configurations` | Nunca usada pela view de temas. |
| `description` de `background` | Vazia; a instrução real está em `small_description`. |
| `notnull` de `cor_cms` | `true`, mas sem `max_input`/`min_input` — a validação gera apenas `required`. |

## 5. Itens removidos na v6 (em relação ao v5) que afetam este módulo

| Item | Observação |
|---|---|
| `v5/database/seeds/PrivacyBarSeeder.php` | Removido na v6 (as 4 chaves da barra de privacidade migraram para `ConfigurationsTableSeeder`) — pertence a `../lgpd/`. |
| Validação "É obrigatório selecionar uma posição para todos os selos" | Removida na v6 (selos) — pertence a `../configuracoes-da-instituicao/`. |
| Validação de `quantidade_noticias_ultimas_noticias` (número par entre 2 e 20) | Removida na v6 — **não** afeta o módulo de temas (pertence a conteúdos). |

Nada foi removido da v5 **no escopo do módulo de temas**: as 9 chaves, a grade de templates,
a lista de skins e o modelo 5 continuam presentes.

## 6. Mapa v5 → v6 (módulo `temas-e-cores`)

| Artefato | v5 | v6 | Natureza da mudança |
|---|---|---|---|
| `case 'temas'` do `ConfigurationController` | existe | existe | **Idêntico** entre as duas versões (mesmo `$temas_lte` com 11 skins e mesmo `$templatesPortal` com 6 modelos). |
| `v6/resources/views/prefecture/configurations/temas/select_layout.blade.php` | existe | existe | **Divergente**: a v6 reindentou o bloco e moveu o `@if (has_access(['layouts']))` para o nível do `@include` (abrangendo o `<div id="cp2">` e o `@foreach`); a v5 mantinha o `has_access` fora. Comportamento observável equivalente. |
| `v6/resources/views/prefecture/configurations/temas/form.blade.php` | existe | existe | **Idêntico**. |
| `v6/resources/views/prefecture/configurations/temas/edit.blade.php` | existe | existe | **Idêntico**. |
| `v6/resources/views/prefecture/configurations/temas/modal.blade.php` | existe | existe | **Idêntico**. |
| `v6/app/Http/Controllers/Prefecture/LayoutController.php` | existe | existe | **Divergente** (v6 é superconjunto). |
| `v6/app/Http/Model/Prefecture/Layout.php` | existe | existe | A v6 adiciona `columns` ao `get()` do controller e ao tratamento de `input-columns*`; o model continua sem `title`/`style`/`columns` no `$fillable`. |
| `v6/database/migrations/2024_10_20_070912_add_title_to_layout_table.php` | — | **novo** | Coluna `title` para títulos editáveis dos blocos. |
| `v6/database/migrations/2025_04_27_225303_add_column_columns_to_layout_table.php` | — | **novo** | Coluna `columns` para o nº de colunas dos grids. |
| `v6/app/Http/Controllers/Prefecture/LayoutController.php::updateModuleStatus()` | ausente | **novo** | `updateOrInsert` em `layouts` com `key='secretarias-grid'`, `value="{module}:{subId}"`. |
| `legacyMap` (`0..3` → `transparencia`/`secretaria`/`menus`) | ausente | **novo** | Compatibilidade com valores antigos de `secretarias-grid`. |
| `input-title-carousel-noticias` | ausente | **novo** | Edição do título do carrossel de notícias. |
| `input-columnsTransparenciaFiscal*` | ausente | **novo** | Edição do nº de colunas da transparência fiscal. |
| `status = 0` para o grupo `tags` | `status = 1` fixo | alterado | O grupo `tags` passou a ser gravado desabilitado. |
| `v6/app/Http/Controllers/Prefecture/LayoutController.php::edit()` | carregava apenas `configStyle`/`configLayout` | carrega também `activeModule`, `activeSubId`, `transparenciaCategories`, `menusDisponiveis` | Leitura de `category_contents` (`transparencia-fiscal`) e de `menus`. |

## 7. Riscos de dados (migração de schema)

| Risco | Detalhe |
|---|---|
| `configurations` compartilhada | Migrar as 9 chaves de `temas` para uma tabela própria exige preencher `themes` a partir de `configurations` + `category_configurations` (id 3), respeitando `order` (46-52, 68, 69) e `notnull`/`max_input`/`min_input`. |
| Valores de `cor_cms` fora da lista de skins | Aceitar hoje, rejeitar com enum na v7 → normalizar antes. |
| Cores em formato não-hex | `min:3`/`max:50` permitem `red` ou `rgb(1,2,3)` → normalizar com um value object. |
| `layouts.value` com ~77 KB | Exige `longText`/`text` (não `varchar`). |
| `layouts` sem `updated_at` sem preenchimento | `created_at`/`updated_at` são `timestamp` livres; usar `softDeletes`? Não existe coluna `deleted_at` — blocos removidos são deletados fisicamente ou marcados com `status = false`. |
| `secretarias-grid` ausente em algumas bases | A migração deve criar a linha com `value = 'transparencia'`. |
| `configurations.key` sem índice único | Ao criar `themes`, criar `UNIQUE (slug)` e, se mantida a tabela antiga, um índice único em `configurations.key` (após detectar e limpar duplicatas). |

## 8. Perguntas em aberto para o negócio

1. As 11 skins do CMS (`$temas_lte`) ainda são todas suportadas pelo AdminLTE 1.22 em uso?
   A lista de skins de `v6/public/assets/vendor/adminlte/dist/css/skins/` é a fonte da verdade?
2. `cor_4` ("Cor de fundos de texto") é usada como **cor de fundo de datas** em listagens — há
   exigência de contraste (WCAG) para essa cor? Hoje não há validação nem aviso.
3. O plano de fundo tem limite de tamanho/tipo? A seed sugere 1920×1080, mas nada é validado.
4. O Modelo 5 é usado por todos os municípios ou é um recurso de nicho? O custo de mantê-lo
   (tabela + editor + CSS customizado) é proporcional?
5. A troca de template deve preservar a home de cada município? A troca atual é global
   (uma configuração por instância) — é isso intencional?
6. Deve ser possível ter mais de um tema salvo e alternar entre eles (com histórico)? Hoje há
   apenas um conjunto de chaves.

## 9. Referências cruzadas

| Dossiê | Relação |
|---|---|
| `../configuracoes-da-instituicao/` | Divide `configurations`, `ConfigurationController`, `ConfigurationRequest` e o model `Configuration`. |
| `../lgpd/` | Divide `configurations`, `ConfigurationController`, `ConfigurationRequest` e o model `Configuration`. |
| `../outras-configuracoes/` | Divide `configurations`, `ConfigurationController`, `ConfigurationRequest` e o model `Configuration`. |
