# Pauta do Dia — Migração para Laravel Moderno (v7)

> **Slug:** `pauta-do-dia` · **Módulo:** `modulos.id = 42` · **Novo na v6**
> Alvo: PHP 8.x + Laravel 11/12. Este documento é o plano de reconstrução
> do módulo, não uma implementação.

---

## 1. Diagnóstico: o que precisa mudar

| Problema na v6 | Consequência | Solução proposta na v7 |
|---|---|---|
| Não há entidade própria: tudo vive em `contents` + `category_contents`. | Slug semântico implícito; listagem pública e listagem do CMS usam fontes diferentes (tabela x view SQL). | Tabela e model `pautas_do_dia` próprios. |
| O número da sessão é **extraído do título** por `regexp_replace`. | Ordenação depende de digitação; títulos com ano quebram a ordem. | Campo `session_number` (inteiro) explícito. |
| `orderByRaw` com SQL exclusivo do PostgreSQL. | Quebra em qualquer outro motor. | Ordenação Eloquent por colunas indexadas. |
| View SQL `pauta_do_dia` como fonte do CMS. | Colunas textuais (`data_e_hora`), `row_number()` enganoso, carga total em memória. | Query Eloquent paginada no servidor. |
| Ordenação implementada duas vezes (PHP e JS). | Divergência silenciosa entre portal e CMS. | Ordenação **apenas no servidor**; o DataTable só solicita a ordem. |
| `status` não filtra a publicação. | Pauta inativa visível ao cidadão. | `status` como enum e filtro aplicado nas listagens. |
| `paginate()` sem argumento. | Comportamento implícito. | `perPage` explícito e configurável. |

## 2. Schema da migration Laravel

```php
// exemplo ilustrativo
Schema::create('pautas_do_dia', function (Blueprint $table) {
    $table->id();
    $table->string('title', 191);
    $table->string('slug', 191)->unique();
    $table->unsignedInteger('session_number')->nullable();
    $table->date('session_date')->nullable();
    $table->timestamp('published_at')->nullable();
    $table->longText('body')->nullable();
    $table->string('summary', 300)->nullable();
    $table->string('author', 191)->nullable();
    $table->string('source', 191)->nullable();
    $table->string('status', 20)->default('draft');
    $table->string('highlight_path')->nullable();
    $table->foreignId('published_by')->nullable()->constrained('users');
    $table->timestamps();
    $table->softDeletes();

    $table->index(['session_date', 'session_number'], 'pautas_ordem_idx');
    $table->index('status');
});
```

| Campo | Tipo | Origem na v6 | Observação |
|---|---|---|---|
| `id` | bigint | `contents.id` | — |
| `title` | string(191) | `contents.title` | Texto livre; exibido no portal e no CMS. |
| `slug` | string(191) único | `contents.slug` | Link permanente `/pauta-do-dia/{slug}`. |
| `session_number` | unsigned int, nullable | **novo** (extraído do título na v6) | Número da sessão (ex.: 19). |
| `session_date` | date, nullable | derivado de `contents.publication_date` | Dia da sessão; base da ordenação. |
| `published_at` | timestamp, nullable | `contents.publication_date` | Data **e hora** de publicação (mostrada no detalhe). |
| `body` | longText | `contents.text` | HTML do editor rico. |
| `summary` | string(300), nullable | **novo** | Substitui o `str_limit(..., 200)` calculado em tempo de renderização. |
| `author` / `source` | string(191) | `contents.author` / `contents.source` | Exibidos no detalhe público. |
| `status` | string(20) | `contents.status` (boolean) | `draft` \| `published` \| `archived`. |
| `highlight_path` | string | `archives.path` (com `highlight = 1`) | Imagem de destaque. |
| `published_by` | FK users | — | Rastreabilidade. |

### Tabela de anexos

```php
// exemplo ilustrativo
Schema::create('pauta_do_dia_attachments', function (Blueprint $table) {
    $table->id();
    $table->foreignId('pauta_do_dia_id')->constrained()->cascadeOnDelete();
    $table->string('name');
    $table->string('path');
    $table->string('mime_type', 191)->nullable();
    $table->string('extension', 191)->nullable();
    $table->string('description')->nullable();
    $table->unsignedInteger('size')->nullable();
    $table->boolean('is_highlight')->default(false);
    $table->boolean('is_embeddable')->default(false); // substitui view_pdf
    $table->unsignedInteger('order')->default(0);
    $table->timestamps();
});
```

