# Sessões da Câmara Municipal — API, Views Modernas e Infraestrutura

> Continuação de `06-MIGRACAO-MODERNA.md` (fase 2 do plano de reconstrução
> em PHP 8.x + Laravel 11/12).
> Todas as decisões de equivalência funcional estão em
> `06-MIGRACAO-MODERNA.md`, seção 1.

## Escopo desta segunda parte

| Bloco | Seção | Resultado esperado na v7 |
|---|---|---|
| Contrato JSON | 7 | Um `SessionResource` que expõe título, tipo, datas, vídeo em *embed*, capa e galeria. |
| Views | 8 | Um conjunto único de views Blade (em vez de 13 cópias na v6), com componentes reutilizáveis. |
| Infraestrutura | 9 | Storage em disco `public`, fila para thumbnails, cache da listagem pública. |
| Cobertura | 10 | Plano de testes alinhado a `07-TESTES.md`. |
| Riscos | 11 | Registro do que pode quebrar na migração de dados. |

---

## 7. API Resource

```php
class SessionResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'            => $this->id,
            'title'         => $this->title,
            'slug'          => $this->slug,
            'session_type'  => $this->session_type->value,
            'session_number'=> $this->session_number,
            'starts_at'     => $this->starts_at->toIso8601String(),
            'ends_at'       => $this->ends_at?->toIso8601String(),
            'published_at'  => $this->starts_at->format('d/m/Y') . ' às ' . $this->starts_at->format('H:i'),
            'is_published'  => $this->is_published,
            'has_gallery'   => $this->has_gallery,
            'video_embed'   => $this->video_embed_url,
            'cover_image'   => $this->coverImage?->url,
            'gallery'       => SessionImageResource::collection($this->images),
            'body_html'     => $this->body,
            'links'         => [
                ['rel' => 'self', 'href' => route('api.sessions.show', $this->slug)],
            ],
        ];
    }
}
```

## 8. Views Blade modernas

| Legado | v7 |
|---|---|
| 6 cópias de `index.blade.php` e `show.blade.php` | **1** conjunto de views em `resources/views/portal/sessions/` com um `<x-portal-layout>` parametrizado pelo tema. |
| `v6/resources/views/portal/layouts/modelo5/home/sessoes/sessoes-list.blade.php` | Componente `<x-portal.session-list :sessions="$sessions" :limit="4" />`. |
| `v6/resources/views/portal/components/galerias/index.blade.php` | Componente `<x-portal.gallery :images="$session->images" />` com Alpine.js + lightbox. |
| `v6/resources/views/portal/statics/video.blade.php` | Componente `<x-portal.video :url="$linkVideo" />`. |
| `v6/resources/views/prefecture/contents/sessoes/form.blade.php` | Livewire ou Inertia + um único componente de formulário, com abas "Conteúdo" e "Galeria". |

Exemplo do componente de listagem:

```blade
<x-portal.session-list :sessions="$sessions">
    <x-empty-state message="Nenhuma registro encontrado." />
</x-portal.session-list>
```

O **DataTables server-side** do CMS pode ser substituído por
**Laravel Livewire / Inertia + paginação nativa**, mantendo as colunas
equivalentes (`Título`, `Status`, `Data de publicação`) e a exportação
Excel/PDF via `maatwebsite/excel` e `barryvdh/laravel-dompdf`.

## 9. Fila, cache e storage

| Item | Proposta |
|---|---|
| **Storage** | Disco `public` com `store('sessions/{id}/cover')` e `store('sessions/{id}/gallery')`; served por `php artisan storage:link`. O legado gravava em `storage/app/public/content/sessoes/{id}/destaque`. |
| **Thumbnails** | Job `GenerateSessionThumbnail` (queued) para gerar miniaturas WebP e a thumbnail do YouTube/Vimeo quando não há imagem de destaque (hoje isso é síncrono, em `ContentPresenter::getArchiveContentHighlightOrThumbnail`). |
| **Conversão de imagem** | Job `OptimizeSessionImage` usando `spatie/image` ou `intervention/image` v3, com `WebP` + `min` + `med` + `larg` variantes — substitui `getMinFile`, `getMedFile`, `getLargFile` (`v6/app/Helpers/functions.php:977-1006`). |
| **Cache** | `Cache::remember("sessions.index.{$page}", 300, $callback)` para a listagem pública; invalidação via `SessionObserver` (saved/deleted). O legado não tinha cache. |
| **Fila** | `QUEUE_CONNECTION=database` ou `redis` (o projeto já usa Redis). |
| **Busca** | Se houver busca full-text no portal, `laravel/scout` com índice por `title` e `body`. |

