# Agenda de Eventos — Plano de Migração para Laravel 11/12 (v7)

> Objetivo: reconstruir o módulo com o **mesmo comportamento observável**, removendo o
> acoplamento com `archives` e a view SQL sem perder nenhuma funcionalidade.

---

## 1. Schema de migração (equivalente Laravel moderno)

```php
// v7: database/migrations/2025_xx_xx_create_calendar_events_tables.php
Schema::create('category_calendar_events', function (Blueprint $table) {
    $table->id();
    $table->string('name', 191);
    $table->string('slug', 191)->nullable();
    $table->timestamps();
    $table->softDeletes();
    $table->unique('slug');
});

Schema::create('calendar_events', function (Blueprint $table) {
    $table->id();
    $table->string('title', 191);
    $table->timestamp('initial_date')->nullable();
    $table->timestamp('final_date')->nullable();
    $table->string('local', 191)->nullable();
    $table->string('source', 191)->nullable();
    $table->text('text');
    $table->boolean('status')->default(false);
    $table->boolean('gallery')->default(false);
    $table->string('slug', 191)->nullable();
    $table->foreignId('category_calendar_events_id')
          ->nullable()->constrained('category_calendar_events')->nullOnDelete();
    $table->foreignId('secretaria_id')->nullable()->constrained('contents')->nullOnDelete();
    $table->foreignId('orgao_id')->nullable()->constrained('contents')->nullOnDelete();
    $table->timestamps();
    $table->softDeletes();
    $table->index(['status', 'initial_date']);
    $table->index('slug');
});
```

**Decisões de equivalência funcional:**

- Manter os **nomes de coluna atuais** (`category_calendar_events_id`, `secretarias_id`,
  `orgaos_id`, `galery`, `source`, `local`) para permitir migração de dados sem
  renomeações; opcionalmente criar *aliases* em Eloquent via `accessors`.
- `galery` (grafia incorreta, herdada) deve ser preservada no banco ou renomeada com
  cuidado — a coluna é lida por SQL cru na view e por scripts de terceiros.
- Substituir a coluna `archives.calendar_events_id` por uma tabela própria
  `calendar_event_archives` (ou uma tabela `media` polimórfica com `morphs`), **ou**
  manter `archives` se os demais módulos exigirem. Se a v7 unificar os módulos, criar:
  `calendar_event_media (id, calendar_event_id, path, name, title, extension, mime_type,
  is_highlight, order, timestamps, softDeletes)`.

---

