# VIAP — Migração para a v7 (PHP 8.x + Laravel 11/12)

> Plano de reconstrução do módulo VIAP. Todo o código deste arquivo é
> **ilustrativo** (`// exemplo ilustrativo`) e deve ser adaptado aos padrões
> (DTOs, `Actions`, `Resources`) adotados na v7.

---

## 1. Schema de migração

### 1.1 Tabela `viap`

```php
// database/migrations/2026_05_25_112000_create_viap_table.php  (exemplo ilustrativo)
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
    public function up(): void
    {
        Schema::create('viap', function (Blueprint $table) {
            $table->id();
            $table->timestamp('publication_date')->nullable();
            $table->decimal('amount', 15, 2)->nullable();      // <- campo morto na v6 (Gap G-02)
            $table->unsignedSmallInteger('reference_month')->nullable();
            $table->unsignedSmallInteger('reference_year')->nullable();
            $table->text('description')->nullable();           // <- campo morto na v6 (Gap G-03)
            $table->string('link_type', 32);                   // gabinete | outro_orgao
            $table->foreignId('councilman_id')->nullable()
                  ->constrained('councilmen')->nullOnDelete();
            $table->foreignId('organ_id')->nullable()
                  ->constrained('contents')->nullOnDelete();
            $table->boolean('status')->default(true);
            $table->string('slug')->nullable();
            $table->timestamps();
            $table->softDeletes();
            $table->index(['status', 'publication_date']);
            $table->index(['reference_year', 'reference_month']);
        });
    }
    public function down(): void
    {
        Schema::dropIfExists('viap');
    }
};
```

**Diferenças em relação à v6 que precisam de decisão:**

| Aspecto | v6 | v7 proposto | Observação |
|---|---|---|---|
| FK `councilman_id` / `organ_id` | `ON DELETE CASCADE` | `nullOnDelete()` | Na v6, apagar um parlamentar/órgão **apaga os lançamentos**; na v7 é mais seguro desvincular |
| `slug` | sem índice | índice (e `unique` se a decisão Q-02 for "slug estável") | Ver `08-GAPS-E-DIVIDAS.md` |
| `link_type` | `varchar(191)` livre | `string` + **check constraint** ou **enum** | PostgreSQL: `CHECK (link_type IN ('gabinete','outro_orgao'))` |
| `amount` / `description` | nullable, nunca preenchidos | depends de Q-01 | Manter para não perder dado histórico |
| `view_viap` | criada por migration | **não portar** | Substituir por query builder/Eloquent |

### 1.2 Tabela pivot `viap_has_archives`

```php
// database/migrations/2026_05_25_112100_create_viap_has_archives_table.php  (exemplo ilustrativo)
Schema::create('viap_has_archives', function (Blueprint $table) {
    $table->id();
    $table->foreignId('viap_id')->constrained('viap')->cascadeOnDelete();
    $table->foreignId('archive_id')->constrained('archives')->cascadeOnDelete();
    $table->unique(['viap_id', 'archive_id']);   // <- evita duplicidade (a v6 não tinha)
});
```

### 1.3 Dados do módulo (`modulos`, `resources`, `menu_orders`)

Na v7, o registro do módulo deve sair de um **seeder**, igual à v6, mas com a
mesma lição aplicada (ver Q-03 em `08-GAPS-E-DIVIDAS.md`): ou o registro passa
para uma migration, ou o `db:seed` passa a ser obrigatório no deploy.

```php
// database/seeders/ViapModuleSeeder.php  (exemplo ilustrativo)
DB::table('modulos')->updateOrCreate(
    ['slug' => 'viap'],
    ['name' => 'VIAP', 'status' => 1, 'updated_at' => now(), 'created_at' => now()]
);
```

### 1.4 Migration de dados (v6 → v7)

```php
// database/migrations/..._migrate_viap_data.php  (exemplo ilustrativo)
DB::statement('INSERT INTO viap (id, publication_date, amount, reference_month,
    reference_year, description, link_type, councilman_id, organ_id, status, slug,
    created_at, updated_at, deleted_at)
    SELECT id, publication_date, amount, reference_month, reference_year, description,
           link_type, councilman_id, organ_id, status, slug, created_at, updated_at, deleted_at
    FROM viap ON CONFLICT (id) DO NOTHING');
// idem para o pivot viap_has_archives
```

No banco de análise as tabelas estão **vazias** (`count(*) = 0`), portanto a
migração de dados é trivial — mas o script deve ser idempotente para
instalações com dados reais.

## 2. Modelos