## 10. Testes previstos

| Tipo | Classe sugerida | Cobertura |
|---|---|---|
| Feature (público) | `SessionIndexTest` | E1, E2, E3, E7 |
| Feature (público) | `SessionShowTest` | E4, E5, E6, E10 |
| Feature (CMS) | `AdminSessionTest` | Permissões, validação, CRUD |
| Unit | `VideoEmbedTest` | YouTube, Vimeo, Facebook, Instagram, fallback |
| Unit | `SessionScopeTest` | `scopePublished` |
| Feature | `SessionMigrationTest` | Integridade da migração de `contents` → `sessions` |

Detalhamento em `07-TESTES.md`.

## 11. Riscos da migração

| # | Risco | Mitigação |
|---|---|---|
| M1 | Perder conteúdo por não mapear a categoria raiz corretamente (`category_contents_id IS NULL`). | Validar com `SELECT count(*)` antes e depois da migração. |
| M2 | Colisão de `slug` ao criar o índice `unique`. | Resolver duplicatas com sufixo antes de criar o índice. |
| M3 | Fotos de galeria com `highlight = 1` (destaque) sendo migradas também para a galeria. | Migrar `highlight IS NULL` para `session_images` e `highlight = true` para a capa, com o mesmo critério do legado. |
| M4 | Links de vídeo com `isBlank` ou duplicados. | Migrar apenas o primeiro link por `contents_id` (mais baixo `id`), como faz `linksUnique`. |
| M5 | Textos com HTML antigo do Summernote e imagens em `summernote-files/temporarios`. | Sanear o HTML no job de migração e mover os arquivos. |
| M6 | URLs antigas (`/portal/sessoes/{slug}`) deixarem de funcionar. | Criar *redirect* 301 de `/portal/sessoes/{slug}` para `/sessoes/{slug}` e vice-versa, conforme a decisão de URL canônica. |
| M7 | Datas passadas com `publication_date` nulas (conteúdos antigos) ficarem "no futuro" ou inválidas. | Definir fallback para `created_at` e marcar `is_published = false` quando a data for nula. |
| M8 | Perda das permissões `sessoes.*` do Sentinel ao migrar para o novo sistema de permissões. | Mapear o recurso Sentinel para as permissões do Spatie ou do Laravel Policy. |
| M9 | Regressão no widget da home se o `$sessions` do `PortalController` mudar de formato. | Usar o mesmo `scopePublished` no widget e adicionar teste que garanta a presença dos 4 itens mais recentes. |

## 12. Checklist de execução da migração

| Ordem | Passo | Entregável |
|---|---|---|
| 1 | Criar a tabela `sessions` e a migration correspondente. | Schema com `starts_at`, `ends_at`, `session_type`, `video_url`, `is_published`, `has_gallery`, `cover_image_id`. |
| 2 | Modelos `Session`, `SessionImage`, `Media` e o service `VideoEmbed`. | Eloquent enxuto com `scopePublished` e casts declarados. |
| 3 | Policy `SessionPolicy` + registro do Gate. | Autorização equivalente ao `has_access` legado. |
| 4 | `StoreSessionRequest` e `UpdateSessionRequest`. | Regras equivalentes ao caso `sessoes` do `ContentRequest` legado. |
| 5 | Migration de dados `contents` → `sessions`. | Script idempotente, com relatório de contagem e checagem de slugs duplicados. |
| 6 | Views públicas e componentes Blade. | `/sessoes` e `/sessoes/{slug}` renderizando com o mesmo conteúdo observável. |
| 7 | CMS (rotas administrativas, controller e views). | Listagem paginada no servidor, formulário com abas "Conteúdo" e "Galeria". |
| 8 | `SessionResource` + endpoint `GET /api/sessions`. | Contrato JSON estável para integrações futuras. |
| 9 | Jobs de otimização de imagem e geração de thumbnail. | Fila configurada (`QUEUE_CONNECTION=redis`). |
| 10 | Cache da listagem pública + invalidação por observer. | `Cache::remember` com invalidação em `saved`/`deleted`. |
| 11 | Redirects 301 entre `/portal/sessoes/{slug}` e `/sessoes/{slug}`. | URLs antigas preservadas para o/link externo. |
| 12 | Testes automatizados conforme `07-TESTES.md`. | Suíte verde antes do corte. |
| 13 | Reciclagem do módulo legado (`sessoes` na `modulos`, permissões, seeds de layout). | Remoção completa do código da v6, sem shim. |