> Se a v7 mantiver a tabela `archives` (recomendado, por ser compartilhada
> com Notícias, Requerimentos, Transmissões, etc.), a v7 pode reaproveitá-la e
> apenas criar a FK `pauta_do_dia_id`. A decisão é de arquitetura da v7.

## 3. Model

```php
// exemplo ilustrativo
namespace App\Models;

enum PautaStatus: string
{
    case Draft = 'draft';
    case Published = 'published';
    case Archived = 'archived';
}

class PautaDoDia extends Model
{
    use HasFactory, SoftDeletes;

    protected $fillable = [
        'title', 'slug', 'session_number', 'session_date',
        'published_at', 'body', 'summary', 'author', 'source', 'status',
    ];

    protected $casts = [
        'session_date' => 'date',
        'published_at' => 'datetime',
        'status' => PautaStatus::class,
    ];

    public function attachments(): HasMany
    {
        return $this->hasMany(PautaDoDiaAttachment::class);
    }

    public function highlight(): HasOne
    {
        return $this->hasOne(PautaDoDiaAttachment::class)->where('is_highlight', true);
    }

    public function embeddable(): HasOne
    {
        return $this->hasOne(PautaDoDiaAttachment::class)->where('is_embeddable', true);
    }

    /** Ordenação canônica: data da sessão DESC, número da sessão DESC, id DESC. */
    public function scopeOrdenada(Builder $query): Builder
    {
        return $query
            ->orderByDesc('session_date')
            ->orderByDesc('session_number')
            ->orderByDesc('id');
    }
}
```

Pontos de atenção:

- `session_number` é **nullable**: pautas que não são de sessão (pauta
  extraordinária sem número, por exemplo) continuam ordenando — os nulos
  ficam por último dentro do dia.
- O `scopeOrdenada` é a **única** fonte de ordenação; o portal, o CMS e a
  API usam o mesmo escopo (elimina a duplicação PHP/JS).
- Se o negócio exigir ordenação por *data e hora* em vez de *data*, a
  mudança é uma linha — mas **mudaria a semântica observável** (ver seção 8).

## 4. Form Request

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

    public function rules(): array
    {
        return [
            'title' => ['required', 'string', 'max:120'],
            'slug' => ['nullable', 'string', 'max:191', 'alpha_dash', Rule::unique('pautas_do_dia')->ignore($this->route('pauta'))],
            'session_number' => ['required', 'integer', 'min:1', 'max:9999'],
            'session_date' => ['required', 'date'],
            'published_at' => ['nullable', 'date'],
            'body' => ['required', 'string'],
            'summary' => ['nullable', 'string', 'max:300'],
            'author' => ['nullable', 'string', 'max:191'],
            'source' => ['nullable', 'string', 'max:191'],
            'status' => ['required', Rule::enum(PautaStatus::class)],
            'highlight' => ['nullable', 'image', 'max:5120'],
            'attachments' => ['nullable', 'array', 'max:20'],
            'attachments.*' => ['file', 'mimes:jpg,jpeg,png,bmp,gif,rar,zip,doc,docx,pdf,odt,xls,xlsx,xlsb,xslm,ppt,pptx,ods,wav,mp3,wma,mp4,mpg4,mkv,flv'],
        ];
    }
}
```

Regras que **mudam** em relação à v6:

| Regra | v6 | v7 | Motivo |
|---|---|---|---|
| `title` | `required|max:120` | `required|string|max:120` | Mantida; eliminar a divergência com o `maxlen` 150 do formulário. |
| Número da sessão | (não existia) | `required|integer|min:1|max:9999` | Elimina a dependência do texto do título. |
| Data | `publication_date` `required` (datetime) | `session_date` `required|date` + `published_at` opcional | Separa o **dia da sessão** do **momento de publicação**. |
| Anexo em destaque | `image|max:5000` | `image|max:5120` (5 MB em KB) | Mesma intenção, unidade explícita. |
| Estado | (ausente no formulário) | `status` obrigatório com enum | Publicação passa a ser explícita. |

## 5. Policy e autorização

```php
// exemplo ilustrativo
class PautaDoDiaPolicy
{
    public function viewAny(User $user): bool { return $user->can('pauta-do-dia.index'); }
    public function create(User $user): bool { return $user->can('pauta-do-dia.create'); }
    public function update(User $user, PautaDoDia $pauta): bool { return $user->can('pauta-do-dia.edit'); }
    public function delete(User $user, PautaDoDia $pauta): bool { return $user->can('pauta-do-dia.delete'); }
    public function publish(User $user, PautaDoDia $pauta): bool { return $user->can('pauta-do-dia.publish'); }
}
```

| Permissão | Equivalente v6 | Uso na v7 |
|---|---|---|
| `pauta-do-dia.index` | derivado pelo middleware | Ver listagem no CMS. |
| `pauta-do-dia.create` | `has_access(['pauta-do-dia.create'])` | Ver o botão "Cadastrar Pauta do Dia". |
| `pauta-do-dia.edit` | flag `edit` do `struct_views` | Ver o botão "Editar". |
| `pauta-do-dia.delete` | flag `delete` do `struct_views` | Ver o botão "Excluir" (individual e em lote). |
| `pauta-do-dia.publish` | inexistente | Publicar/despublicar (novo, substitui o `status` booleano). |

O `super-admin` deve continuar passando por todas (o `has_access()` da v6 já
faz isso; na v7 isso é feito por Gate `before` ou por middleware).

## 6. Rotas REST e API Resource

```php
// exemplo ilustrativo
Route::middleware(['auth', 'can:pauta-do-dia.index'])
    ->prefix('api/v1/pautas-do-dia')
    ->name('api.pautas-do-dia.')
    ->group(function () {
        Route::get('/', [PautaDoDiaController::class, 'index'])->name('index');
        Route::post('/', [PautaDoDiaController::class, 'store'])->name('store');
        Route::get('/{pauta}', [PautaDoDiaController::class, 'show'])->name('show');
        Route::put('/{pauta}', [PautaDoDiaController::class, 'update'])->name('update');
        Route::delete('/{pauta}', [PautaDoDiaController::class, 'destroy'])->name('destroy');
        Route::post('/{pauta}/publish', [PautaDoDiaController::class, 'publish'])->name('publish');
    });
