# Destaques — Migração para Laravel Moderno (v7)

## 1. Schema de migração

```php
// database/migrations/xxxx_xx_xx_create_highlights_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('highlights', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('subtitle');
            $table->string('text_button');
            $table->string('url', 2999);
            $table->unsignedInteger('position');
            $table->boolean('status')->nullable()->default(false);
            $table->boolean('status_button')->nullable()->default(false);
            $table->unsignedTinyInteger('layout')->nullable()->default(2);
            $table->boolean('is_blank')->nullable()->default(false);
            $table->timestamps();
        });

        Schema::table('archives', function (Blueprint $table) {
            $table->foreignId('highlight_id')
                ->nullable()
                ->constrained('highlights')
                ->cascadeOnDelete();
        });
    }

    public function down(): void
    {
        Schema::table('archives', function (Blueprint $table) {
            $table->dropConstrainedForeignId('highlight_id');
        });
        Schema::dropIfExists('highlights');
    }
};
```

**Decisões de equivalência**

| Mudança | Motivo |
|---|---|
| `url` com 2999 de comprimento | O request legado já validava `max:2999` |
| `layout` como `unsignedTinyInteger` | Elimina a comparação `varchar = int` (armadilha A5) |
| `status` default `false` em vez de `null` | Elimina o "ternário para null" do `toggleStatus` |
| `position` com índice único | Garante a unicidade que o legado só garantia por convenção |

> **Atenção**: mudar `status` de `null` para `false` altera a semântica da coluna. Se
> houver dados legados, use `UPDATE highlights SET status = false WHERE status IS NULL`
> antes de aplicar o `default`.

## 2. Modelos

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

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;

class Highlight extends Model
{
    use HasFactory;

    protected $fillable = [
        'title', 'subtitle', 'text_button', 'url', 'status',
        'is_blank', 'position', 'layout', 'status_button',
    ];

    protected $casts = [
        'status'        => 'boolean',
        'status_button' => 'boolean',
        'is_blank'      => 'boolean',
        'position'      => 'integer',
        'layout'        => 'integer',
    ];

    public function archive(): HasOne
    {
        return $this->hasOne(Archive::class, 'highlight_id');
    }

