# Transmissões — Plano de Migração para Laravel Moderno (v7)

> Alvo: PHP 8.x + Laravel 11/12. O módulo é **novo na v6**; não há migração de
> dados legada a preservar além das tabelas genéricas.

## 1. Decisões de equivalência funcional (o que NÃO pode mudar)

| # | Decisão | Motivo |
|---|---|---|
| E1 | O player continua **na barra lateral** de todas as páginas do portal. | É o único ponto de entrada visível; mudar de posição altera o UX público. |
| E2 | Continua existindo **apenas uma** transmissão ativa. | Regras operacional do município (só uma sessão por vez). |
| E3 | O rótulo público continua sendo **"Transmissão"** e o formato **16:9**. | Identidade visual do portal. |
| E4 | O módulo continua **desligado por padrão** e ligável pelo município. | Controle de quais módulos o município deseja usar. |
| E5 | O campo aceita **URL do YouTube** e **ID**, e **os dois precisam funcionar**. | O ID já é aceito na UI; o bug é da implementação (ver `05-REGRAS-E-PORTABILIDADE.md`, A2). |
| E6 | Sem transmissão configurada → **nenhum card**; link inválido → **"Transmissão não disponível"**. | Comportamento observável atual. |
| E7 | A troca de status por **um clique** na listagem continua disponível. | Rotina do operador durante a sessão. |

## 2. Schema de migração Laravel

```php
// (ilustrativo) database/migrations/2024_xx_xx_create_transmissions_table.php
Schema::create('transmissions', function (Blueprint $table) {
    $table->id();
    $table->string('title', 120);
    $table->string('video_url', 200);          // URL canônica (embed) ou URL original
    $table->string('video_id', 32)->nullable(); // ID do YouTube, extraído na gravação
    $table->string('channel', 60)->default('youtube'); // youtube | vimeo | custom
    $table->boolean('is_live')->default(false);  // "no ar" no momento
    $table->timestamp('starts_at')->nullable();   // início previsto da sessão
    $table->timestamp('ends_at')->nullable();     // fim previsto
    $table->boolean('status')->default(false);   // ativo/inativo (equivale ao legado)
    $table->timestamps();
    $table->softDeletes();

    $table->index(['status', 'starts_at']);
});
```

### 2.1 Migração dos dados legados

Para cada `contents` da categoria `transmissoes` (id 36) com `links`:

```sql
INSERT INTO transmissions (title, video_url, video_id, channel, status, created_at, updated_at)
SELECT c.title,
       l.link,
       NULL,
       'youtube',
       COALESCE(c.status, false),
       c.created_at,
       c.updated_at
  FROM contents c
  JOIN links l ON l.contents_id = c.id
 WHERE c.category_contents_id = 36
   AND c.deleted_at IS NULL;
```

O ID do YouTube é extraído em PHP (regex), não em SQL, reaproveitando o
serviço de normalização descrito na seção 5.

### 2.2 O que permanece genérico

`contents`, `category_contents` e `links` **continuam existindo** para os
demais módulos. `transmissions` é uma tabela **nova e dedicada**; o módulo
deixa de depender de `category_contents` para o dado em si (a categoria legada
permanece apenas como fonte da migração inicial).

## 3. Modelos

```php
// (ilustrativo) app/Models/Transmission.php
namespace App\Models;

enum VideoChannel: string { case Youtube = 'youtube'; case Vimeo = 'vimeo'; case Custom = 'custom'; }

class Transmission extends Model
{
    use HasFactory, SoftDeletes;

    protected $fillable = ['title', 'video_url', 'video_id', 'channel', 'is_live', 'starts_at', 'ends_at', 'status'];

    protected $casts = [
        'is_live'  => 'boolean',
        'status'   => 'boolean',
        'starts_at' => 'datetime',
        'ends_at'   => 'datetime',
        'channel'  => VideoChannel::class,
    ];

    public function scopeActive(Builder $q): Builder
    {
        return $q->where('status', true);
    }

    public function scopeLive(Builder $q): Builder
    {
        return $q->where('status', true)
                 ->where('is_live', true)
                 ->where(fn ($x) => $x->whereNull('starts_at')->orWhere('starts_at', '<=', now()));
    }
}
```

### 3.1 Atributos e casts

