# Temas e Cores — Plano de Migração para Laravel Moderno

> **Slug do módulo:** `temas-e-cores` (tabela `modulos`, id 2)
> **Alvo:** PHP 8.x + Laravel 11/12

## 1. Decisões de equivalência funcional (o que NÃO pode mudar)

1. A chave `layout` continua sendo **uma** das 6 opções `modelo1`..`modelo6`; o mesmo valor
   deve continuar selecionável e gravável.
2. A troca de template deve continuar alterando a home, a quantidade de notícias e as classes
   CSS dos cards — a lista `getNewsQuantity()` é comportamento observável.
3. As 4 cores continuam sendo aplicadas como `--cor-1..--cor-4` no `<head>` e em regras
   `background`/`color` do portal, e `cor_1`/`cor_2` continuam alimentando PDF e Swagger.
4. `cor_cms` continua elegendo uma skin do AdminLTE (ou seu equivalente) e continue trocando o
   logo do CMS conforme o valor.
5. O modelo 5 continua sendo customizável **por município** (ordem, visibilidade, título,
   colunas, CSS e HTML de preview/tags).
6. O endpoint público que devolve as cores deve continuar respondendo as 4 chaves.

## 2. Schema de migração Laravel equivalente

### 2.1 Nova tabela `themes` (substitui a categoria `temas` de `configurations`)

```php
// exemplo ilustrativo
Schema::create('themes', function (Blueprint $table) {
    $table->id();
    $table->string('slug')->unique();                 // 'default'
    $table->string('layout')->default('modelo1');      // modelo1..modelo6
    $table->string('primary_color')->default('#1075ad');
    $table->string('title_color')->default('#66b367');
    $table->string('button_color')->default('#fece50');
    $table->string('text_background_color')->default('#fece50');
    $table->string('cms_skin')->default('black');
    $table->string('background_path')->nullable();
    $table->boolean('background_fixed')->default(false);
    $table->boolean('background_repeat')->default(false);
    $table->timestamps();
});
```

Decisão: **não** reaproveitar a tabela `configurations`. O módulo ganha identidade própria,
o que elimina a dependência da permissão `configuracoes.edit` e do `ConfigurationRequest`
genérico. Se a transição exigir manter a tabela antiga, criar uma **view** `themes` sobre
`configurations` filtrada por `category_configurations_id = 3` e migrar os dados com um
comando `php artisan themes:migrate-from-configurations`.

### 2.2 Tabela `layouts` — migrada sem alteração de semântica, com ajustes

```php
// exemplo ilustrativo
Schema::create('layout_blocks', function (Blueprint $table) {
    $table->id();
    $table->string('key')->index();
    $table->string('section')->nullable()->index();
    $table->string('type');                            // section|element|config
    $table->longText('value')->nullable();
    $table->unsignedInteger('order')->default(0);
    $table->longText('style')->nullable();
    $table->boolean('status')->default(true);
    $table->string('path')->nullable();
    $table->string('title')->nullable();
    $table->unsignedTinyInteger('columns')->nullable();
    $table->timestamps();
});
```

- `value` vira `longText` (o HTML de `preview`/`tags` tem ~77 KB).
- `key` ganha índice (o `where` por `key` é o acesso quente).
- Coluna `order` renomeada para `position` (o Laravel usa `order` como nome de coluna de
  agregação; `position` evita ambiguidade).
- Sugestão: `jsonb` para `value` no caso de `secretarias-grid` (`"{module}:{subId}"` →
  `{"module": "...", "sub_id": ...}`).

## 3. Modelos

```php
// exemplo ilustrativo
class Theme extends Model
{
    protected $fillable = [
        'slug', 'layout', 'primary_color', 'title_color',
        'button_color', 'text_background_color', 'cms_skin',
        'background_path', 'background_fixed', 'background_repeat',
    ];

    protected $casts = [
        'background_fixed'  => 'boolean',
        'background_repeat' => 'boolean',
    ];
}
```