    public function scopePublished($query)
    {
        return $query->where('status', true)->orderBy('position');
    }
}
```

Sugestão de **enum** para o layout:

```php
enum HighlightLayout: int
{
    case FullWidth = 1;   // "Largura da página"
    case Centered  = 2;   // "Centralizado"
}
```

## 3. Reordenação

O rebalanceamento legado (blocos `increment`/`decrement`) deve virar uma única
operação transacional:

```php
// app/Services/HighlightPositionService.php
public function move(Highlight $highlight, int $newPosition): void
{
    DB::transaction(function () use ($highlight, $newPosition) {
        $old = $highlight->position;

        if ($old === $newPosition) {
            return;
        }

        if ($newPosition > $old) {
            Highlight::where('position', '>', $old)
                ->where('position', '<=', $newPosition)
                ->decrement('position');
        } else {
            Highlight::where('position', '>=', $newPosition)
                ->where('position', '<', $old)
                ->increment('position');
        }

        $highlight->update(['position' => $newPosition]);
    });
}
```

Alternativa mais simples e equivalente: índice único em `position` + reescrita total
(`$i => $id`) dentro de uma transação.

## 4. Form Requests

```php
// app/Http/Requests/StoreHighlightRequest.php
public function rules(): array
{
    return [
        'title'            => ['required', 'string', 'max:150'],
        'subtitle'         => ['required', 'string', 'max:999'],
        'text_button'      => ['required', 'string', 'max:255'],
        'url'              => ['required', 'string', 'max:2999'],
        'position'         => ['required', 'integer', 'min:1'],
        'status'           => ['sometimes', 'boolean'],
        'status_button'    => ['sometimes', 'boolean'],
        'is_blank'         => ['sometimes', 'boolean'],
        'layout'           => ['required', Rule::enum(HighlightLayout::class)],
        'archive_highlight'=> ['required', 'image', 'max:20000', 'mimes:jpg,jpeg,png,gif,webp'],
    ];
}
```

Melhorias sobre o legado:

- `title` limitado a **150** (o Blade já limitava a 150, o request a 255) — unifica.
- `archive_highlight` passa a ser **obrigatório** (o legado não exigia, mas a UI sim).
- Mensagem de `position` corrigida (no legado dizia "informe um título").

## 5. Policies

```php
// app/Policies/HighlightPolicy.php
class HighlightPolicy
{
    public function viewAny(User $user): bool   { return $user->can('destaques.index'); }
    public function create(User $user): bool   { return $user->can('destaques.create'); }
    public function update(User $user): bool   { return $user->can('destaques.edit'); }
    public function delete(User $user): bool   { return $user->can('destaques.delete'); }
    public function publish(User $user): bool  { return $user->can('destaques.publish'); }
}
```

A autorização atual é feita por `has_access()` (helper legado do Sentinel) dentro das
views e do `DatatableController`. A_policy substitui isso de forma declarativa.

## 6. Endpoints REST/JSON sugeridos

| Método | URI | Descrição |
|---|---|---|
| GET | `/api/v1/highlights` | Lista paginada (filtro `status`) |
| GET | `/api/v1/highlights/{id}` | Detalhe |
| POST | `/api/v1/highlights` | Cria (upload via `multipart/form-data`) |
| PUT | `/api/v1/highlights/{id}` | Atualiza |
| PATCH | `/api/v1/highlights/{id}/status` | Publica/despublica |
| DELETE | `/api/v1/highlights/{id}` | Remove (arquivo + registro) |
| POST | `/api/v1/highlights/reorder` | Recebe `[{id, position}]` |

Exemplo de recurso:

```php
// app/Http/Resources/HighlightResource.php
return [
    'id'           => $this->id,
    'title'        => $this->title,
    'subtitle'     => $this->subtitle,
    'text_button'  => $this->text_button,
    'url'          => $this->url,
    'position'     => $this->position,
    'status'       => $this->status,
    'status_button'=> $this->status_button,
    'is_blank'     => $this->is_blank,
    'layout'       => $this->layout,
    'image_url'    => $this->archive?->url,
];
```

## 7. Storage

| Item | Legado | Moderno |
|---|---|---|
| Upload | `$request->file->storeAs("highlights/{$id}", $name)` | `Storage::disk('public')->putFileAs("highlights/{$id}", $file, $name)` |
| Caminho no banco | `storage/highlights/<id>/<nome>` (string) | coluna `path` na tabela `archives` ou `media` dedicado |
| Exclusão | não apaga o arquivo | `Storage::disk('public')->delete($archive->path)` no `deleted` event |

Recomenda-se extrair as imagens para a tabela `media` (polymórfica), reaproveitando o
`media` do framework, e remover `archives.highlight_id`.

## 8. Views Blade modernas

- **CMS:** `<x-admin.highlight.index />`, `<x-admin.highlight.form />` ( Alpine.js +
  Livewire para upload com preview), lista paginada server-side.
- **Portal:** componente único `<x-portal.highlight-carousel :highlights="$highlights" />`
  com as variantes de layout (`full-width` / `centered`) como **props**, em vez de
  seis cópias (`modelo1..modelo6/home/destaque.blade.php`).
- Substituir `lightslider.js` por um carousel Bootstrap 5 nativo já presente no stack.

## 9. Cache e fila

| Aspecto | Sugestão |
|---|---|
| Cache | `Cache::remember('portal.highlights', 300, fn() => Highlight::published()->with('archive')->get())` — o portal chama a query a cada request |
| Invalidação | `Model::saved` / `Model::deleted` → `Cache::forget('portal.highlights')` |
| Fila | Processamento de imagem (thumbnail `_min`, `_med`, `_larg`) via job `ProcessHighlightImage` (o `getLargFile()` depende de variantes geradas) |
| Eventos | `HighlightPublished`, `HighlightDeleted` para auditoria |

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

1. **Fallback de notícias**: se não houver destaque ativo, o carrossel de notícias
   aparece. Esse comportamento **não pode** ser perdido.
2. **`status` semântica**: mudar `null` → `false` exige migração de dados.
3. **`layout`**: `varchar` → `tinyint` exige `CAST`; conferir os valores gravados.
4. **`is_blank`**: legadamente não aplicado no portal; decidir se a v7 passa a aplicá-lo
   (recomendado: sim, com `target="_blank" rel="noopener"`).
5. **Slug do módulo**: corrigir `detaques` → `destaques` na tabela `modulos` **e**
   definir `modulos.status = 1` para o módulo voltar a aparecer.
6. **Ordem**: a ordenação por `position` precisa continuar sendo determinística;
   adicionar `orderBy('id')` como desempate.