| Atributo | Tipo | Origem legada | Observação |
|---|---|---|---|
| `title` | string(120) | `contents.title` | Regra `required|max:120` mantida. |
| `video_url` | string(200) | `links.link` | Valor persistido. |
| `video_id` | string(32) nullable | derivado | ID do YouTube extraído na gravação. |
| `channel` | enum | derivado do host | `youtube`, `vimeo`, `custom`. |
| `is_live` | bool | **novo** | Distingue "no ar agora" de "gravado". |
| `starts_at` / `ends_at` | datetime nullable | **novo** | Agenda da sessão; habilita "ao vivo" automático. |
| `status` | bool | `contents.status` | Semântica de ativo/inativo preservada. |

## 4. Form Request

```php
// (ilustrativo) app/Http/Requests/StoreTransmissionRequest.php
class StoreTransmissionRequest extends FormRequest
{
    public function authorize(): bool { return $this->user()->can('create', Transmission::class); }

    public function rules(): array
    {
        return [
            'title'     => ['required', 'string', 'max:120'],
            'video_url' => ['required', 'string', 'max:200', 'url_or_youtube_id'],
            'channel'   => ['nullable', Rule::enum(VideoChannel::class)],
            'is_live'   => ['nullable', 'boolean'],
            'starts_at' => ['nullable', 'date'],
            'ends_at'   => ['nullable', 'date', 'after_or_equal:starts_at'],
            'status'    => ['nullable', 'boolean'],
        ];
    }
}
```

Regras de validação customizadas:

| Regra | Comportamento |
|---|---|
| `url_or_youtube_id` | Aceita URL completa **ou** ID puro do YouTube (`/^[A-Za-z0-9_-]{11}$/`). |
| `after_or_equal:starts_at` | Coerência da janela de transmissão. |

Regras de domínio aplicadas em `prepareForValidation()` ou no Service:

1. **Exclusividade:** ativar uma transmissão desativa as demais
   (`Transmission::where('status', true)->update(['status' => false])` dentro de
   transação). Isso sai do controller — vira regra do **modelo/serviço**.
2. **Normalização do link:** `https://` garantido; ID puro convertido em
   `https://www.youtube.com/watch?v={id}`.
3. **Extração do ID:** regex única, com teste unitário.

## 5. Serviço de normalização (testável)

```php
// (ilustrativo) app/Services/VideoUrlResolver.php
final class VideoUrlResolver
{
    public function resolve(string $input): ResolvedVideo
    {
        // 1. ID puro -> URL de watch
        // 2. URL sem esquema -> https://
        // 3. YouTube (watch, youtu.be, embed, shorts) -> id
        // 4. Vimeo -> id
        // 5. Outros hosts -> embed direto (ou canal custom)
    }
}
```

O serviço substitui as ~35 linhas duplicadas em 6 Blades e resolve de fato o
ID cru. Pode reaproveitar, como ponto de partida, a lógica já existente em
`embedVideo()` (`v6/app/Helpers/functions.php:607`), que é mais completa.

## 6. Policy

```php
// (ilustrativo) app/Policies/TransmissionPolicy.php
class TransmissionPolicy
{
    public function viewAny(User $u): bool  { return $u->can('transmissoes.view'); }
    public function create(User $u): bool  { return $u->can('transmissoes.create'); }
    public function update(User $u, Transmission $t): bool { return $u->can('transmissoes.edit'); }
    public function delete(User $u, Transmission $t): bool { return $u->can('transmissoes.delete'); }
    public function toggleStatus(User $u, Transmission $t): bool { return $u->can('transmissoes.inactivate') || $u->can('transmissoes.restore'); }
}
```

O papel `super-admin` continua com acesso total (equivalente ao
`has_access()` atual). As permissões migram do par
`resources`/`menu_orders` legado para o gate do Laravel.

## 7. Endpoints REST / API JSON

```php
// (ilustrativo) routes/api.php
Route::middleware(['auth:sanctum', 'can:transmissoes.view'])
    ->apiResource('transmissions', TransmissionController::class)
    ->names('admin.transmissions');

Route::patch('transmissions/{transmission}/status', [TransmissionController::class, 'toggleStatus'])
    ->name('admin.transmissions.status');

// (ilustrativo) routes/web.php — endpoint público leve
Route::get('/api/transmission-atual', PublicTransmissionController::class)
    ->middleware('throttle:60,1')
    ->name('api.transmission.current');
```

