# Telefones Úteis — Migração para Laravel Moderno

## 1. Decisão de modelagem

**Recomendação:** criar uma tabela dedicada `useful_phones` em vez de reaproveitar `contents.text`.

Justificativa: o dado atual é um texto livre dentro de uma tabela genérica com 27 colunas; a view SQL
`telefones_uteis` existe só para produzir 3 colunas. Uma tabela dedicada permite tipar o telefone,
indexar por número e expor API sem ambiguidade.

```php
// migration: create_useful_phones_table.php
Schema::create('useful_phones', function (Blueprint $table) {
    $table->id();
    $table->string('title', 120);
    $table->string('phone', 32);
    $table->string('slug', 160)->unique();
    $table->boolean('active')->default(true);
    $table->unsignedInteger('sort_order')->default(0);
    $table->timestamps();
    $table->softDeletes();

    $table->index(['active', 'sort_order']);
});
```

### Migração dos dados legados

```php
// exemplo ilustrativo
DB::table('contents')
    ->join('category_contents', 'contents.category_contents_id', '=', 'category_contents.id')
    ->where('category_contents.slug', 'telefones-uteis')
    ->whereNull('contents.deleted_at')
    ->orderBy('contents.id')
    ->select(['contents.id', 'contents.title', 'contents.text', 'contents.slug', 'contents.created_at'])
    ->get()
    ->each(fn ($row) => UsefulPhone::updateOrCreate(
        ['legacy_id' => $row->id],
        ['title' => $row->title, 'phone' => $row->text, 'slug' => $row->slug ?: Str::slug($row->title)]
    ));
```

## 2. Modelos

```php
// app/Models/UsefulPhone.php  (exemplo ilustrativo)
class UsefulPhone extends Model
{
    use HasFactory, SoftDeletes;

    protected $fillable = ['title', 'phone', 'slug', 'active', 'sort_order'];

    protected function casts(): array
    {
        return [
            'active' => 'boolean',
            'sort_order' => 'integer',
        ];
    }
}
```

- `Phone` legado (`phones`/`phones_type`) permanece para o fluxo LAI; **não** migrar para `useful_phones`.
- Se a prefeitura quiser reaproveitar o conteúdo genérico, manter `Content` e criar um `ContentType`
  (enum `UsefulPhone`), mas com coluna `phone` própria.

## 3. Form Requests

```php
// app/Http/Requests/StoreUsefulPhoneRequest.php  (exemplo ilustrativo)
public function rules(): array
{
    return [
        'title' => ['required', 'string', 'max:120'],
        'phone' => ['required', 'string', 'max:32', 'regex:/^[0-9()\s+-]+$/'],
        'active' => ['boolean'],
    ];
}
```

Regras herdadas do legado que **devem** ser preservadas: título obrigatório com no máximo 120
caracteres; número obrigatório; máscara `(99) 99999-9999` no cliente.

## 4. Policies e autorização

```php
// app/Policies/UsefulPhonePolicy.php  (exemplo ilustrativo)
public function update(User $user, UsefulPhone $phone): bool
{
    return $user->can('telefones-uteis.edit');
}
```

Permissões a migrar (do seed `v6/database/seeds/ResourcesTableSeeder.php`):
`telefones-uteis.create`, `telefones-uteis.edit`, `telefones-uteis.delete`,
`telefones-uteis.restore`, `telefones-uteis.inactivate`.

## 5. Endpoints REST

### Portal (público)

| Método | URI | Retorno |
|---|---|---|
| GET | `/api/v1/telefones-uteis` | `UsefulPhoneResource::collection` |
| GET | `/api/v1/telefones-uteis/{slug}` | `UsefulPhoneResource` |

```php
// app/Http/Resources/UsefulPhoneResource.php  (exemplo ilustrativo)
return [
    'id' => $this->id,
    'title' => $this->title,
    'phone' => $this->phone,
    'slug' => $this->slug,
];
```

### CMS (autenticado)

| Método | URI | Descrição |
|---|---|---|
| GET | `/api/v1/cms/telefones-uteis` | listagem paginada + filtros |
| POST | `/api/v1/cms/telefones-uteis` | criação (`StoreUsefulPhoneRequest`) |
| PUT | `/api/v1/cms/telefones-uteis/{id}` | atualização (`UpdateUsefulPhoneRequest`) |
| DELETE | `/api/v1/cms/telefones-uteis/{id}` | exclusão (soft delete) |
| POST | `/api/v1/cms/telefones-uteis/deletar-lote` | exclusão em lote |

O endpoint de DataTable do legado (`GET /cms/datatable/telefones_uteis`) deve ser substituído por
paginação server-side nativa; a exportação Excel/PDF pode ser mantida no front ou gerada no backend
(com `maatwebsite/excel` ou `barryvdh/laravel-dompdf`).

## 6. Views Blade modernas

- Componente `<x-useful-phones-list :items="$phones" />` com `@foreach` e busca opcional.
- Para a busca, preferir **filtro no servidor** (`?q=`) em vez do `keyup` sobre o DOM: elimina a
  divergência de comportamento entre layouts e permite paginação.
- Unificar os 8 templates de layout em um único componente parametrizado por tema, eliminando a
  divergência de case-sensitivity (armadilha A6) e o caminho órfão do layout `default` (A4).
- Blade: usar `{{ $phone->title }}` e `{{ $phone->phone }}` (escape automático) e remover o
  `Breadcrumbs::render('category-content', $category)` sem variável.

## 7. Fila, cache e storage

| Item | Decisão |
|---|---|
| Cache | `Cache::remember('useful-phones', 300, fn () => UsefulPhone::active()->orderBy('sort_order')->get())` para a listagem pública; invalidado por evento de model |
| Fila | desnecessária (sem upload no módulo) |
| Storage | não se aplica (o telefone é texto) |
| Logs | `spatie/laravel-activitylog` para create/update/delete, com `LogBatch` na exclusão em lote |

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

| Risco | Decisão |
|---|---|
| Mudar a fonte de dados pode perder registros com `text` vazio ou não numérico | validar a conversão e reportar os descartados |
| Perder o “ID” exibido (era `row_number()`) | expor `id` real na API e na tabela do CMS (mudança visual aceitável) |
| Filtrar por `status` no portal mudaria o comportamento atual | decisão de negócio: por padrão manter visível tudo que não foi apagado |
| Buscar no servidor muda a latência perceived | indexar `title`/`phone` com `pg_trgm` se a busca por similaridade for necessária |
| Remover a view SQL quebra consumidores externos (relatórios) | manter a view como camada de compatibilidade por um ciclo, se houver consumidores |
| Trocar `Route::any` por `Route::get` | `Route::any` aceitando POST é um bug de superfície; mudar para `GET` é seguro (não há formulários POSTando nessa rota) |
