# Acesso Rápido — Migração para Laravel Moderno (PHP 8.x / Laravel 11-12)

## 1. Objetivo da reconstrução

Substituir o reaproveitamento genérico de `contents` + `links` por um **modelo
dedicado** (`quick_links`), com API REST, Policy, cache e componentes Blade.
O comportamento observável (quais atalhos aparecem, em que ordem, para onde vão)
deve ser **preservado exatamente**.

## 2. Schema de migração equivalente

```php
// database/migrations/2026_XX_XX_create_quick_links_table.php
Schema::create('quick_links', function (Blueprint $table) {
    $table->id();
    $table->string('title', 25);          // limite herdado do ContentRequest legado
    $table->string('description', 50);    // idem
    $table->string('icon', 120)->default('fas fa-edit');
    $table->string('url', 2999);          // varchar(3000) na tabela links legado
    $table->boolean('is_blank')->default(false);   // abre em nova aba
    $table->boolean('is_active')->default(true);    // contents.status legado
    $table->unsignedInteger('position')->default(1);// contents.order legado
    $table->timestamps();
    $table->softDeletes();

    $table->index(['is_active', 'position']);
});
```

### 2.1 Backfill a partir do legado

```php
DB::statement("
    INSERT INTO quick_links (title, description, icon, url, is_blank, is_active, position, created_at, updated_at)
    SELECT c.title,
           c.text,
           c.icon,
           COALESCE(l.link, ''),
           COALESCE(l.isBlank, false),
           COALESCE(c.status, false),
           COALESCE(c.order, 1),
           c.created_at,
           c.updated_at
    FROM contents c
    JOIN category_contents cc ON cc.id = c.category_contents_id
    LEFT JOIN links l ON l.contents_id = c.id
    WHERE cc.slug = 'acesso-rapido' AND c.deleted_at IS NULL
    ORDER BY c.order ASC
");
```

> Cuidado: `links` é 1-1 com `contents` no legado (`hasOne`), mas o schema permite
> vários links por `contents_id`. O backfill deve escolher o mais recente
> (`ORDER BY l.created_at DESC LIMIT 1`) para não duplicar linhas.

### 2.2 Dados que **não** devem ser migrados

- `contents.clicks` — os atalhos nunca incrementaram contador.
- `contents.slug` — sempre nulo nos atalhos seedados.
- `contents.category_contents_id` — a categoria `acesso-rapido` (id 45) pode ser
  removida se nenhum outro módulo a usar (verificar `perguntas-frequentes` = id 12,
  que é categoria distinta).

## 3. Modelos

```php
// app/Models/QuickLink.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class QuickLink extends Model
{
    use SoftDeletes;

    protected $fillable = ['title', 'description', 'icon', 'url', 'is_blank', 'is_active', 'position'];

    protected $casts = [
        'is_blank'  => 'boolean',
        'is_active' => 'boolean',
        'position'  => 'integer',
    ];

    public function scopeActive($query)
    {
        return $query->where('is_active', true);
    }

    public function scopeOrdered($query)
    {
        return $query->orderBy('position');
    }
}
```

Regras de equivalência funcional:

| Legado | Moderno | Observação |
|---|---|---|
| `contents.status` | `is_active` | `where('status', true)` no composer legado. |
| `contents.order` | `position` | `order = índice + 1` no reorder legado. |
| `contents.text` | `description` | Renderizado com `{!! !!}` no legado; ver item 6. |
| `links.link` | `url` | `varchar(3000)` -> `string(2999)` (limite do `ContentRequest`). |
| `links.isBlank` | `is_blank` | `boolean`. |
| `contents.icon` | `icon` | Classe CSS de Font Awesome. |

## 4. Form Requests

```php
// app/Http/Requests/StoreQuickLinkRequest.php
class StoreQuickLinkRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('create', QuickLink::class);
    }

    public function rules(): array
    {
        return [
            'title'       => ['required', 'string', 'max:25'],
            'description' => ['required', 'string', 'max:50'],
            'icon'        => ['required', 'string', 'max:120'],
            'url'         => ['required', 'string', 'max:2999', 'url'],
            'is_blank'    => ['boolean'],
            'is_active'   => ['boolean'],
        ];
    }
}
```

