# Obras Paralisadas — Migração para Laravel Moderno

> Plano de reconstrução do módulo (existente apenas na v5) em **PHP 8.x +
> Laravel 11/12**.

---

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

| # | Comportamento a preservar | Justificativa |
|---|---|---|
| E1 | Os quatro campos de texto são obrigatórios; anexos são opcionais. | Regra de negócio vigente no request da v5. |
| E2 | Somente PDF, até 10 MB por arquivo. | Restrição legal/operacional do órgão. |
| E3 | Múltiplos anexos por registro, cada um com descrição livre. | Usado pelo operador para identificar documentos. |
| E4 | Exclusão de registro é lógica (soft delete) e o registro some do portal. | Preserva histórico e evita perda de dados públicos já linkados. |
| E5 | A listagem pública é ordenada por `identificacao` e paginada. | Já publicado no portal. |
| E6 | A busca cobre `identificacao` **ou** `motivo_paralisacao`. | Já publicado no portal. |
| E7 | Exportação em xls, csv, txt e pdf com as mesmas 4 colunas. | Já publicado no portal. |
| E8 | Anexos são baixáveis pelo cidadão na ficha da obra. | Já publicado no portal. |
| E9 | A URL da ficha pública é estável por obra. | Links externos podem existir. **Melhorar** a implementação v5, que quebrava o link ao renomear. |
| E10 | Quem não tem a permissão de escrita não altera nada, nem por URL direta. | Correção da fragilidade descrita em `03-ROTAS-E-CONTROLADORES.md`. |

## 2. Schema de migração Laravel

```php
// exemplo ilustrativo — Laravel 11/12
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('obras_paralisadas', function (Blueprint $table) {
            $table->id();
            $table->string('identificacao', 255);
            $table->text('motivo_paralisacao');
            $table->string('responsavel', 255);
            $table->date('previsao_reinicio');
            $table->string('slug', 191)->unique();
            $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
            $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
            $table->timestamps();
            $table->softDeletes();

            $table->index('identificacao');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('obras_paralisadas');
    }
};
```

Notas de equivalência:

- `previsao_reinicio` muda de `varchar(255)` para `date`. Na v5 o valor
  gravado é a string `YYYY-MM-DD` vinda do `<input type="date">`, então a
  conversão é direta, mas exige uma migração de dados caso existam linhas
  fora do formato ISO.
- `slug` ganha **índice único** — na v5 a unicidade era convenção do código.
- `identificacao` ganha índice, para sustentar a ordenação e a busca.
- `created_by`/`updated_by` são novidade: o módulo v5 não guarda autoria.

### 2.1 Tabela de anexos

```php
// exemplo ilustrativo
Schema::create('obra_paralisada_archives', function (Blueprint $table) {
    $table->id();
    $table->foreignId('obra_paralisada_id')->constrained()->cascadeOnDelete();
    $table->string('path', 300);
    $table->string('name', 300);
    $table->string('mime_type', 100)->default('application/pdf');
    $table->unsignedBigInteger('size')->nullable();
    $table->string('description', 255)->nullable();
    $table->unsignedInteger('order')->default(0);
    $table->timestamps();
});
```

Vantagens sobre a coluna JSON da v5: integridade referencial, ordenação
determinística, orphan tracking, metadados, e Possibilidade de reordenar
anexos (o que a v5 não suporta — `ArchiveController@defaultOrder`/`putOrder`
tratam a tabela `archives`, que este módulo nunca usa).

Alternativa mais simples, se a equipe quiser manter o formato JSON: trocar a
coluna para `json` e criar um accessor `arquivos` que sempre devolve o array
normalizado (elimina os `json_decode` espalhados pelas views).