| Classe | Papel |
|---|---|
| `App\Models\Theme` | Registro único (ou por município) com o tema ativo. |
| `App\Models\LayoutBlock` | Blocos do modelo 5 (substitui `App\Http\Model\Prefecture\Layout`). |
| `App\Enums\CmsSkin` | Cases `blue`, `black`, `purple`, `green`, `red`, `yellow`, `blue-light`, `black-light`, `green-light`, `red-light`, `yellow-light`. |
| `App\Enums\PortalLayout` | Cases `modelo1`..`modelo6`. |
| `App\ValueObjects\HexColor` | Validação e normalização de `#rgb`/`#rrggbb`. |
| `App\Services\ThemeResolver` | Ponto único que devolve o tema ativo (com cache). |
| `App\Services\LayoutBlockRepository` | Leitura/escrita dos blocos do modelo 5. |

`PortalLayout` pode absorver a lógica de `getNewsQuantity()`:

```php
// exemplo ilustrativo
enum PortalLayout: string
{
    case Modelo1 = 'modelo1';
    case Modelo2 = 'modelo2';
    case Modelo3 = 'modelo3';
    case Modelo4 = 'modelo4';
    case Modelo5 = 'modelo5';
    case Modelo6 = 'modelo6';

    public function newsQuantity(): int
    {
        return match ($this) {
            self::Modelo1 => 6,
            self::Modelo2 => 7,
            self::Modelo3 => 10,
            self::Modelo4 => 3,
            self::Modelo5 => 8,
            self::Modelo6 => 4,
        };
    }
}
```

## 4. Form Requests

| Request | Campos | Regras |
|---|---|---|
| `UpdateThemeRequest` | `layout` | `required`, `Rule::enum(PortalLayout::class)` |
| | `primary_color`, `title_color`, `button_color`, `text_background_color` | `required`, `max:50`, regex de cor, coerção para minúsculo |
| | `cms_skin` | `required`, `Rule::enum(CmsSkin::class)` |
| | `background` | `nullable`, `image`, `mimes:jpg,jpeg,png,webp`, `max:4096` |
| | `background_fixed`, `background_repeat` | `boolean` |
| `SaveLayoutRequest` | `html`, `htmlTags` | `nullable`, `string`, `max:200000` |
| | `input-*` | `nullable`, `string`, `max:191` |
| | `modules` / `order` | `array` |

Diferença em relação ao legado: o legado validava `required|max:50|min:3` para as cores e
**nada** para `cor_cms` nem para o formato da cor. A nova versão valida o enum de skins e o
formato hexadecimal — ganho de robustez, mas **exige plano de migração dos dados existentes**
(valeores de `cor_cms` fora da lista precisam ser normalizados).

## 5. Policies / autorização

```php
// exemplo ilustrativo
class ThemePolicy
{
    public function viewAny(User $user): bool   { return $user->hasPermissionTo('tema-do-portal.index'); }
    public function update(User $user): bool    { return $user->hasPermissionTo('tema-do-portal.edit'); }
    public function editLayout(User $user): bool { return $user->hasPermissionTo('modelo-personalizavel.edit'); }
}
```

Correção do defeito do legado: **leitura e gravação usam a mesma permissão**
(`tema-do-portal.edit`), e as rotas do modelo 5 passam a exigir `modelo-personalizavel.edit`
(check explícito no controller ou via `can:`).

## 6. Endpoints REST/JSON sugeridos

| Método | URI | Descrição |
|---|---|---|
| GET | `/api/v1/theme` | Devolve o tema ativo (cores, skin, layout, caminho do fundo). Substitui `GET /getColors` (manter por compatibilidade). |
| GET | `/api/v1/themes/templates` | Lista os 6 templates com miniatura e URL de pré-visualização. |
| GET | `/api/v1/themes/cms-skins` | Lista as skins do CMS (substitui o array no controller). |
| PUT | `/api/v1/theme` | Atualiza o tema (validação via `UpdateThemeRequest`). |
| GET | `/api/v1/layout-blocks` | Blocos do modelo 5. |
| PUT | `/api/v1/layout-blocks/order` | Reordena (substitui `order/modelo-personalizavel`). |
| PUT | `/api/v1/layout-blocks/{key}` | Atualiza `style`/`title`/`columns` de um bloco. |
| POST | `/api/v1/layout-blocks/module-status` | Define o módulo ativo do grid (substitui `update-module-status`). |
| POST | `/api/v1/theme/background` | Upload do plano de fundo. |