| Método | URI | Descrição |
|---|---|---|
| GET | `/api/transmission-atual` | Retorna a transmissão ativa (ou `null`). Consumido por um widget JS opcional e integrações externas. |
| GET | `/api/admin/transmissions` | Listagem paginada. |
| POST | `/api/admin/transmissions` | Cria (valida exclusividade). |
| PUT | `/api/admin/transmissions/{id}` | Atualiza. |
| DELETE | `/api/admin/transmissions/{id}` | Exclui (soft delete). |
| PATCH | `/api/admin/transmissions/{id}/status` | Toggle de status. |

## 8. API Resource

```php
// (ilustrativo) app/Http/Resources/TransmissionResource.php
class TransmissionResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'        => $this->id,
            'title'     => $this->title,
            'embed_url' => $this->embed_url,     // URL pronta para <iframe>
            'is_live'   => $this->is_live,
            'starts_at' => $this->starts_at?->toIso8601String(),
            'status'    => $this->status,
        ];
    }
}
```

## 9. Views Blade modernas

### 9.1 Componente único (substitui as 6 cópias)

```blade
{{-- (ilustrativo) resources/views/components/portal/transmission-widget.blade.php --}}
@props(['module' => 'transmissoes'])

@php
    $transmission = $transmission ?? null;
@endphp

@if ($transmission)
    <div class="card mx-auto mb-3 w-100">
        <div class="card-body text-center">
            <div class="piscar bg-danger rounded">
                <small class="text-light"><i class="fas fa-video mr-1"></i> Transmissão</small>
            </div>
            <div class="mt-3 embed-responsive embed-responsive-16by9">
                <iframe class="embed-responsive-item" style="width:100%"
                        src="{{ $transmission->embed_url }}"
                        title="{{ $transmission->title }}"
                        allowfullscreen allow="autoplay; encrypted-media"></iframe>
            </div>
        </div>
    </div>
@endif
```

Uso nos temas:

```blade
{{-- (ilustrativo) --}}
<x-portal.transmission-widget :transmission="$transmission" />
```

O controller do portal injeta `$transmission` via **View Composer** (o portal já
usa `view()->composer` no `ModulosServiceProvider`), de modo que a view não
consulta o banco.

### 9.2 Telas do CMS

| Tela | Rota | Observação |
|---|---|---|
| Listagem | `/cms/transmissoes` | DataTables server-side com Livewire/Inertia ou manter Yajra. |
| Formulário | `/cms/transmissoes/create`, `/cms/transmissoes/edit/{id}` | Campo único de link com **preview** do embed e validação em tempo real. |
| Toggle | `PATCH /cms/transmissoes/{id}/status` | Botão único na linha da tabela. |

## 10. Cache, fila e storage

| Recurso | Estratégia |
|---|---|
| Transmissão ativa | `Cache::remember('transmission.current', now()->addSeconds(30), fn () => Transmission::active()->latest('starts_at')->first())`, com invalidação no `saved`/`deleted` do model. |
| Flag do módulo | Manter `rememberForever`, mas com invalidação explícita ao alternar em `/cms/modulos` (hoje o cache só expira por restart de fila/clear). |
| Embeddings | Nenhum worker necessário; o `iframe` resolve no cliente. |
| Fila | Opcional: notificação de "transmissão iniciada" (e-mail/WhatsApp) quando `is_live` muda. |
| Storage | Nenhum upload associado. |

## 11. Riscos de migração

| Risco | Mitigação |
|---|---|
| Registros legados com `links.link` em formato inesperado | Normalizar no script de migração e registrar os rejeitados. |
| Múltiplas transmissões ativas no legado (bug A4) | Escolher a mais recente na migração e desativar as demais. |
| Módulos desligados no legado | Preservar `modulos.status` como chave de feature flag na v7 (`Module::enabled('transmissoes')`). |
| Dependência implícita da categoria 36 | Após a migração, o módulo passa a depender de `transmissions`, não de `category_contents`. |

## 12. Ordem sugerida de implementação

1. Migration + model + factory + seeder.
2. `VideoUrlResolver` + testes unitários (cobre o bug do ID cru).
3. Service de transmissão ativa + cache.
4. Policy + Form Requests.
5. Controller REST + `TransmissionResource`.
6. Telas do CMS (listagem + formulário + toggle).
7. Componente Blade único + View Composer + substitution nos 6 temas.
8. Testes feature (CMS) e view (portal).
9. Script de migração dos dados legados + conferência.