```php
// app/Models/Viap.php  (exemplo ilustrativo)
namespace App\Models;
use App\Models\Concerns\BelongsToCouncilman;   // conforme padrão do projeto
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class Viap extends Model
{
    use HasFactory, SoftDeletes;
    public const LINK_GABINETE = 'gabinete';
    public const LINK_OUTRO_ORGAO = 'outro_orgao';
    protected $fillable = [
        'publication_date',
        'reference_month',
        'reference_year',
        'link_type',
        'councilman_id',
        'organ_id',
        'status',
        'slug',
        'amount',        // <- incluir após decidir Q-01
        'description',   // <- incluir após decidir Q-01
    ];
    protected $casts = [
        'publication_date' => 'datetime',
        'amount'           => 'decimal:2',
        'reference_month'  => 'integer',
        'reference_year'   => 'integer',
        'status'           => 'boolean',
    ];
    protected function councilman(): BelongsTo
    {
        return $this->belongsTo(Councilman::class);
    }
    protected function organ(): BelongsTo
    {
        return $this->belongsTo(Content::class, 'organ_id');
    }
    protected function archives(): BelongsToMany
    {
        return $this->belongsToMany(Archive::class, 'viap_has_archives');
    }
    public function scopeActive(Builder $query): Builder
    {
        return $query->where('status', true);
    }
    public function scopeFiltros(Builder $query, array $filtros): Builder
    {
        return $query
            ->when($filtros['link_type'] ?? null, fn (Builder $q) => $q->where('link_type', $filtros['link_type']))
            ->when($filtros['councilman_id'] ?? null, fn (Builder $q) => $q->where('councilman_id', $filtros['councilman_id']))
            ->when($filtros['organ_id'] ?? null, fn (Builder $q) => $q->where('organ_id', $filtros['organ_id']))
            ->when($filtros['month'] ?? null, fn (Builder $q) => $q->where('reference_month', $filtros['month']))
            ->when($filtros['year'] ?? null, fn (Builder $q) => $q->where('reference_year', $filtros['year']));
    }
    public function getLinkedNameAttribute(): ?string
    {
        return match ($this->link_type) {
            self::LINK_GABINETE   => $this->councilman?->name,
            self::LINK_OUTRO_ORGAO => $this->organ?->title,
            default               => null,
        };
    }
    public function getDisplayTitleAttribute(): string
    {
        $mes = self::referenceMonths()[$this->reference_month] ?? $this->reference_month;
        return $this->link_type === self::LINK_GABINETE
            ? "VIAP {$mes} {$this->reference_year} Gabinete " . ($this->councilman?->name ?? 'Gabinete')
            : "VIAP {$mes} {$this->reference_year} Órgão " . ($this->organ?->title ?? 'Órgão');
    }
    public static function referenceMonths(): array
    {
        return [
            1 => 'Janeiro', 2 => 'Fevereiro', 3 => 'Março', 4 => 'Abril',
            5 => 'Maio', 6 => 'Junho', 7 => 'Julho', 8 => 'Agosto',
            9 => 'Setembro', 10 => 'Outubro', 11 => 'Novembro', 12 => 'Dezembro',
        ];
    }
}
```

Observações:

- `linked_name` (que a view SQL produzia) vira um **accessor** — dispensa a view.
- `referenceMonths()` pode virar um **Enum** (`MonthReference`) com `->label()`;
  `link_type` pode virar **Enum** (`LinkType: string`) com `Gabinete` e `OutroOrgao`.
- O accessor `display_title` preserva a saída observável no portal.

## 3. Form Requests

```php
// app/Http/Requests/CMS/StoreViapRequest.php  (exemplo ilustrativo)
namespace App\Http\Requests\CMS;
use App\Enums\LinkType;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreViapRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()?->can('create', \App\Models\Viap::class) ?? false;
    }
    public function rules(): array
    {
        return [
            'publication_date' => ['required', 'date'],
            'reference_month'  => ['required', 'integer', 'between:1,12'],
            'reference_year'   => ['required', 'integer', 'min:1900'],
            'link_type'        => ['required', Rule::in(LinkType::values())],
            'councilman_id'    => ['required_if:link_type,gabinete', 'nullable', 'exists:councilmen,id'],
            'organ_id'         => ['required_if:link_type,outro_orgao', 'nullable', 'exists:contents,id'],
            'amount'           => ['nullable', 'numeric', 'min:0'],       // se Q-01 for "preencher"
            'description'      => ['nullable', 'string', 'max:5000'],    // se Q-01 for "preencher"
            'status'           => ['sometimes', 'boolean'],
            'archive'          => ['sometimes', 'array'],
            'archive.*'        => ['file', 'max:20480'],                 // 20 MB, igual ao legado?
        ];
    }
    public function messages(): array
    {
        return [
            'publication_date.required' => 'O campo data de publicação é obrigatório.',
            'reference_month.required'  => 'O campo mês de referência é obrigatório.',
            'reference_year.required'   => 'O campo ano de referência é obrigatório.',
            'link_type.required'        => 'O campo tipo de vínculo é obrigatório.',
            'councilman_id.required_if' => 'O campo parlamentar é obrigatório quando o vínculo é Gabinete.',
            'organ_id.required_if'      => 'O campo órgão é obrigatório quando o vínculo é Outros Órgãos.',
        ];
    }
}
```

