# Avisos — Migração para Laravel Moderno (PHP 8.x + Laravel 11/12)

> Plano de reconstrução do módulo. **DECISÃO** marca o que não pode mudar.

---

## 1. Schema de migração Laravel

A v7 deve ter tabela própria, com os dados hoje espalhados em `contents`, `links` e
`archives`:

```php
// exemplo ilustrativo
Schema::create('announcements', function (Blueprint $table) {
    $table->id();
    $table->string('title', 191);
    $table->string('url', 2999);
    $table->string('image_path')->nullable();   // storage/app/public/avisos/aviso.jpg
    $table->boolean('open_in_new_tab')->default(false);
    $table->boolean('active')->default(false)->index();
    $table->timestamp('starts_at')->nullable();
    $table->timestamp('ends_at')->nullable();
    $table->timestamps();
    $table->softDeletes();
});
```

**DECISÃO 1 — o aviso continua sendo "uma imagem + um link + um título".** A migração
mapeia `contents.title` -> `title`, `links.link` -> `url`, `links.isBlank` ->
`open_in_new_tab`, `archives.path` (com `highlight = 1`) -> `image_path` e
`contents.status` -> `active`.

**DECISÃO 2 — no máximo um aviso ativo.** Na v7 a garantia deve vir do banco:

```php
// exemplo ilustrativo — índice único parcial (PostgreSQL)
DB::statement('CREATE UNIQUE INDEX announcements_active_unique ON announcements (active) WHERE active = true AND deleted_at IS NULL');
```

Alternativa multiplataforma: manter a regra na aplicação (como o legado) **e** validar
com um teste de feature.

Script de migração de dados (exemplo ilustrativo):

```php
DB::table('contents')
    ->where('category_contents_id', $anunciosCategoryId)
    ->whereNull('deleted_at')
    ->orderBy('id')
    ->chunk(500, function ($rows) use ($anunciosCategoryId) {
        foreach ($rows as $row) {
            $link = DB::table('links')->where('contents_id', $row->id)->first();
            $image = DB::table('archives')
                ->where('contents_id', $row->id)->where('highlight', 1)->first();

            DB::table('announcements')->insert([
                'title' => $row->title,
                'url' => $link->link ?? '',
                'open_in_new_tab' => (bool) ($link->isBlank ?? 0),
                'image_path' => $image->path ?? null,
                'active' => (bool) $row->status,
                'created_at' => $row->created_at,
                'updated_at' => $row->updated_at,
                'deleted_at' => $row->deleted_at,
            ]);
        }
    });
```

## 2. Models

```php
// exemplo ilustrativo
class Announcement extends Model
{
    use SoftDeletes;

    protected $fillable = ['title', 'url', 'image_path', 'open_in_new_tab', 'active', 'starts_at', 'ends_at'];

    protected $casts = [
        'open_in_new_tab' => 'boolean',
        'active' => 'boolean',
        'starts_at' => 'datetime',
        'ends_at' => 'datetime',
    ];

    public function scopeActive(Builder $query): Builder
    {
        return $query->where('active', true)
            ->where(fn ($q) => $q->whereNull('starts_at')->orWhere('starts_at', '<=', now()))
            ->where(fn ($q) => $q->whereNull('ends_at')->orWhere('ends_at', '>=', now()));
    }

    public function imageUrl(): string
    {
        return $this->image_path ? Storage::disk('public')->url($this->image_path) : asset('img/avisos-sem-imagem.png');
    }
}
```

| Model | Tabela | Observações |
|---|---|---|
| `Announcement` | `announcements` | Aviso |
| `AnnouncementImage` (opcional) | `announcement_images` | Só se o módulo aceitar mais de uma imagem |

## 3. Form Requests

```php
// exemplo ilustrativo
class StoreAnnouncementRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('create', Announcement::class);
    }

    public function rules(): array
    {
        return [
            'title' => ['required', 'string', 'max:120'],
            'url' => ['required', 'url', 'max:2999'],
            'open_in_new_tab' => ['boolean'],
            'active' => ['boolean'],
            'image' => ['required', 'image', 'mimes:jpg,jpeg,png,gif,webp', 'max:20000'],
        ];
    }
}
```

**DECISÃO 3 — a imagem continua obrigatória na criação** (o legado exige `archive_highlight`
apenas no `store`); na edição, o campo é opcional e mantém a imagem anterior.

**DECISÃO 4 — o limite do link permanece 2999** (o markup do CMS legado usava 200; a v7
deve padronizar em 2999, que é o limite já validado no servidor).

## 4. Policies / autorização

