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

## 1. Schema de migração

A decisão estrutural mais importante: **criar tabelas próprias** para banners em vez
de reaproveitar `contents`.

```php
// database/migrations/xxxx_xx_xx_create_banners_table.php
Schema::create('banners', function (Blueprint $table) {
    $table->id();
    $table->foreignId('banner_category_id')->constrained('banner_categories')->cascadeOnDelete();
    $table->string('title');
    $table->string('slug')->unique();
    $table->unsignedSmallInteger('position')->default(1);   // ordem na lista
    $table->string('url')->nullable();
    $table->boolean('open_in_new_tab')->default(false);
    $table->boolean('is_active')->default(false);
    $table->timestamps();
    $table->softDeletes();
    $table->index(['banner_category_id', 'position']);
});

Schema::create('banner_categories', function (Blueprint $table) {
    $table->id();
    $table->string('slug')->unique();       // 'banners' | 'super-banners'
    $table->string('name');
    $table->string('type');                  // 'sidebar' | 'super'
    $table->timestamps();
});

Schema::create('banner_media', function (Blueprint $table) {
    $table->id();
    $table->foreignId('banner_id')->constrained('banners')->cascadeOnDelete();
    $table->string('path');
    $table->string('disk')->default('public');
    $table->unsignedSmallInteger('width')->nullable();
    $table->unsignedSmallInteger('height')->nullable();
    $table->string('alt')->nullable();
    $table->timestamps();
});
```

**Alternativa de menor risco**: manter `contents` + `category_contents` e apenas
extrair as views SQL para consultas Eloquent. É o caminho indicado para uma
reconstrução incremental.

## 2. Modelos

```php
// app/Models/Banner.php
class Banner extends Model
{
    use HasFactory, SoftDeletes;

    protected $fillable = ['title', 'slug', 'url', 'open_in_new_tab', 'is_active', 'position', 'banner_category_id'];

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

    public function category(): BelongsTo { return $this->belongsTo(BannerCategory::class); }
    public function media(): HasOne { return $this->hasOne(BannerMedia::class); }

    public function scopeActive(Builder $q): Builder { return $q->where('is_active', true); }
    public function scopeOrdered(Builder $q): Builder { return $q->orderBy('position')->orderBy('id'); }
}
```

```php
// app/Enums/BannerPosition.php
enum BannerPosition: int
{
    case Top    = 1;   // "Em cima"  / "Esquerda"
    case Bottom = 2;   // "Em baixo" / "Direita"

    public function label(string $template): string
    {
        return $this === self::Top
            ? ($template === 'modelo5' ? 'Esquerda' : 'Em cima')
            : ($template === 'modelo5' ? 'Direita' : 'Em baixo');
    }
}
```

O enum substitui a subconsulta SQL da view `super_banners` (armadilha A5).

## 3. Regras de negócio → Services

```php
// app/Services/BannerActivationService.php
public function activateSuperBanner(Banner $banner): void
{
    DB::transaction(function () use ($banner) {
        Banner::where('banner_category_id', $banner->banner_category_id)
            ->where('position', $banner->position)
            ->where('is_active', true)
            ->whereKeyNot($banner->getKey())
            ->update(['is_active' => false]);

        $banner->update(['is_active' => true]);
    });
}
```

```php
// app/Services/BannerReorderService.php
public function reorder(array $orderedIds): void
{
    DB::transaction(function () use ($orderedIds) {
        foreach (array_values($orderedIds) as $index => $id) {
            Banner::whereKey($id)->update(['position' => $index + 1]);
        }
    });
}
```

## 4. Form Requests

```php
// app/Http/Requests/StoreBannerRequest.php
public function rules(): array
{
    return [
        'title'          => ['required', 'string', 'max:120'],
        'url'            => ['nullable', 'string', 'max:2999'],
        'position'       => ['required', 'integer', 'min:1'],
        'is_active'      => ['sometimes', 'boolean'],
        'open_in_new_tab'=> ['sometimes', 'boolean'],
        'image'          => ['required', 'image', 'max:20000', 'mimes:jpg,jpeg,png,gif,webp'],
        'width'          => ['nullable', 'integer'],
        'height'         => ['nullable', 'integer'],
    ];
}
```

A regra condicional "imagem obrigatória só no create" deixa de depender do nome da
rota: usar `Rule::requiredIf($this->isMethod('POST'))` ou, melhor, separar
`StoreBannerRequest` de `UpdateBannerRequest`.

## 5. Policies