## 3. Modelos

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

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class ObraParalisada extends Model
{
    use HasFactory, SoftDeletes;

    protected $table = 'obras_paralisadas';

    protected $fillable = [
        'identificacao',
        'motivo_paralisacao',
        'responsavel',
        'previsao_reinicio',
    ];

    protected $casts = [
        'previsao_reinicio' => 'date',
    ];

    public function archives()
    {
        return $this->hasMany(ObraParalisadaArchive::class)->orderBy('order');
    }

    protected static function booted(): void
    {
        static::creating(function (self $obra) {
            $obra->slug = static::gerarSlug($obra->identificacao);
        });
    }

    public static function gerarSlug(string $identificacao): string
    {
        return Str::slug($identificacao) . '-' . Str::random(6);
    }
}
```

Decisões:

| Tema | Decisão | Motivo |
|---|---|---|
| Slug | Gerado **uma vez**, na criação, com sufixo aleatório; **não** é recalculado em update | Preserva a URL pública quando a identificação muda (E9). O sufixo aleatório evita colisão entre duas obras com o mesmo nome. |
| Tabela | `obras_paralisadas` (nome preservado) | Permite reexportar dados da v5 com `INSERT ... SELECT` sem renomear. |
| Casts | `previsao_reinicio => 'date'` | O tipo correto para o dado. |
| Anos/versão | `created_by`, `updated_by` | Autoria e auditoria. |
| `arquivo` (JSON) | **Removido** em favor da tabela de anexos | Elimina a coluna sem índice e sem FK. |

## 4. Form Requests

```php
// exemplo ilustrativo
namespace App\Http\Requests\Admin;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class ObraParalisadaRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('update', app(\App\Models\ObraParalisada::class));
    }

    public function rules(): array
    {
        return [
            'identificacao'      => ['required', 'string', 'max:255'],
            'motivo_paralisacao' => ['required', 'string'],
            'responsavel'        => ['required', 'string', 'max:255'],
            'previsao_reinicio'  => ['required', 'date'],
            'arquivo'            => ['nullable', 'array', 'max:20'],
            'arquivo.*'          => ['file', 'mimes:pdf', 'max:10240'],
            'arquivo_desc'       => ['nullable', 'array'],
            'arquivo_desc.*'     => ['nullable', 'string', 'max:255'],
        ];
    }

    public function messages(): array
    {
        return [
            'identificacao.required'      => 'O campo Identificação da Obra é obrigatório.',
            'motivo_paralisacao.required' => 'O campo Motivo da Paralisação é obrigatório.',
            'responsavel.required'        => 'O campo Responsável é obrigatório.',
            'previsao_reinicio.required'  => 'O campo Previsão de Reinício é obrigatório.',
            'arquivo.*.mimes'             => 'Os arquivos devem estar no formato PDF.',
            'arquivo.*.max'               => 'Cada arquivo não pode ser maior que 10MB.',
        ];
    }
}
```

Mudanças em relação à v5:

- `authorize()` deixa de ser `true` e passa a delegar à Policy.
- `arquivo` ganha `max:20` (limite de anexos por obra) — nova regra de
  negócio a validar com o negócio.
- `arquivo_desc.*` deixa de ser `nullable` quando há arquivo correspondente:
  usar `required_with:arquivo.*` ou validar o pareamento no `withValidator`.

## 5. Policies e autorização

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

use App\Models\ObraParalisada;
use App\Models\User;

class ObraParalisadaPolicy
{
    public function viewAny(User $user): bool
    {
        return $user->can('obras-paralisadas.index');
    }

    public function create(User $user): bool
    {
        return $user->can('obras-paralisadas.create');
    }

    public function update(User $user, ObraParalisada $obra): bool
    {
        return $user->can('obras-paralisadas.edit');
    }

    public function delete(User $user, ObraParalisada $obra): bool
    {
        return $user->can('obras-paralisadas.delete');
    }
}
```

Regras de migração:

1. Criar o recurso `obras-paralisadas` na tabela de permissões da v7, com as
   permissões `index`, `create`, `edit`, `delete`. Na v5 o módulo **não tinha**
   recurso próprio (usava `obras`), o que impedia distinguir quem pode
   gerenciar obras paralisadas de quem pode gerenciar obras.