`UpdateViapRequest` idêntico, com `authorize()` usando
`$this->user()->can('update', $this->route('viap'))`.

Mudanças em relação à v6: `authorize()` **não** retorna mais `true` fixo; há
`exists:` para as FKs; `archive.*` é validado como arquivo.

## 4. Policies / autorização

A v6 resolve permissão pelo **nome da rota**; a v7 deve usar Policies.

```php
// app/Policies/ViapPolicy.php  (exemplo ilustrativo)
namespace App\Policies;
use App\Models\Viap;
use App\Models\User;
class ViapPolicy
{
    public function viewAny(User $user): bool { return $user->can('viap.index'); }
    public function view(User $user, Viap $viap): bool { return $user->can('viap.show'); }
    public function create(User $user): bool { return $user->can('viap.create'); }
    public function update(User $user, Viap $viap): bool { return $user->can('viap.edit'); }
    public function delete(User $user, Viap $viap): bool { return $user->can('viap.delete'); }
    public function deleteAny(User $user): bool
    {
        return $user->can('viap.delete');
    }
}
```

Regras de equivalência:

| Operação legada | Permissão legada | Nova permissão |
|---|---|---|
| `GET /cms/viap` | `viap.index` | `viap.index` |
| `GET /cms/viap/create` | `viap.create` | `viap.create` |
| `POST /cms/viap` | (aberta na v6!) | `viap.create` |
| `PUT /cms/viap/{id}` | (aberta na v6!) | `viap.edit` |
| `DELETE /cms/viap/{id}` | (aberta na v6!) | `viap.delete` |
| `POST /cms/viap/delete-por-lote` | `viap.delete-por-lote` / `delete-por-lote.index` (nunca concedida) | `viap.delete` |
| `POST /cms/viap/ajax-delete-files` | (aberta na v6) | `viap.edit` (ou `viap.delete`) |
| `GET /cms/datatable/viap` | (aberta na v6) | `viap.index` |

> **Restrição de equivalência:** fechar as rotas de escrita ( Gap G-05 ) muda o
> comportamento observável para o operador. É a correção recomendada, mas
> precisa ser combinada com o negócio — quem tinha acesso "de facto" passa a
> precisar da permissão.

O `Gate::before` para `super-admin` deve ser mantido.

## 5. Endpoints REST/JSON sugeridos

### 5.1 CMS (mantendo o padrão do portal legado)

```php
// routes/cms.php  (exemplo ilustrativo)
Route::middleware(['auth', 'can:viewAny,' . Viap::class])->group(function () {
    Route::get('viap', [ViapController::class, 'index'])->name('viap.index');
    Route::get('viap/create', [ViapController::class, 'create'])->name('viap.create');
    Route::post('viap', [ViapController::class, 'store'])->name('viap.store');
    Route::get('viap/{viap}', [ViapController::class, 'show'])->name('viap.show');   // <- corrigir Gap G-04
    Route::get('viap/{viap}/edit', [ViapController::class, 'edit'])->name('viap.edit');
    Route::put('viap/{viap}', [ViapController::class, 'update'])->name('viap.update');
    Route::delete('viap/{viap}', [ViapController::class, 'destroy'])->name('viap.destroy');
    Route::post('viap/delete-por-lote', [ViapController::class, 'destroyBulk'])->name('viap.delete-por-lote');
    Route::post('viap/ajax-delete-files', [ViapController::class, 'destroyArchive'])->name('viap.ajax.delete.files');
    Route::get('datatable/viap', [ViapDataTableController::class, '__invoke'])->name('datatable.viap');
});
```

### 5.2 API pública (JSON) — nova, opcional

```
GET  /api/v1/viap?link_type=&councilman_id=&organ_id=&month=&year=&page=
GET  /api/v1/viap/{slug}
GET  /api/v1/viap/export?type=xls|pdf|csv|txt&<mesmo conjunto de filtros>
```

### 5.3 Portal (público, HTML): `GET /viap`, `GET /viap/{slug}`, `GET /viap/export?type=…`

## 6. API Resource