## 2. Models

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

    protected $fillable = [
        'title', 'initial_date', 'final_date', 'local', 'source', 'text',
        'slug', 'status', 'galery', 'category_calendar_events_id',
        'secretarias_id', 'orgaos_id',
    ];

    protected $casts = [
        'initial_date' => 'datetime',
        'final_date'   => 'datetime',
        'status'       => 'boolean',
        'galery'       => 'boolean',
    ];

    public function category(): BelongsTo
    {
        return $this->belongsTo(CategoryCalendarEvent::class, 'category_calendar_events_id');
    }

    public function secretaria(): BelongsTo
    {
        return $this->belongsTo(Content::class, 'secretarias_id');
    }

    public function orgao(): BelongsTo
    {
        return $this->belongsTo(Content::class, 'orgaos_id');
    }

    public function media(): HasMany
    {
        return $this->hasMany(CalendarEventMedia::class);
    }
}
```

- `CategoryCalendarEvent`: `HasMany` para `calendarEvents` (corrigindo A2 do legado).
- **Enum sugerido:** `EventStatus { PUBLISHED = 1, DRAFT = 0 }` (mantendo `boolean` no
  banco até a migração de dados) e `MediaKind { HIGHLIGHT, GALLERY, ATTACHMENT }`.
- Slug: `Str::slug(Str::limit($title, 50))` + desambiguação — extrair para um
  `HasSlug` trait reutilizável (o mesmo padrão existe em categorias).

---

## 3. Form Requests

```php
// app/Http/Requests/StoreCalendarEventRequest.php
class StoreCalendarEventRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('agenda-evento.create'); // Gate derivado do Sentinel
    }

    public function rules(): array
    {
        return [
            'category_calendar_events_id' => ['required', 'integer',
                Rule::exists('category_calendar_events', 'id')->whereNull('deleted_at')],
            'title'         => ['required', 'string', 'max:120'],
            'initial_date'  => ['required', 'date_format:Y-m-d H:i'],
            'final_date'    => ['nullable', 'date_format:Y-m-d H:i', 'after_or_equal:initial_date'],
            'local'         => ['nullable', 'string', 'max:120'],
            'source'        => ['nullable', 'string', 'max:120'],
            'text'          => ['required', 'string'],
            'status'        => ['sometimes', 'boolean'],
            'galery'        => ['sometimes', 'boolean'],
            'archive_highlight' => ['nullable', 'file', 'max:5120', 'mimes:jpg,jpeg,png,gif,webp'],
        ];
    }
}
```

- `UpdateCalendarEventRequest` idem, com `Rule::unique` para o slug.
- `StoreCategoryCalendarEventRequest`: `name` -> `['nullable','string','max:190']`
  (preservando a opcionalidade da v6) + `slug` gerado no servidor.
- `DeleteCalendarEventBulkRequest`: `toDelete` -> `['required','array']`,
  `toDelete.*` -> `['integer','exists:calendar_events,id']` (corrige A17).
- Mensagens em português mantidas as do legado (bloco `messages()`).

---

## 4. Policies / autorização

| Ação | Gate |
|---|---|
| Listar / criar / editar / excluir | `agenda-evento.index` / `.create` / `.edit` / `.delete` (traduzir as permissões Sentinel do papel "Administrador" registradas em `v6/database/seeds/RolesTableSeeder.php:28`) |
| Gerenciar categorias | Reutilizar as permissões de `agenda-evento` (como no legado) ou criar `agenda-categoria.*` — decisão do negócio |
| Portal | Público, sem autenticação |

Implementação sugerida: `CalendarEventPolicy` + `Gate::before` que concede tudo a
`super-admin`, espelhando `has_access()`.

---

## 5. Endpoints REST/JSON

### CMS (continuar sob `/cms`, com `auth:sentinel` ou o guard equivalente)

| Método | URI | Nome sugerido | Controller |
|---|---|---|---|
| GET | `/cms/agenda-evento` | `agenda-evento.index` | `CalendarEventController@index` (JSON, paginado) |
| POST | `/cms/agenda-evento` | `agenda-evento.store` | `CalendarEventController@store` |
| GET | `/cms/agenda-evento/{id}` | `agenda-evento.show` | `CalendarEventController@show` (**implementar** — hoje a rota existe e o método não) |
| PUT | `/cms/agenda-evento/{id}` | `agenda-evento.update` | `CalendarEventController@update` |
| DELETE | `/cms/agenda-evento/{id}` | `agenda-evento.destroy` | `CalendarEventController@destroy` |
| POST | `/cms/agenda-evento/bulk-delete` | `agenda-evento.bulk-destroy` | `CalendarEventController@destroyBulk` |
| GET/POST/PUT/DELETE | `/cms/agenda-evento/categorias[...]` | `agenda-categoria.*` | `CategoryCalendarEventController` |
| POST | `/cms/agenda-evento/{id}/media` | `agenda-evento.media.store` | `CalendarEventMediaController@store` |
| DELETE | `/cms/agenda-evento/media/{id}` | `agenda-evento.media.destroy` | `CalendarEventMediaController@destroy` |

### Portal

| Método | URI | Nome | Observação |
|---|---|---|---|
| GET | `/api/agenda-eventos` | `api.agenda-eventos.index` | JSON paginado (filtros: `category`, `status`, `from`, `to`, `per_page`) |
| GET | `/api/agenda-eventos/{category}/{slug}` | `api.agenda-eventos.show` | Detalhe |
| GET | `/agenda-eventos` | `agenda-eventos` | Blade (manter) |
| GET | `/agenda-eventos/{category}` | `agenda-eventos-category` | Blade (manter) |
| GET | `/agenda-eventos/{category}/{slug}` | `agenda-eventos.show` | Blade (manter) |

Manter `/agenda` como alias permanente de `/agenda-eventos` (a v6 já o adicionou) ou
remover com redirect 301 — **decisão do negócio**.

---

## 6. API Resources

```php
// app/Http/Resources/CalendarEventResource.php
return [
    'id'          => $this->id,
    'title'       => $this->title,
    'slug'        => $this->slug,
    'category'    => new CategoryCalendarEventResource($this->whenLoaded('category')),
    'initial_date'=> $this->initial_date?->toIso8601String(),
    'final_date'  => $this->final_date?->toIso8601String(),
    'local'       => $this->local,
    'source'      => $this->source,
    'text'        => $this->text,          // HTML sanitizado
    'status'      => $this->status,
    'gallery'     => $this->galery,
    'highlight'   => new MediaResource($this->media->firstWhere('is_highlight', true)),
    'gallery_items' => MediaResource::collection($this->media->where('is_highlight', false)),
    'links'       => [
        'portal' => route('agenda-eventos.show', [$this->category->slug, $this->slug]),
    ],
];
```

O portal atual **não** tem API JSON; este recurso é uma adição. O `text` é HTML de
Summernote: na v7, sanitizar na entrada (HTMLPurifier) ou migrar para Markdown.

---

## 7. Views Blade modernas

- Componente `<x-calendar-event-card :event="$event" />` para o card da listagem
  (hoje o markup está inline em `portal/layouts/calendar-events/index.blade.php`).
- Componente `<x-media-gallery :items="$media" />` no lugar de
  `portal/components/galerias/index.blade.php` (lightgallery).
- Manter os **cinco templates** (`modelo1`..`modelo5`) como views-variantes ou
  unificá-los num único layout com *slots*; eliminar o template `default`, que hoje
  quebra as views da agenda (falta `portal/layouts/default/layout/layout.blade.php`).
- Formulário do CMS: Blade + Alpine.js/Vue leve, com componente de upload (Dropzone)
  substituindo `jquery-upload-files`.
- Editor de texto: TinyMCE/Lexical/TipTap no lugar do Summernote, mantendo a
  compatibilidade com o HTML já gravado.

---

## 8. Fila, cache e storage

| Aspecto | Legado | v7 |
|---|---|---|
| Upload | `storeAs` direto em `storage/app/public/calendar-events/...` + `compress` (Intervention Image) | `Storage::disk('public')->put(...)`, com um `MediaService` que gera as variantes `_min`/`_med`/`_larg` |
| Fila | Nenhuma | Converter imagens em **job** (`GenerateImageVariants`) para não bloquear a requisição |
| Cache | Nenhum | `Cache::remember` do payload público por 5 min, invalidado ao salvar/excluir evento |
| Purga | Nenhuma | Job `PurgeOrphanMedia` para `archives`/`media` sem evento (hoje orfãos permanecem) |
| E-mail | Nenhum | Notificação opcional ao publicar evento (não existe no legado) |

---

## 9. Riscos e decisões de equivalência funcional (o que NÃO pode mudar)

| # | Comportamento a preservar | Motivo |
|---|---|---|
| E1 | Portal lista **somente** eventos com `status = true` | Regra central de publicação |
| E2 | Paginação de **12** por página no portal | Layout e UX já comunicados |
| E3 | Slug do evento derivado do título (limite de 50 caracteres) e do slug da categoria derivado do nome | URLs públicas já divulgadas; mudar quebra links |
| E4 | Desambiguação `-N` de slugs duplicados | Evita colisão de rota `/agenda-eventos/{category}/{slug}` |
| E5 | Exclusão é **lógica** (soft delete) em eventos e categorias | Registros antigos podem estar referenciados |
| E6 | Imagem de destaque é **substituída** (não acumular) e a galeria é **acumulativa** | Comportamento observável pelo operador |
| E7 | `highlight = 1` = destaque; `highlight = null` = galeria | Semântica usada por várias queries do portal |
| E8 | Contagem do dashboard = eventos **não** soft-deleted (sem filtro de status/data) | Regressão coberta por `v6/tests/Feature/DashboardEventosWidgetTest.php` |
| E9 | Rótulos "Publicado"/"Rascunho" na listagem do CMS | Textos usados em relatórios e no front |
| E10 | O **período completo** aparece no detalhe ("Data do evento: ... até ...") | Texto exibido ao cidadão |
| E11 | Continuar **sem** filtro de data na listagem pública (eventos passados seguem visíveis) | Regra implícita; se mudar, é decisão de negócio explícita |

---

## 10. Ordem sugerida de execução

1. Migrations + models + factories (preservando nomes de coluna).
2. Migration de dados: `archives.calendar_events_id` -> `calendar_event_media`
   (ou mantida em `archives`).
3. `FormRequest`s + `Policies` + `Services` (slug, datas, upload).
4. Controllers do CMS + endpoints JSON + Resources.
5. Rotas do portal + views Blade (componente de card e galeria).
6. Reprodução da listagem do CMS: substituir a view SQL por
   `CalendarEvent::query()->with('category')->orderByDesc('created_at')` paginado no
   servidor (mantendo as colunas `titulo`, `status`, `data_inicial`, `data_final`,
   `categoria`, `categoria_slug`, `id`, `galeria`, `slug` para não quebrar o front).
7. Testes de feature (ver `07-TESTES.md`).
8. Remoção do código legado: rota `ajax-upload-files-calendar-event`, método
   `ajaxUploadFiles`, rota `agenda-evento.show` sem método, view SQL e o token
   `@getMinFile`.
