# Outras Configurações — Migração para Laravel Moderno

> Plano de reconstrução do módulo `outras-configuracoes` em PHP 8.x +
> Laravel 11/12. O módulo deve ser reconstruído como **um recurso REST de
> chaves de configuração**, corrigindo as divergências registradas em
> `08-GAPS-E-DIVIDAS.md` sem quebrar as regras listadas em
> `05-REGRAS-E-PORTABILIDADE.md`.

---

## 1. Schema de migração Laravel

O legado usa a tabela `configurations` com `category_configurations_id` e soft
delete. A v7 deve manter os mesmos nomes de coluna (o portal lê
`configurations.key` diretamente em
`v6/app/Providers/ConfigHeaderServiceProvider.php` e
`v6/app/Helpers/functions.php`).

```php
// exemplo ilustrativo — migration 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('configuracoes', function (Blueprint $table) {
            $table->id();
            $table->string('key', 191);
            $table->text('value')->nullable();
            $table->string('type', 191);
            $table->string('title', 191);
            $table->foreignId('categoria_configuracao_id')->constrained('categorias_configuracao');
            $table->unsignedInteger('max_input')->nullable();
            $table->unsignedInteger('min_input')->nullable();
            $table->boolean('notnull')->default(false);
            $table->unsignedInteger('order')->nullable();
            $table->text('description')->nullable();
            $table->text('small_description')->nullable();
            $table->string('placeholder', 191)->nullable();
            $table->boolean('status_create')->default(false);
            $table->timestamps();
            $table->softDeletes();

            $table->unique(['key', 'deleted_at']);
            $table->index(['categoria_configuracao_id', 'order']);
        });
    }
};
```

> Decisão de equivalência: manter `key` e `value` com os mesmos nomes evita
> reescrever todos os consumidores (header do portal, `getActualLayout()`,
> `ConfigHeaderServiceProvider`). Renomear tabelas/colunas é possível desde que
> os consumidores sejam atualizados em conjunto.

### 1.1 View SQL — decidir entre manter ou remover

| Opção | Prós | Contras |
|---|---|---|
| Manter a view `configuracoes` (adaptada) | Menos mudança no `DatatableController` | Continua sem filtro por categoria; `row_number()` enganoso |
| Remover a view e usar `Query Builder` + `JOIN` | Filtro por categoria nativo; ordenação previsível | Exige reescrever o provider de dados |

Recomendação: **remover a view** e montar a listagem com Eloquent:

```php
// exemplo ilustrativo
$query = Configuracao::query()
    ->with('categoria')
    ->when($request->input('categoria'), fn ($q, $c) => $q->whereHas('categoria', fn ($sub) => $sub->where('slug', $c)))
    ->orderBy('categoria_id')
    ->orderBy('order');
```

## 2. Modelos

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

enum TipoConfiguracao: string
{
    case Texto = 'text';
    case AreaTexto = 'textarea';
    case Arquivo = 'file';
    case Checkbox = 'checkbox';
    case Senha = 'password';
}

class Configuracao extends Model
{
    use SoftDeletes;

    protected $table = 'configuracoes';

    protected $fillable = [
        'key', 'value', 'status_create', 'type', 'title',
        'categoria_configuracao_id', 'max_input', 'min_input',
        'notnull', 'order', 'description', 'placeholder', 'small_description',
    ];

    protected $casts = [
        'notnull' => 'boolean',
        'status_create' => 'boolean',
        'max_input' => 'integer',
        'min_input' => 'integer',
        'order' => 'integer',
    ];