```php
// app/Http/Resources/ViapResource.php  (exemplo ilustrativo)
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ViapResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'                => $this->id,
            'publication_date'  => $this->publication_date?->toDateString(),
            'amount'            => $this->amount !== null ? (float) $this->amount : null,
            'reference_month'   => $this->reference_month,
            'reference_year'    => $this->reference_year,
            'description'       => $this->description,
            'link_type'         => $this->link_type,
            'linked_name'       => $this->linked_name,   // reproduz a coluna da view SQL
            'status'            => (bool) $this->status,
            'slug'              => $this->slug,
            'councilman'        => $this->whenLoaded('councilman', fn () => [
                'id'   => $this->councilman->id,
                'name' => $this->councilman->name,
            ]),
            'organ'             => $this->whenLoaded('organ', fn () => [
                'id'    => $this->organ->id,
                'title' => $this->organ->title,
            ]),
            'archives'          => ArchiveResource::collection($this->whenLoaded('archives')),
        ];
    }
}
```

## 7. Views Blade modernas

| Legado | v7 |
|---|---|
| `prefecture/viap/index.blade.php` + `datatable.js` | `<x-cms.viap.index>` com `<x-datatable>` (server-side, `perPage=15`) |
| `prefecture/viap/form.blade.php` (bloco gigante) | `<x-cms.viap.form :viap="$viap" />` com componentes `<x-form.date>`, `<x-form.select>`, `<x-form.upload-multiple>` |
| `portal/layouts/modelo1..6/viap/*` (12 cópias) | **um** par de views (`portal/viap/index.blade.php` e `show.blade.php`) incluídas por um `<x-portal.layout>` parametrizado pelo template ativo — elimina as 12 cópias |
| `portal/viap/*` (órfãs) | remover |
| `public/assets/js/custom/contents/viap/datatable.js` | componente Alpine/Livewire ou DataTables server-side com Yajra |
| `public/assets/js/modulos/jspdf/gerar-pdf-viap.js` | remover (export server-side já resolve) |

Componentes sugeridos: `<x-viaps.filters>` (filtros do portal),
`<x-viaps.table>` (tabela com paginação server-side), `<x-viaps.card>` (linha do
lançamento) e `<x-viaps.documents>` (lista de anexos).

## 8. Fila, cache e storage

| Aspecto | v6 | v7 |
|---|---|---|
| Armazenamento de arquivos | `storage/` local, via `FileTrait::saveUpload()` | `Storage::disk('public')` (ou S3), com `->store('viap/{id}/arquivos')` |
| Fila | não usada | opcional: processar/thumbnail e **enviar e-mail** ao aprovar um lançamento |
| Cache | não usado | cachear a lista de órgãos/parlamentares (`Cache::remember`, 10 min) e o layout ativo |
| Logs | `spatie/laravel-activitylog` + `Log::error` | `spatie/laravel-activitylog` v4 + `Log::channel('viap')` |
| Events | nenhum | `ViapCreated`, `ViapUpdated`, `ViapDeleted` (permite Reaction, e-mail, auditoria) |

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

| # | Risco | Mitigação |
|---|---|---|
| R1 | Fechar as rotas de escrita quebra operadores que hoje escrevem sem permissão específica | Migrar as permissões `viap.*` para os papéis corretos **antes** de ativar a Policy; documentar a mudança |
| R2 | Migrar `view_viap` para accessor pode alterar o valor de `linked_name` quando o registro está sem vínculo | Manter o `CASE` original na semântica do accessor (`null` quando o vínculo não resolve) |
| R3 | Trocar `ON DELETE CASCADE` por `nullOnDelete` muda o comportamento ao apagar parlamentar/órgão | Decidir com o negócio; documentar |
| R4 | Preencher `amount`/`description` muda o dado exibido no portal (hoje `R$ 0,00`) | Decidir Q-01; se preencher, sanitizar o HTML do Summernote (XSS) |
| R5 | Slug único pode colidir (mês/ano + mesmo parlamentar) | Índice unique + sufixo (`-2`) ou UUID; e decidir se o slug deve ser **estável** (não regerar no update) |
| R6 | Publicação por `status` vs por `publication_date` | Manter `status` como chave de publicabilidade (R-01) |
| R7 | Registro do módulo só por seed | Tornar o seed obrigatório no pipeline de deploy **ou** mover para migration (Q-03) |
| R8 | Remover as 12 cópias de view pode quebrar temas que referenciem `modelo4`/`modelo5` com layout específico | Manter o componente de layout parametrizado e testar os 6 modelos |
| R9 | Migração de dados sem transação entre tabelas | Migrar em transação e com `ON CONFLICT DO NOTHING` para reexecução |