> A regra `url` do legado era apenas `required|max:2999` (aceita `/licitacoes`,
> que é um path relativo). Se a validação `url` for adotada, é preciso
> `url:http,https` **ou** aceitar paths relativos — decisão de equivalência:
> **manter apenas `max:2999`**, para não quebrar os links internos relative.

## 5. Policies / autorização

O legado usa `has_access(['acesso-rapido.create'])` e o middleware
`check.route.permission`, que resolve a permissão pelo nome da rota. Traduzir para
Policy:

```php
// app/Policies/QuickLinkPolicy.php
class QuickLinkPolicy
{
    public function viewAny(User $user): bool   { return $user->can('acesso-rapido.index'); }
    public function create(User $user): bool    { return $user->can('acesso-rapido.create'); }
    public function update(User $user, QuickLink $q): bool { return $user->can('acesso-rapido.edit'); }
    public function delete(User $user, QuickLink $q): bool { return $user->can('acesso-rapido.delete'); }
}
```

Mapeamento das permissões legadas (tabela `resources` id 69 + `permissions`):

| Permissão legada | Uso no legado |
|---|---|
| `acesso-rapido.index` | acesso à listagem do CMS |
| `acesso-rapido.create` | botão "Cadastrar Acesso Rápido" |
| `acesso-rapido.edit` | botão de editar na listagem |
| `acesso-rapido.delete` | botão de excluir na listagem |
| `acesso-rapido.inactivate` / `.restore` | botão liga/desliga (genérico do `struct_views`) |
| `acesso-rapido.inactive` / `.show` | declaradas no seed de papéis, sem uso no código |

> O `super-admin` legado é quem **passa** por todas as checagens. Manter um papel
> equivalente (`Gate::before` para `super-admin`) é obrigatório para não perder
> acesso.

## 6. Endpoints REST/JSON sugeridos

| Método | URI | Descrição | Equivalente legado |
|---|---|---|---|
| GET | `/api/quick-links` | lista pública (ativos, ordenados) | composer `$acessos` |
| GET | `/api/admin/quick-links` | listagem paginada para o CMS | `/cms/datatable/acesso_rapido` |
| POST | `/api/admin/quick-links` | cria atalho | `POST /cms/conteudo` |
| PUT | `/api/admin/quick-links/{id}` | atualiza | `PUT /cms/conteudo/{id}` |
| DELETE | `/api/admin/quick-links/{id}` | remove | `DELETE /cms/conteudo/delete/{id}` |
| PUT | `/api/admin/quick-links/reorder` | persiste nova ordem | `POST /cms/ajax/ajust-banners` |

Recurso de API:

```php
// app/Http/Resources/QuickLinkResource.php
return [
    'id'          => $this->id,
    'title'       => $this->title,
    'description' => $this->description,
    'icon'        => $this->icon,
    'url'         => $this->url,
    'is_blank'    => (bool) $this->is_blank,
    'is_active'   => (bool) $this->is_active,
    'position'    => (int) $this->position,
];
```

## 7. Rotas públicas (compatibilidade)

As URLs abaixo **não podem mudar** (estão em links externos, menus e indexação):

| Legado | Novo |
|---|---|
| `GET /acesso-rapido/{slug}` | `GET /acesso-rapido/{slug}` (mesma URI) |
| `GET /acessos-rapido/{slug}` | `GET /acessos-rapido/{slug}` (mesma URI) |
| `GET /cms/conteudos/acesso-rapido` | `GET /cms/acesso-rapido` (pode mudar: é URL interna de CMS) |

Sugestão de implementação com uma **única** rota por trás de um controller
dedicado, removendo a ambiguidade atual entre singular/plural:

```php
Route::get('/acesso-rapido/{slug}', QuickAccessController::show)->name('acesso-rapido.show');
Route::get('/acessos-rapido/{slug}', QuickAccessController::staticPage)->name('acessos-rapido.static');
```

> Manter os dois prefixes evita quebrar links externos. O que **deve** ser
> eliminada é o `Route::any` (usar `Route::get`).

## 8. Views Blade modernas

Componente único, reutilizado por todos os layouts:

```blade
{{-- resources/views/components/portal/quick-links.blade.php --}}
@props(['links' => null])

@if (($links ?? $quickLinks)->isNotEmpty())
    <section class="row quick-access">
        @foreach ($links ?? $quickLinks as $link)
            <div class="col-sm-12 col-md-4">
                <a href="{{ $link->url }}" @if ($link->is_blank) target="_blank" rel="noopener" @endif
                   class="quick-access-card">
                    <i class="{{ $link->icon }}"></i>
                    <h3 class="card-title">{{ $link->title }}</h3>
                    <p class="card-description">{{ $link->description }}</p>
                    <span class="btn-veja-mais">VEJA MAIS</span>
                </a>
            </div>
        @endforeach
    </section>
@endif
```