```

| Endpoint | Retorno | Observação |
|---|---|---|
| `GET /api/v1/pautas-do-dia` | JSON paginado, ordenado por `scopeOrdenada` | Suporta `?session_date=`, `?session_number=`, `?year=`. |
| `GET /api/v1/pautas-do-dia/{id}` | JSON do registro | — |
| `POST /api/v1/pautas-do-dia` | 201 + JSON | Valida com o Form Request. |
| `PUT /api/v1/pautas-do-dia/{id}` | 200 + JSON | — |
| `DELETE /api/v1/pautas-do-dia/{id}` | 204 | Soft delete. |
| `POST /api/v1/pautas-do-dia/{id}/publish` | 200 + JSON | Alterna `status`. |

Resource:

```php
// exemplo ilustrativo
class PautaDoDiaResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'slug' => $this->slug,
            'session_number' => $this->session_number,
            'session_date' => $this->session_date?->toDateString(),
            'published_at' => $this->published_at?->toIso8601String(),
            'summary' => $this->summary,
            'body' => $this->body,
            'author' => $this->author,
            'source' => $this->source,
            'status' => $this->status->value,
            'highlight' => $this->highlight?->url,
            'attachments' => PautaDoDiaAttachmentResource::collection($this->attachments),
            'links' => [
                'public' => route('pauta-do-dia.show', $this->slug),
                'api' => route('api.pautas-do-dia.show', $this->id),
            ],
        ];
    }
}
```

## 7. Views e componentes

| Tela v6 | Proposta v7 |
|---|---|
| `portal/layouts/modelo1/pauta-do-dia/index.blade.php` (6 cópias) | **Uma** view + `@include` de um componente `<x-pauta.lista :pautas="$pautas" />`; os temas passam a variar apenas o layout. |
| `portal/layouts/modelo1/pauta-do-dia/show.blade.php` (6 cópias) | Componente `<x-pauta.detalhe :pauta="$pauta" />`. |
| `portal/components/view_pdf/files-list.blade.php` | Componente Blade `<x-arquivos.lista :arquivos="$anexos" :embeddable="$embed" />`; verificação com `Storage::exists()`. |
| `prefecture/contents/pauta-do-dia/index.blade.php` | Tela dedicada `pautas/index.blade.php` com Livewire ou tabela server-side; ordenação enviada ao backend. |
| `prefecture/contents/pauta-do-dia/form.blade.php` | Formulário com `<x-form.pauta>`; o campo "Número da sessão" passa a ser explícito. |
| JS `datatables.js` com `converterDataPauta` | **Eliminar**: ordenação no servidor, sem chave artificial em JavaScript. |

Componente de resumo (elimina o `str_limit` em tempo de renderização):

```blade
{{-- exemplo ilustrativo --}}
<p class="small content-pub">{{ Str::limit(strip_tags($pauta->body), 200) }}</p>
```

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

### 8.1 O que **não pode** mudar

| Semântica | Regra a preservar | Como verificar |
|---|---|---|
| Ordem da listagem pública | Dia da sessão decrescente; dentro do dia, **número da sessão decrescente** (19 antes de 18); desempate por id decrescente. | Teste de feature com fixtures. |
| Coluna "Data e Hora" do CMS | Continua ordenando pela data (a chave artificial desaparece porque o backend passa a ordenar). | Teste do endpoint do DataTable. |
| URL pública | `/pauta-do-dia` e `/pauta-do-dia/{slug}` (o slug continua editável e único). | Teste de rota. |
| Comportamento do detalhe | Autor, fonte, data **e hora**, imagem de destaque, texto, PDF embutido (desktop) e anexos para download. | Teste de feature. |
| Slug inexistente | 404. | Teste de rota. |
| 15 itens por página (ou o novo valor decidido) | Definir explicitamente e documentar. | Teste de paginação. |

### 8.2 Decisões que exigem o negócio

| # | Decisão | Impacto |
|---|---|---|
| D1 | O número da sessão é obrigatório? | Pauta "extraordinária" sem número ficaria sem ordenação dentro do dia. |
| D2 | A ordenação numérica é decrescente (como na v6) ou crescente? | **A v6 é decrescente**; mudar isso altera o que o cidadão vê. |
| D3 | Pauta "não publicada" deve sumir do portal? | Na v6 ela aparece; a v7 deve filtrar por `status`. |
| D4 | O que é exibido na coluna "ID" do CMS? | Na v6 é um `row_number()` enganoso; a v7 deve mostrar o id real. |
| D5 | A coluna "ID" é removida ou mantida? | Afeta a exportação Excel/PDF. |
| D6 | Manter o módulo genérico de `contents` ou isolar? | Isolar é mais limpo, mas exige duplicar CRUD, anexos e auditoria. |
| D7 | O PDF embutido continua sendo exibido no celular? | Na v6, não. |
| D8 | Valor padrão de itens por página. | 15 na v6 (padrão do framework). |

### 8.3 Riscos de migração de dados

| Risco | Mitigação |
|---|---|
| Conteúdos existentes na categoria `pauta-do-dia` (hoje 0 no banco de referência) precisam ser migrados para `pautas_do_dia`. | Script de migração idempotente, lendo `contents` + `category_contents` e extraindo `session_number` do título **uma única vez**, com relatório dos títulos ambíguos para revisão manual. |
| A extração automática pode errar (ano no título). | Gerar um relatório de pendências e exigir confirmação do operador. |
| Slugs em uso podem colidir com a unicidade nova. | Reutilizar o `contents.slug`; em caso de colisão, sufixar e registrar no relatório. |
| Anexos em `archives` (com `contents_id`) precisam ser reassociados. | Migrar por `contents_id` → `pauta_do_dia_id`, preservando `highlight` e `view_pdf`. |
| Links públicos já publicados (`/pauta-do-dia/{slug}`) não podem quebrar. | Manter a mesma estrutura de rota e o mesmo `slug`. |
| Redirecionamentos do CMS (`/cms/conteudos/pauta-do-dia`) mudam. | Criar rota de redirecionamento para a nova tela ou manter o alias. |

## 9. Fila, cache e storage

| Aspecto | Proposta |
|---|---|
| Storage | `Storage::disk('public')->putFile("pauta-do-dia/{id}", $file)`; servir via rota de download assinada ou `Storage::url()`. |
| Fila | Processamento assíncrono de imagens (geração de `webp`, como no portal da v6) e geração de `summary` automático. |
| Cache | `Cache::remember("pauta-do-dia.list.{$page}", 300, ...)` para a listagem pública; invalidar ao publicar/excluir. |
| Indexação | Índice `(session_date, session_number)` atende à ordenação canônica. |
| PDF | Se houver geração de PDF da pauta, usar fila e storage de arquivos, nunca renderização síncrona. |