    public function categoria(): BelongsTo
    {
        return $this->belongsTo(CategoriaConfiguracao::class, 'categoria_configuracao_id');
    }
}
```

| Mudança | Motivo |
|---|---|
| `hasOne` → `belongsTo` | Correção semântica do legado |
| Casts para booleanos e inteiros | Elimina comparações frouxas (`notnull == true`) |
| Enum `TipoConfiguracao` | Elimina o `if` do JavaScript e o select solto no Blade |
| `SoftDeletes` mantido | Compatibilidade com a listagem (filtro `deleted_at IS NULL`) |
| Índice único `(key, deleted_at)` | Elimina a ambiguidade de chaves duplicadas (achado A2) |

## 3. Form Requests

```php
// exemplo ilustrativo
class StoreConfiguracaoRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->hasRole('super-admin');
    }

    public function rules(): array
    {
        return [
            'categoria_configuracao_id' => ['required', 'integer', 'exists:categorias_configuracao,id'],
            'type' => ['required', Rule::enum(TipoConfiguracao::class)],
            'title' => ['required', 'string', 'max:190'],
            'key' => ['required', 'string', 'max:190', 'regex:/^[a-z0-9_]+$/'],
            'value' => ['required_unless:type,file'],
            'max_input' => ['nullable', 'integer', 'min:0'],
            'min_input' => ['nullable', 'integer', 'min:0'],
            'notnull' => ['boolean'],
            'order' => ['nullable', 'integer'],
            'description' => ['nullable', 'string'],
            'small_description' => ['nullable', 'string'],
            'placeholder' => ['nullable', 'string', 'max:191'],
        ];
    }
}
```

Para a gravação por categoria, substituir o N+1 por um carregamento único:

```php
// exemplo ilustrativo
$regras = Configuracao::whereIn('key', array_keys($this->all()))
    ->get()
    ->keyBy('key')
    ->map(fn (Configuracao $c) => array_filter([
        $c->notnull ? 'required' : null,
        $c->max_input ? 'max:' . $c->max_input : null,
        $c->min_input ? 'min:' . $c->min_input : null,
    ]))
    ->all();
```

## 4. Policies / autorização

```php
// exemplo ilustrativo
class ConfiguracaoPolicy
{
    public function viewAny(User $user): bool
    {
        return $user->can('outros.index');
    }

    public function view(User $user, Configuracao $configuracao): bool
    {
        return $user->can('outros.index');
    }

    public function create(User $user): bool
    {
        return $user->hasRole('super-admin');
    }

    public function update(User $user, Configuracao $configuracao): bool
    {
        return $user->hasRole('super-admin');
    }