2. Se o negócio preferir manter a herança do recurso `obras`, fazer isso em
   código (Policy delegando a `obras.*`), e não por *alias* de string de rota.
3. Aplicar `auth` **e** `can:` em **todas** as rotas de escrita, fechando a
   fragilidade da v5.

## 6. Endpoints REST / JSON

### 6.1 CMS

| Método | URI | Nome | Autorização |
|---|---|---|---|
| GET | `/admin/obras-paralisadas` | `admin.obras-paralisadas.index` | `viewAny` |
| GET | `/admin/obras-paralisadas/create` | `admin.obras-paralisadas.create` | `create` |
| POST | `/admin/obras-paralisadas` | `admin.obras-paralisadas.store` | `create` |
| GET | `/admin/obras-paralisadas/{obra}/edit` | `admin.obras-paralisadas.edit` | `update` |
| PUT | `/admin/obras-paralisadas/{obra}` | `admin.obras-paralisadas.update` | `update` |
| DELETE | `/admin/obras-paralisadas/{obra}` | `admin.obras-paralisadas.destroy` | `delete` |
| DELETE | `/admin/obras-paralisadas/{obra}/arquivos/{arquivo}` | `admin.obras-paralisadas.arquivos.destroy` | `update` |

Substituem as 7 rotas da v5 com nomes mais previsíveis e **route model
binding** (`{obra}` resolve o model, com `SoftDeletes` aplicado).

### 6.2 Portal

| Método | URI | Nome | Descrição |
|---|---|---|---|
| GET | `/obras-paralisadas` | `obras-paralisadas.index` | Listagem paginada (15). Aceita `busca`. |
| GET | `/obras-paralisadas/{obra:slug}` | `obras-paralisadas.show` | Ficha da obra. Route model binding por slug. |
| GET | `/obras-paralisadas/exportar/{formato}` | `obras-paralisadas.exportar` | Exportação csv/xls/txt. |
| GET | `/api/obras-paralisadas` | `api.obras-paralisadas.index` | JSON paginado para o PDF do navegador ou para integradores. |

Nota de segurança: a rota de exportação deve exigir `formato` na lista
permitida (`csv`, `xls`, `txt`) e responder 404 para os demais, como na v5.

## 7. API Resources

```php
// exemplo ilustrativo
namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class ObraParalisadaResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'          => $this->id,
            'slug'        => $this->slug,
            'identificacao'      => $this->identificacao,
            'motivo_paralisacao' => $this->motivo_paralisacao,
            'responsavel'        => $this->responsavel,
            'previsao_reinicio'  => $this->previsao_reinicio->format('d/m/Y'),
            'arquivos' => $this->archives->map(fn ($a) => [
                'descricao'   => $a->description,
                'nome'        => $a->name,
                'url_download' => Storage::disk('public')->url($a->path),
            ]),
        ];
    }
}
```

O Resource padroniza o formato de saída e elimina o `json_encode($allData)`
embutido no HTML (armadilha A3 em `05-REGRAS-E-PORTABILIDADE.md`).

## 8. Views Blade modernas

| View v5 | View v7 | Observação |
|---|---|---|
| `prefecture/obras-paralisadas/index.blade.php` | `admin/obras-paralisadas/index.blade.php` | Manter DataTable, mas via `@livewire`/`<x-data-table>` ou endpoint JSON próprio. |
| `prefecture/obras-paralisadas/create.blade.php` | `admin/obras-paralisadas/create.blade.php` | Componente `<x-form.obra-paralisada>`. |
| `prefecture/obras-paralisadas/edit.blade.php` | `admin/obras-paralisadas/edit.blade.php` | Idem. |
| `prefecture/obras-paralisadas/form.blade.php` | `admin/obras-paralisadas/_form.blade.php` | Componente Blade único. |
| `portal/layouts/modeloN/obras/paralisadas/index.blade.php` (6 cópias) | `portal/obras-paralisadas/index.blade.php` | **Um** template, com o layout resolvido por componente/variável. |
| `portal/layouts/modeloN/obras/paralisadas/show.blade.php` (6 cópias) | `portal/obras-paralisadas/show.blade.php` | idem |
| `portal/layouts/modeloN/obras/paralisadas/filtro.blade.php` (6 cópias) | `portal/obras-paralisadas/_filtro.blade.php` | idem |
| `portal/components/obras-paralisadas/table.blade.php` | `portal/obras-paralisadas/_table.blade.php` | Mantido como partial. |