```php
// app/Policies/BannerPolicy.php
class BannerPolicy
{
    public function viewAny(User $u): bool    { return $u->can('banners.index'); }
    public function create(User $u): bool    { return $u->can('banners.create'); }
    public function update(User $u): bool    { return $u->can('banners.edit'); }
    public function delete(User $u): bool    { return $u->can('banners.delete'); }
    public function reorder(User $u): bool   { return $u->can('banners.edit'); }
}
```

Substitui `has_access('banners.*')` espalhado por views e JS.

## 6. Endpoints REST/JSON

| Método | URI | Descrição |
|---|---|---|
| GET | `/api/v1/banners?type=sidebar` | Lista de banners ativos (público) |
| GET | `/api/v1/banners/{slug}` | Detalhe |
| POST | `/api/v1/banners` | Cria |
| PUT | `/api/v1/banners/{id}` | Atualiza |
| PATCH | `/api/v1/banners/{id}/status` | Ativa/desativa |
| DELETE | `/api/v1/banners/{id}` | Remove |
| POST | `/api/v1/banners/reorder` | Recebe `[{id, position}]` |
| PATCH | `/api/v1/super-banners/{id}/activate` | Ativa e desativa os demais da mesma posição |

Recurso:

```php
// app/Http/Resources/BannerResource.php
return [
    'id'              => $this->id,
    'title'           => $this->title,
    'url'             => $this->url,
    'open_in_new_tab' => $this->open_in_new_tab,
    'position'        => $this->position,
    'position_label'  => BannerPosition::from($this->position)->label($this->template),
    'image_url'       => $this->media?->url,
    'alt'             => $this->media?->alt,
];
```

## 7. Views Blade modernas

- **CMS:** `<x-admin.banner.index>` com tabela paginada e **botões de mover
  (▲/▼)** em vez de drag & drop (acessibilidade e simplicidade); ou Alpine.js +
  sortable.js.
- **Portal:** um único componente

```blade
{{-- resources/views/components/portal/banner-strip.blade.php --}}
@props(['banners'])
<div class="banner-strip">
    @foreach ($banners as $banner)
        <a href="{{ $banner->url }}" @if($banner->open_in_new_tab) target="_blank" rel="noopener" @endif>
            <img src="{{ $banner->image_url }}" alt="{{ $banner->alt ?? $banner->title }}">
        </a>
    @endforeach
</div>
```

  substituiria `default/side-banners.blade.php`,
  `modelo1/layout/sidebar/banners.blade.php`, `modelo1/home/elementos.blade.php`,
  `modelo2/home/elementos.blade.php` e `modelo2/layout/nav.blade.php`.
- **Modelo5:** manter a composição por `layouts`, mas com um componente
  `<x-portal.super-banner :banner="$banner" :config="$config" />` no lugar de
  `super-banners-1/-2`.

## 8. Cache, fila e storage

| Aspecto | Sugestão |
|---|---|
| Cache | `Cache::remember('portal.banners.sidebar', 300, ...)` e `Cache::remember('portal.banners.super', 300, ...)`; invalidar em `BannerSaved`/`BannerDeleted` |
| Fila | Job `ProcessBannerImage` para gerar thumbnails (`_min`, `_med`, `_larg`) e WebP |
| Storage | `Storage::disk('public')->putFileAs("banners/{$id}", $file, $name)`; apagar o arquivo ao remover o banner |
| Livewire/Alpine | Upload com preview e barra de progresso, sem recarregar a página |

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

1. **Semântica do `order`**: no super banner, `order` é posição na tela. Ao migrar para
   uma tabela própria, separar `position` (posição na lista) de `slot`
   (`top`/`bottom`). A migração de dados precisa mapear `order` 1 → `slot=top`,
   `order` 2 → `slot=bottom`.
2. **Layout**: a posição exibida depende do layout ativo; o enum resolve, mas é preciso
   garantir que a configuração de layout continue acessível no controller (via
   `Configuration::where('key','layout')`).
3. **Links e arquivos**: `links` e `archives` continuam existindo para os demais
   módulos; apenas os banners migram.
4. **Carrossel do modelo5**: a composição livre da tabela `layouts` deve continuar
   funcionando — mudar a view de super banners pode quebrar o modelo personalizável.
5. **Tamanho dos banners**: 267x95 e 772x90 são apenas sugestões da UI; o portal não
   valida dimensões. Decidir se a v7 passa a validar.
6. **Ativação exclusiva**: só um super banner por posição — comportamento que não pode
   ser perdido.
7. **Views SQL**: se mantidas, precisam ser versionadas como migrations, e não seeds
   (a v6 depende do seed `UpdateSuperBannersViewFixEmBaixo.php`).