    public function delete(User $user, Configuracao $configuracao): bool
    {
        return $user->hasRole('super-admin');
    }
}
```

| Decisão | Justificativa |
|---|---|
| Leitura por `outros.index` / `outros.edit` | Preserva a regra R1/R2 observável |
| Escrita restrita a `super-admin` | Preserva R6 e elimina a assimetria A4 (gravação por categoria deixa de existir como rota genérica) |
| Permissões por recurso (`configuracoes`, `outros`) | Mantém a matriz de permissões atual; opcionalmente unificar em `configuracoes.*` |

## 5. Endpoints REST/JSON sugeridos

| Método | URI | Nome | Descrição |
|---|---|---|---|
| GET | `/api/cms/configuracoes` | `api.configuracoes.index` | Listagem paginada, **com filtro `categoria`**, colunas `id`, `titulo`, `tipo`, `categoria` |
| GET | `/api/cms/configuracoes/{id}` | `api.configuracoes.show` | Detalhe |
| POST | `/api/cms/configuracoes` | `api.configuracoes.store` | Cadastro (`super-admin`) |
| PUT | `/api/cms/configuracoes/{id}` | `api.configuracoes.update` | Atualização (`super-admin`) |
| DELETE | `/api/cms/configuracoes/{id}` | `api.configuracoes.destroy` | Soft delete; resposta `{ "success": "..." }` (200) ou `{ "erro": "..." }` (500) — **manter o contrato do front-end** |
| GET | `/api/cms/configuracoes/{id}/arquivo` | `api.configuracoes.arquivo` | Download do arquivo (`type = file`) |
| POST | `/api/cms/configuracoes/{id}/arquivo/remover` | `api.configuracoes.remover-arquivo` | Equivalente a `removeArquivo()` |

Regras de equivalência do `DELETE`: manter os pares `success` / `erro` e os
status 200/500, porque o JavaScript do front-end depende deles.

## 6. API Resources

```php
// exemplo ilustrativo
class ConfiguracaoResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'titulo' => $this->title,
            'tipo' => $this->type,
            'categoria' => $this->categoria->slug,
            'key' => $this->key,
            'value' => $this->type === 'password' ? '******' : $this->value,
            'notnull' => (bool) $this->notnull,
            'max_input' => $this->max_input,
            'min_input' => $this->min_input,
            'order' => $this->order,
        ];
    }
}
```

| Item | Observação |
|---|---|
| Mascaramento de `password` | O tipo `password` existe no banco (ex.: `mail_password`) e **não** pode ser devolvido em claro |
| Paginação | `per_page` e ordenação por `categoria` + `order` (não por `row_number()`) |

## 7. Views Blade modernas

| Tela | Componente sugerido |
|---|---|
| Listagem | `<x-admin.data-table>` com colunas `Título`, `Tipo`, `Categoria`, `Ações`, filtro por categoria e exportação |
| Formulário | `<x-admin.form-field>` para `key`, `value`, `type`, `categoria`, limites; tipo como `<select>` com `x-model` |
| Rotas | `Route::resource('configuracoes', ConfiguracaoController::class)` + rota de edição por categoria removida (não há mais `case 'outros'`) |

Back-end do formulário (evita a gambiarra do `disabled`):

```php
// exemplo ilustrativo — no controller
$configuracao->status_create
    ? ['readonly' => true]
    : [];
```

## 8. Fila, cache e storage

| Aspecto | v6 | v7 |
|---|---|---|
| Cache do módulo | `Cache::rememberForever('modulos', ...)` em `v6/app/Providers/ModulosServiceProvider.php`; invalidado com `Cache::pull('modulos')` em `v6/app/Http/Controllers/Admin/ModulosController.php` | `Cache::remember('modulos', ttl: 3600, ...)` + invalidação em `Cache::forget('modulos')` |
| Arquivos | Disco `public`, pasta `configuracoes/` | Disco `public` via `Storage::disk('public')`; nomes padronizados `configuracoes/{YmdHi}{4chars}.{ext}` |
| Fila | Não usada | Opcional: regeneração de `config.json` e limpeza de arquivos órfãos em job |
| `config.json` | Escrito no controller, com `echo` (achado A5) | Escrito por um job/serviço `ConfigJsonExporter`, sem `echo`, com log |

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

| # | Risco | Decisão |
|---|---|---|
| M1 | Quebrar a leitura de `configurations.key` pelo portal | Manter nomes de tabela/coluna ou migrar todos os consumidores em conjunto |
| M2 | Adicionar índice único em `key` com dados duplicados | Antes de criar o índice, rodar uma auditoria de duplicatas e decidir a política (unificar ou renomear) |
| M3 | Mudar o contrato JSON do `DELETE` | Manter `success`/`erro` e 200/500 |
| M4 | Deixar de exibir a listagem "todas as categorias" | A listagem v7 deve ser filtrável; se o negócio exigir a visão global, expor um filtro "Todas as categorias" como padrão explícito |
| M5 | Permissões de escrita | Consolidar em `super-admin` + `configuracoes.edit` conforme decisão do negócio |
| M6 | Esconder a categoria `outros` vazia | Documentar no código que a categoria é um slot livre; não semear chaves fictícias |
| M7 | Menu sumindo fora do dashboard (achado A14) | Registrar os composers corretamente no provider da v7 ou usar um middleware/View Composer único para `$modulos` |
| M8 | Vazamento de segredos (e-mail, Gemini, recaptcha) | Mascarar no JSON, auditar logs de atividade (o `store()` grava o valor no log) |