Elimina a replicação em 6 layouts. O CSS (`.quick-access-card`, hover) deve ir para
um stylesheet único, fora do Blade.

### 8.1 Substituição do view composer

O `ConfigHeaderServiceProvider` hoje injeta `$acessos` em 6 views. Substituir por
um **View Composer** dedicado ou, melhor, por um dado de view
(`View::composer('*', function ($view) { return $view->with('acessos', $consulta); })`) ou por um `<x-portal.quick-links />` que resolve os
dados por conta própria (com cache).

## 9. Fila, cache, storage

| Aspecto | Legado | Moderno |
|---|---|---|
| Cache | `Cache::rememberForever('modulos')` para o status do módulo | `Cache::remember('quick_links.public', 300, fn () => QuickLink::query()->orderBy('position')->get())` para a lista pública; invalidar no `save`/`delete` do model via `Cache::forget`. |
| Fila | nenhuma | dispensável; a reordenação é uma única `UPDATE` transacional. |
| Storage | — | apenas se forem anexados; o módulo legado não anexa arquivos aos atalhos (os anexos são de `static_pages`). |
| Reorder | N+1 no `reorder()` legado (`find` + `update` por item) | uma única query com `CASE WHEN id = <id> THEN <position>` por item, ou `upsert` em lote. |

## 10. Riscos e decisões de equivalência funcional

| # | Decisão | Justificativa |
|---|---|---|
| D1 | **Manter** as URIs `/acesso-rapido/*` e `/acessos-rapido/*`. | Links externos, menus e materiais impressos apontam para elas. |
| D2 | **Manter** o redirecionamento `/acesso-rapido/ouvidoria` -> e-SIC. | Comportamento observável já publicado. |
| D3 | **Manter** o limite `title <= 25` e `description <= 50`. | O layout dos cards foi desenhado para esses limites; enlargar quebra o design. |
| D4 | **Manter** a ordem 1-based (`position` começa em 1). | O CMS legado grava `order = índice + 1`; a convenção é observável no banco. |
| D5 | **Manter** a semântica de `is_active` (itens desativados somem do portal mas continuam no CMS). | Permite "publicar" um atalho sem excluí-lo. |
| D6 | **Não** criar `url` como `string` validada por `url:`. | Os links internos são paths relativas (`/licitacoes`). |
| D7 | Substituir o `contents.text` (HTML Summernote) por texto puro ou sanitizar. | Riscos de XSS; se a descrição precisar de HTML, sanitizar com `strip_tags` allowlist no `FormRequest` e nunca usar `{!! !!}`. |
| D8 | **Manter** a semântica "sem conteúdo => página estática" (fallback). | URL `/acesso-rapido/manual-de-navegacao` precisa funcionar sem conteúdo na categoria. |
| D9 | **Remover** a dependência do diretório de views por layout. | A reconstrução deve usar componentes; se o portal mantiver 6 layouts, o componente único é obrigatório. |
| D10 | **Preservar** a checagem de "não encontrado" **antes** de usar a variável (corrigir o bug A1 do legado). | Um slug inexistente deve devolver 404, não um erro fatal 500. |

## 11. Checklist de migração

1. Criar `quick_links` + backfill.
2. Model `QuickLink` com casts e scopes.
3. Policy + `Gate::before` para `super-admin`.
4. API Resource + endpoints `/api/quick-links` e `/api/admin/quick-links`.
5. Componente Blade `<x-portal.quick-links />` + stylesheet único.
6. Rotas `GET /acesso-rapido/{slug}` e `GET /acessos-rapido/{slug}` (sem `any`).
7. Remover o bloco de `$acessos` do `ConfigHeaderServiceProvider`.
8. Apagar a view SQL `acesso_rapido` e o método `get_acesso_rapido()` do
   `DatatableController` após a migração da listagem do CMS.
9. Converter a tela do CMS em um CRUD próprio (hoje reaproveita `contents`).
10. Testes de equivalência para as URLs públicas e para a ordem dos atalhos.