Eliminar as 6 cópias por template é uma das maiores economias de manutenção
ganha: na v5, qualquer ajuste na listagem precisa ser replicado em 6 arquivos
(ou o usuário vê versões diferentes conforme o layout ativo).

Componentes Blade sugeridos:

| Componente | Campos |
|---|---|
| `<x-input name="identificacao" label="Identificação da Obra" required maxlength="255" />` | texto |
| `<x-textarea name="motivo_paralisacao" label="Motivo da Paralisação" required />` | multilinha |
| `<x-input type="date" name="previsao_reinicio" label="Previsão de Reinício" required />` | data |
| `<x-file-upload name="arquivo" accept="application/pdf" :max-size="10240" />` | arquivos + descrição |
| `<x-toast />` | mensagens de sucesso/erro |

## 9. Fila, cache e storage

| Aspecto | Recomendação |
|---|---|
| Storage | Disk `public` (ou `s3` em instalação multi-node), com `Storage::disk('public')->put(...)`. |
| Nome do arquivo | Manter `uniqid()` + aleatório + extensão, mas sanitizar a extensão contra a lista de MIME permitidos (o `mimes:pdf` já limita, mas o nome original deve ser normalizado). |
| Fila | **Obrigatória** se a prefeitura quiser processar/converter PDFs (OCR, extração de texto, thumbnail) ou disparar e-mail ao publicar. Implementar um job `ProcessObraParalisadaAnexos` com `ShouldQueue`. |
| Cache | Cachear a listagem pública por alguns minutos (`Cache::remember`) ou usar `Cache-Control` no controller do portal. A v5 não cacheia nada. |
| Índice de busca | `pg_trgm` em `identificacao` e `motivo_paralisacao`, com `ILIKE` ou busca full-text, substituindo o `translate()` manual. |

## 10. Riscos e decisões pendentes

| # | Risco / decisão | Impacto | Ação |
|---|---|---|---|
| M1 | Conversão de `previsao_reinicio` de `varchar` para `date` | Dados fora do formato ISO quebram a migração | Validar as linhas existentes antes; a base v5 está vazia, então o risco é teórico hoje. |
| M2 | Tabela `obras_paralisadas` não existe na v6 | Não há dado a migrar | Criar a tabela do zero; planejar o seed inicial de conteúdo. |
| M3 | Recurso/permissão `obras-paralisadas` inexistente | Ninguém tem permissão ao publicar | Criar o recurso e vincular aos papéis. |
| M4 | Slug com sufixo aleatório vs. `identificacao-id` | URLs ficam menos "legíveis" | Manter `identificacao-id` e apenas **não** recalcular em update — solução mais simples e equivalente. |
| M5 | Remoção do `translate()` na busca | Termos com acento podem deixar de achar registros | Testar com o negócio a lista real de termos de busca. |
| M6 | PDF gerado no cliente | Continua carregando todos os registros | Migrar para endpoint de exportação no servidor (mesmo formato jsPDF no backend ou PDF via `barryvdh/laravel-dompdf`). |
| M7 | Exclusão de anexo deixa arquivo órfão | Vazamento de documento | Apagar o arquivo no storage ao remover o registro do anexo. |
| M8 | Sem trilha de auditoria | Impossibilidade de saber quem alterou a obra | Adicionar `created_by`/`updated_by` + auditoria. |