Recursos de API sugeridos:

```php
// exemplo ilustrativo
class ThemeResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'layout'       => $this->layout,
            'colors'       => [
                'primary'            => $this->primary_color,
                'title'              => $this->title_color,
                'button'             => $this->button_color,
                'text_background'    => $this->text_background_color,
            ],
            'cms_skin'     => $this->cms_skin,
            'background'   => $this->background_path
                ? Storage::disk('public')->url($this->background_path)
                : null,
            'background_fixed'  => (bool) $this->background_fixed,
            'background_repeat' => (bool) $this->background_repeat,
        ];
    }
}
```

## 7. Views / componentes Blade

| Antes | Depois |
|---|---|
| `prefecture/configurations/temas/edit.blade.php` | `resources/views/admin/themes/edit.blade.php` (com `<x-admin.card>`, `<x-color-picker>` e `<x-template-picker>`). |
| `prefecture/configurations/temas/form.blade.php` | `resources/views/admin/themes/partials/form.blade.php` |
| `prefecture/configurations/temas/select_layout.blade.php` | Componente `<x-template-picker :templates="$templates" :selected="$theme->layout" />` (o CSS inline da v6 vira um arquivo SCSS). |
| `prefecture/configurations/modelo5/edit.blade.php` | `resources/views/admin/themes/layout-editor.blade.php` |
| Bloco `<body>` duplicado nos 6 templates | Partial único `portal::partials.background` incluido por `portal.layouts.*.layout.layout`. |
| `head.blade.php` com 4 queries | Componente `<x-portal.theme-variables />` que usa o `ThemeResolver` cacheado. |

## 8. Fila, cache e storage

| Item | Proposta |
|---|---|
| Cache do tema ativo | `Cache::remember('theme.active', 300, ...)` com invalidação no `saved` do model `Theme` (tag por município). |
| Cache do template ativo | Resolver dentro do mesmo cache; o portal chama `ThemeResolver::active()` uma vez por requisição e injeta via `@inject` ou view composer. |
| Cache dos blocos do modelo 5 | `Cache::remember('layout.blocks', ...)` invalidado em `save_layout`/`orderLayout`. |
| Armazenamento do fundo | Disco `public` (ou S3) com `Image` do Intervention; nome UUID + extensão; redimensionar para no máximo 1920×1080 (o `small_description` da seed já pede 1920x1080). |
| Fila | Upload de imagem e limpeza de arquivos órfãos podem ir para uma job `ProcessThemeBackground`. |

## 9. Riscos

| Risco | Mitigação |
|---|---|
| Valores de `cor_cms` fora da lista atual (dados legados) | Regra de enum rejeitaria; normalizar os dados antes de ativar a validação. |
| Cores fora do formato hex (ex.: `rgb(...)`, nome CSS) | Aceitar no formato antigo e converter com `HexColor::normalize()` durante a migração de dados. |
| Template gravado sem view correspondente | Manter o `abort(404)` + `pagina_em_manutencao = 1` como alerta, mas trocar por página de "em manutenção" renderizada (o portal não deve cair). |
| Migration de `layouts` com `value` de ~77 KB | `longText` + `MEDIUMTEXT` equivalente; evitar `TEXT` com limite de 255 bytes em MySQL — o legado já usa PostgreSQL. |
| Chave `layout` compartilhada por outros módulos | Ao extrair `themes`, garantir que o `ThemeResolver` seja o único consumidor e remover as leituras diretas de `Configuration::where('key','layout')`. |
| Comportamento de `status = 0` no grupo `tags` | Preservar exatamente: o grupo `tags` é sempre gravado como desabilitado. |