| Ação | Permissão legada | Policy v7 |
|---|---|---|
| Ver no portal | — | pública |
| Criar/editar/excluir | `anuncios.create`, `anuncios.edit`, `anuncios.delete` | `AnnouncementPolicy` |
| Ativar/desativar | (botão genérico `set.status`) | `AnnouncementPolicy@activate` |

A permissão continua com o slug `anuncios` para preservar os papéis gravados
(`resources.slug = 'anuncios'`), mesmo que o módulo passe a se chamar `avisos` na UI.

## 5. Endpoints REST/JSON sugeridos

| Método | URI | Descrição |
|---|---|---|
| GET | `/api/v1/avisos` | Lista (paginada) — inclusive para consulta histórica |
| GET | `/api/v1/avisos/ativo` | Aviso ativo (o que o portal exibe) |
| GET | `/api/v1/avisos/{id}` | Item único |
| POST/PUT/DELETE | `/api/v1/avisos[/{id}]` | CRUD do CMS |
| POST | `/api/v1/avisos/{id}/ativar` | Ativa e desativa os demais (garantindo exclusividade) |
| GET | `/avisos` | **Nova** página pública de arquivo de avisos (não existe no legado) |

## 6. Recursos de API

```php
// exemplo illustrativo
class AnnouncementResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'url' => $this->url,
            'image_url' => $this->imageUrl(),
            'open_in_new_tab' => $this->open_in_new_tab,
            'active' => $this->active,
            'starts_at' => $this->starts_at?->toIso8601String(),
            'ends_at' => $this->ends_at?->toIso8601String(),
        ];
    }
}
```

## 7. Views Blade modernas

| Antes (v6) | Depois (v7) |
|---|---|
| HTML do modal copiado em 7 arquivos | 1 componente `<x-announcement.modal :announcement="$announcement" />` incluído pelo layout |
| Abertura por script em cada layout | O componente decide a abertura (Bootstrap 5 `data-bs-*` ou `<dialog>`) |
| Imagem via `->path` em `null` | `$announcement->imageUrl()` com imagem padrão |
| Verificação de módulo só em 3 layouts | Componente único, com a checagem em um lugar só |

Exemplo de componente (exemplo ilustrativo):

```blade
{{-- resources/views/components/announcement/modal.blade.php --}}
@props(['announcement'])
@if ($announcement)
    <div class="modal fade" id="anuncio" tabindex="-1" aria-hidden="true">
        <div class="modal-dialog modal-dialog-centered" style="max-width:700px">
            <div class="modal-content">
                <div class="modal-body">
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fechar"></button>
                    <a href="{{ $announcement->url }}"
                       @if ($announcement->open_in_new_tab) target="_blank" rel="noopener" @endif>
                        <img class="img-fluid" src="{{ $announcement->imageUrl() }}" alt="{{ $announcement->title }}">
                    </a>
                </div>
            </div>
        </div>
    </div>
@endif
```

**DECISÃO 5 — o modal continua abrindo automaticamente ao carregar a home** e o clique
na imagem leva ao link informado, com a mesma opção "abrir em nova guia".

## 8. Fila, cache e storage

| Aspecto | Recomendação |
|---|---|
| Storage | `Storage::disk('public')->putFile('avisos', $file)`; recomprimir com `Intervention\Image` para 700x405 |
| Cache | `Cache::remember('aviso:ativo', 60, fn () => Announcement::active()->first())`; invalidar ao ativar/desativar |
| Fila | Opcional: gerar uma versão reduzida (WebP) da imagem em background |
| Eventos | `AnnouncementActivated` (para limpar cache e registrar log) |
| Métricas | Registrar impressões/cliques do aviso (hoje não há rastreamento) |

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

| # | Risco | Mitigação |
|---|---|---|
| R1 | Manter dois avisos ativos quebraria o modal (mostra o primeiro) | Índice único parcial (DECISÃO 2) |
| R2 | Links do portal para o aviso não existem (não há rota) | Nenhum link quebrado |
| R3 | Mudar o tamanho da imagem (700x405) altera o layout do modal | Manter 700x405 |
| R4 | A imagem é obrigatória na criação; se o modelo novo permitirSavê-lo sem imagem, o modal quebra | Manter obrigatória ou usar imagem padrão (DECISÃO 3) |
| R5 | Renomear o slug de `anuncios` para `avisos` quebra permissões e seeds | Manter `anuncios` como slug técnico; expor "Avisos" na UI |
| R6 | O `Cache::rememberForever('modulos')` da v6 não tem equivalente | Cache com invalidação por evento |
| R7 | Fotos com caminho em `storage/content/anuncios/12/destaque/aviso.png` precisam ser migradas para o novo disco | Script de cópia do storage junto com a migração de dados |
