# 🏗️ Padrão de Implementação V2 — SUCESU CMS

Este documento define o padrão arquitetural da V2, consolidado pela análise do Core,
módulos existentes (especialmente Notícias) e serviços compartilhados.
Siga este padrão rigorosamente ao criar ou modificar módulos.

---

## 1. Estrutura de Diretórios de um Módulo

```
V2/Modules/{Grupo}/{ModuleName}/
├── Controllers/
│   ├── {Module}CmsController.php          # CMS CRUD (middleware CmsAuthenticate)
│   └── {Module}PortalController.php       # Portal público
├── Models/
│   └── {Model}.php                        # Eloquent Model
├── Services/                              # Opcional — lógica de negócio
│   └── {Module}Service.php
├── Views/
│   ├── cms/                               # Views do CMS
│   └── portal/                            # Views do portal público
├── Migrations/
│   └── YYYY_MM_DD_HHMMSS_create_{table}.php
├── Routes/
│   ├── cms.php                            # Rotas CMS (prefixo cms/{modulo})
│   └── web.php                            # Rotas públicas
├── Requests/                              # Opcional — FormRequest
│   └── {Module}Request.php
└── ModuleProvider.php                     # OBRIGATÓRIO — implementa ModuleProviderInterface
```

**Regras:**
- ModuleProvider.php é **obrigatório** — sem ele o módulo não é detectado
- Migrations/ é opcional (se o módulo não criar tabelas novas)
- Services/ é opcional (lógica simples pode ficar no Controller)
- Requests/ é opcional (validação inline com $this->validate() também é aceita)

---

> ⚠️ **CmsAuthenticate** já encadeia internamente o **V2PermissionMiddleware** (linha 43 do middleware). Portanto, as rotas CMS precisam apenas do `CmsAuthenticate` — a verificação de permissão de leitura/escrita é automática.

---

## 2. ModuleProvider — Obrigatório em Todo Módulo

### Implementação da Interface

```php
<?php

namespace V2\Modules\{Grupo}\{ModuleName};

use V2\Core\Contracts\ModuleProviderInterface;

class ModuleProvider implements ModuleProviderInterface
{
    public function __construct()
    {
        $app = app();
        // Registrar singletons do módulo aqui
    }

    public function getConfig(): array
    {
        return [
            'title'          => 'Nome do Módulo',
            'description'    => 'Descrição para sidebar.',
            'icon'           => 'bi bi-icon-name',
            'position'       => 2,
            'active_pattern' => ['cms/nome-do-modulo*'],
            'route'          => 'v2.modulo.cms.index',

            'subModulesLevel1' => [
                [
                    'title'    => 'Listar',
                    'icon'     => 'bi bi-list-ul',
                    'route'    => 'v2.modulo.cms.index',
                    'position' => 1,
                ],
            ],
        ];
    }
}
```

### Regras do getConfig()

| Campo | Tipo | Obrigatório | Descrição |
|-------|------|-------------|-----------|
| title | string | Sim | Nome visível na sidebar |
| icon | string | Sim | Bootstrap Icon (ex: bi bi-newspaper) |
| position | int | Sim | Ordem na sidebar (1 = topo, 99 = fim) |
| active_pattern | string|array | Sim | Padrão de URL para highlight (ex: cms/noticias*) |
| route | string | Não | Rota nomeada do index |
| description | string | Não | Tooltip na sidebar |
| subModulesLevel1 | array | Não | Lista de submódulos |
| restricted | string | Não | Se definido, restringe acesso ao email informado |

### ViewComposers no ModuleProvider

```php
public function __construct()
{
    \View::composer('v2-cms::*', function ($view) {
        $view->with('minhaVariavel', 'valor');
    });
}
```

---

## 3. Controllers CMS — Padrão

```php
<?php

namespace V2\Modules\{Grupo}\{ModuleName}\Controllers;

use V2\Core\Controllers\BaseController;
use Illuminate\Http\Request;
use V2\Modules\{Grupo}\{ModuleName}\Models\{Model};
use V2\Services\ActivityLogger\ActivityLoggerService;
use V2\Services\SEO\Services\SEOService;
use Sentinel;

class {Module}CmsController extends BaseController
{
    /** @var ActivityLoggerService */
    protected $activityLogger;

    /** @var SEOService */
    protected $seo;

    public function __construct(
        ActivityLoggerService $activityLogger,
        SEOService $seo
    ) {
        $this->activityLogger = $activityLogger;
        $this->seo = $seo;
    }

    public function index(Request $request)
    {
        $query = {Model}::query();

        if ($request->has('search') && !empty($request->input('search'))) {
            $searchTerm = $request->input('search');
            $query->where(function ($q) use ($searchTerm) {
                $q->where('title', 'ilike', '%' . $searchTerm . '%');
            });
        }

        $items = $query->orderBy('created_at', 'desc')->paginate(15);

        return view('v2-{modulename}::cms.index', compact('items'));
    }

    public function create()
    {
        return view('v2-{modulename}::cms.create');
    }

    public function store(Request $request)
    {
        $data = $request->all();

        $model = new {Model}();
        $model->id = v2_uuid();
        $model->fill($data);
        $model->author_id = Sentinel::getUser()->id ?? null;
        $model->save();

        $this->activityLogger->log($model, 'created', [
            'type' => '{model}_create',
        ]);

        flash('Registro criado com sucesso!')->success();
        return redirect()->route('v2.{modulename}.cms.edit', ['id' => $model->id]);
    }

    public function edit($id)
    {
        $model = {Model}::findOrFail($id);
        return view('v2-{modulename}::cms.edit', compact('model'));
    }

    public function update(Request $request, $id)
    {
        $model = {Model}::findOrFail($id);
        $model->update($request->all());

        $this->activityLogger->log($model, 'updated', ['type' => '{model}_update']);
        flash('Registro atualizado com sucesso!')->success();
        return redirect()->route('v2.{modulename}.cms.edit', ['id' => $model->id]);
    }

    public function destroy($id)
    {
        $model = {Model}::findOrFail($id);
        $name = $model->title ?? $model->name;
        $model->delete();

        $this->activityLogger->log($model, 'archived', ['type' => '{model}_archive']);
        flash('"' . $name . '" arquivado com sucesso!')->success();
        return redirect()->route('v2.{modulename}.cms.index');
    }
}
```

### Padrões Obrigatórios nos Controllers CMS

| Item | Padrão |
|------|--------|
| Namespace | V2\Modules\{Grupo}\{Nome}\Controllers |
| Extends | V2\Core\Controllers\BaseController |
| Injeção ActivityLoggerService | Sempre injetar para auditoria |
| Injeção SEOService | Sempre injetar para metadados |
| Auth | Sentinel::getUser() com ?? null fallback |
| UUID | v2_uuid() |
| Flash | flash('msg')->success()/error()/warning() |
| View | v2-{modulename}::cms.{view} |
| Route | v2.{modulename}.cms.{action} |
| Redirect | redirect()->route(...) — nunca redirect()->to() |
| SoftDelete | $model->delete() + Model::onlyTrashed() |
| Activity | $this->activityLogger->log($model, $action, $props) |

---

## 4. Controllers Portal — Padrão

```php
<?php

namespace V2\Modules\{Grupo}\{ModuleName}\Controllers;

use V2\Core\Controllers\BaseController;
use V2\Modules\{Grupo}\{ModuleName}\Models\{Model};
use V2\Services\SEO\Services\SEOService;
use V2\Services\ContentRichEditor\Services\ContentRichEditorRendererService;

class {Module}PortalController extends BaseController
{
    /** @var SEOService */
    protected $seo;

    /** @var ContentRichEditorRendererService */
    protected $renderer;

    public function __construct(SEOService $seo, ContentRichEditorRendererService $renderer)
    {
        $this->seo = $seo;
        $this->renderer = $renderer;
    }

    public function index(Request $request)
    {
        $items = {Model}::published()->orderBy('published_at', 'desc')->paginate(12);
        $this->seo->setTitle('Listagem — ' . config('app.name'));
        return view('v2-{modulename}::portal.index', compact('items'));
    }

    public function show($slug)
    {
        $model = {Model}::where('slug', $slug)->published()->firstOrFail();
        $this->seo->bindModel($model);

        $htmlContent = '';
        if (!empty($model->content)) {
            try {
                $htmlContent = $this->renderer->toHtml($model->content);
            } catch (\Exception $e) {
                \Log::error('Falha renderização: ' . $e->getMessage());
            }
        }

        return view('v2-{modulename}::portal.show', compact('model', 'htmlContent'));
    }
}
```

---

## 5. Model Eloquent — Padrão

```php
<?php

namespace V2\Modules\{Grupo}\{ModuleName}\Models;

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

/**
 * @property string $id (UUID)
 * @property string $title
 * @property string $slug
 * @property string|null $status
 * @property array|null $metadata (JSONB)
 * @property \Carbon\Carbon $created_at
 * @property \Carbon\Carbon $updated_at
 * @property \Carbon\Carbon|null $deleted_at
 */
class {Model} extends Model
{
    use SoftDeletes;

    /** @var string */
    protected $table = 'v2_{table_name}';

    /** @var bool */
    public $incrementing = false;

    /** @var string */
    protected $keyType = 'string';

    /** @var array */
    protected $fillable = ['id', 'title', 'slug', 'status', 'metadata'];

    /** @var array */
    protected $casts = [
        'metadata'     => 'array',
        'published_at' => 'datetime',
    ];

    /** @var array */
    protected $dates = ['created_at', 'updated_at', 'deleted_at', 'published_at'];

    protected static function boot()
    {
        parent::boot();

        static::creating(function ($model) {
            if (empty($model->id)) {
                $model->id = v2_uuid();
            }
            if (empty($model->slug) && !empty($model->title)) {
                $model->slug = str_slug($model->title);
            }
        });

        static::saving(function ($model) {
            // Garantir unicidade do slug
            $baseSlug = $model->slug ?: str_slug($model->title);
            $slug = $baseSlug;
            $counter = 1;
            if (!empty($model->id)) {
                while (static::where('slug', $slug)->where('id', '!=', $model->id)->exists()) {
                    $slug = $baseSlug . '-' . $counter;
                    $counter++;
                }
            } else {
                while (static::where('slug', $slug)->exists()) {
                    $slug = $baseSlug . '-' . $counter;
                    $counter++;
                }
            }
            $model->slug = $slug;
        });
    }

    // ---- Scopes ----

    public function scopeActive($query)
    {
        return $query->where('status', 'active');
    }

    public function scopePublished($query)
    {
        return $query->where('status', 'published');
    }

    public function scopeRecent($query)
    {
        return $query->orderBy('created_at', 'desc');
    }
}
```

### Relacionamentos com Services

```php
// Galeria de Mídias (imagem de capa) — BelongsTo padrão
public function coverImage()
{
    return $this->belongsTo(\V2\Services\Gallery\Models\GalleryMedia::class, 'cover_image_id', 'id');
}

// Autor (usuário Sentinel) — tabela `users` do Cartalyst Sentinel
public function author()
{
    return $this->belongsTo(\App\Admin\User::class, 'author_id', 'id');
}
```

---

## 6. Migrations — Padrão Safe-Rollback

```php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;

class CreateV2{TableName}Table extends Migration
{
    public function up()
    {
        DB::beginTransaction();
        DB::statement("SET lock_timeout = '3s';");

        try {
            Schema::create('v2_{table_name}', function (Blueprint $table) {
                $table->uuid('id')->primary();
                $table->string('title', 255);
                $table->string('slug', 255)->unique();
                $table->string('status', 20)->default('active');
                $table->jsonb('metadata')->nullable();
                $table->string('created_by', 36)->nullable();
                $table->timestamps();
                $table->softDeletes();
                $table->index('status', 'idx_v2_{table}_status');
            });

            DB::commit();
        } catch (\Exception $e) {
            DB::rollBack();
            throw $e;
        }
    }

    public function down()
    {
        DB::beginTransaction();
        try {
            $backupTable = 'backup_v2_{table_name}_' . date('Ymd_His');
            Schema::rename('v2_{table_name}', $backupTable);
            DB::commit();
        } catch (\Exception $e) {
            DB::rollBack();
            throw $e;
        }
    }
}
```

### Regras

| Regra | Descrição |
|-------|-----------|
| NUNCA use Schema::dropIfExists() | Sempre renomear para backup_*_YYYYmmdd_HHMMSS |
| Use DB::beginTransaction() | Toda migration em transação para atomicidade |
| Use SET lock_timeout | Evita deadlocks em produção |
| Prefixo v2_ | Todas as tabelas V2 começam com v2_ |
| UUID primary key | $table->uuid('id')->primary() |
| jsonb para metadata | Sempre jsonb no PostgreSQL |
| Índices em colunas de filtro | status, slug, etc. |
| softDeletes() | Sempre incluir |

---

## 7. Rotas

### Rotas CMS

```php
<?php
// Routes/cms.php

Route::group([
    'middleware' => [\V2\Core\Middleware\CmsAuthenticate::class],
    'prefix'     => 'cms/{modulo}',   // {modulo} = caminho estático ex: 'noticias'
], function () {

    Route::get('/', [
        'as'   => 'v2.{modulo}.cms.index',
        'uses' => '{Module}CmsController@index',
    ]);

    Route::get('/novo', [
        'as'   => 'v2.{modulo}.cms.create',
        'uses' => '{Module}CmsController@create',
    ]);

    Route::post('/novo', [
        'as'   => 'v2.{modulo}.cms.store',
        'uses' => '{Module}CmsController@store',
    ]);

    Route::get('/editar/{id}', [
        'as'   => 'v2.{modulo}.cms.edit',
        'uses' => '{Module}CmsController@edit',
    ]);

    // Route::match(['PUT', 'POST']) necessário pois HTML forms só suportam GET/POST
    Route::match(['PUT', 'POST'], '/editar/{id}', [
        'as'   => 'v2.{modulo}.cms.update',
        'uses' => '{Module}CmsController@update',
    ]);

    // Archive (soft-delete)
    Route::delete('/{id}', [
        'as'   => 'v2.{modulo}.cms.destroy',
        'uses' => '{Module}CmsController@destroy',
    ]);

    // Restore (opcional)
    Route::post('/{id}/restaurar', [
        'as'   => 'v2.{modulo}.cms.restore',
        'uses' => '{Module}CmsController@restore',
    ]);
});
```

### Rotas Públicas (Portal)

```php
<?php
// Routes/web.php
// ATENÇÃO: Use caminhos ESTÁTICOS (ex: /noticias), nunca parâmetro {modulo}.
// O prefixo público é definido manualmente para cada módulo.

Route::group(['middleware' => ['web']], function () {

    Route::get('/noticias', [
        'as'   => 'v2.noticias.portal.index',
        'uses' => 'NoticiasPortalController@index',
    ]);

    Route::get('/noticia/{slug}', [
        'as'   => 'v2.noticias.portal.show',
        'uses' => 'NoticiasPortalController@show',
    ]);
});
```

**Padrão geral:** substitua `noticias`/`noticia` pelo nome do seu módulo no singular/plural conforme a URL desejada.

### Nomenclatura de Rotas

| Tipo | Padrão | Exemplo |
|------|--------|---------|
| CMS index | v2.{modulo}.cms.index | v2.noticias.cms.index |
| CMS create | v2.{modulo}.cms.create | v2.noticias.cms.create |
| CMS store | v2.{modulo}.cms.store | v2.noticias.cms.store |
| CMS edit | v2.{modulo}.cms.edit | v2.noticias.cms.edit |
| CMS update | v2.{modulo}.cms.update | v2.noticias.cms.update |
| CMS destroy | v2.{modulo}.cms.destroy | v2.noticias.cms.destroy |
| Portal index | v2.{modulo}.portal.index | v2.noticias.portal.index |
| Portal show | v2.{modulo}.portal.show | v2.noticias.portal.show |

---

## 8. Views — Namespaces

| Origem | Diretório | Namespace |
|--------|-----------|-----------|
| Core layouts CMS | V2/layouts/cms/ | v2-cms:: |
| Core layouts Portal | V2/layouts/portal/ | v2-portal:: |
| Views do módulo | V2/Modules/{G}/{N}/Views/ | v2-{lowercasename}:: |
| Views de serviço | V2/Services/{N}/Views/ | v2-service-{lowercasename}:: |

### Estrutura de View CMS

```blade
{{-- cms/index.blade.php --}}
@extends('v2-cms::layouts.app')

@section('title', 'Meu Módulo')
@section('header-title', 'Meu Módulo')
@section('header-description', 'Gerenciamento de...')

@section('content')
    <div class="card">
        <div class="card-body">
            @if(count($items) > 0)
                <table class="table">
                    <thead>
                        <tr>
                            <th>Título</th>
                            <th>Status</th>
                            <th>Ações</th>
                        </tr>
                    </thead>
                    <tbody>
                        @foreach($items as $item)
                        <tr>
                            <td>{{ $item->title }}</td>
                            <td><span class="badge bg-{{ $item->status }}">{{ $item->status }}</span></td>
                            <td>
                                <a href="{{ route('v2.{modulo}.cms.edit', ['id' => $item->id]) }}"
                                   class="btn btn-sm btn-outline-primary">
                                    <i class="bi bi-pencil"></i>
                                </a>
                            </td>
                        </tr>
                        @endforeach
                    </tbody>
                </table>
                {{ $items->links() }}
            @else
                <div class="text-center py-4 text-muted">
                    <i class="bi bi-inbox fs-1"></i>
                    <p class="mt-2">Nenhum registro encontrado.</p>
                </div>
            @endif
        </div>
    </div>
@endsection
```

### Estrutura de View Portal

```blade
{{-- portal/index.blade.php --}}
@extends('v2-portal::layouts.app')

@section('seo')
    @include('v2-portal::partials.seo')
@endsection

@section('content')
    <section class="news-section section-padding fix">
        <div class="container">
            <div class="row g-4">
                @foreach($items as $item)
                <div class="col-xl-4 col-lg-6 col-md-6 wow fadeInUp">
                    <div class="news-card-items mt-0">
                        <div class="news-thumb">
                            <img src="{{ $item->coverImageUrl ?? 'assets/img/default.jpg' }}"
                                 alt="{{ $item->title }}">
                            <h4 class="number">{{ $item->published_at->format('d') }}<span>{{ $item->published_at->format('M') }}</span></h4>
                        </div>
                        <div class="news-content">
                            <h3><a href="{{ route('v2.{modulo}.portal.show', ['slug' => $item->slug]) }}">{{ $item->title }}</a></h3>
                            <p>{{ Str::limit($item->summary ?? strip_tags($item->content), 150) }}</p>
                            <a href="{{ route('v2.{modulo}.portal.show', ['slug' => $item->slug]) }}" class="link-btn">
                                Leia Mais <i class="fa-light fa-arrow-right-long"></i>
                            </a>
                        </div>
                    </div>
                </div>
                @endforeach
            </div>
            {{ $items->links() }}
        </div>
    </section>
@endsection
```

---

## 9. Validação — FormRequest (Opcional)

```php
<?php

namespace V2\Modules\{Grupo}\{ModuleName}\Requests;

use Illuminate\Foundation\Http\FormRequest;

class {Module}Request extends FormRequest
{
    public function authorize()
    {
        return true; // Autorização via middleware v2.permission
    }

    public function rules()
    {
        $rules = [
            'title'    => 'required|string|max:255',
            'slug'     => 'nullable|string|max:255',
            'status'   => 'required|string|in:active,inactive',
            'metadata' => 'nullable|array',
        ];

        $id = $this->route('id');
        if (!empty($id)) {
            $rules['slug'] = 'nullable|string|max:255|unique:v2_{table},slug,' . $id . ',id';
        }

        return $rules;
    }

    public function messages()
    {
        return [
            'title.required' => 'O título é obrigatório.',
            'title.max'      => 'O título deve ter no máximo 255 caracteres.',
        ];
    }

    protected function prepareForValidation()
    {
        if (!$this->has('slug') || empty($this->input('slug'))) {
            $this->merge(['slug' => str_slug($this->input('title', ''))]);
        }
    }
}
```

---

## 10. Integração com Services Compartilhados

### ActivityLoggerService (Sempre Obrigatório)

```php
use V2\Services\ActivityLogger\ActivityLoggerService;

// Injetar no construtor do controller
// Toda operação de escrita DEVE logar:
$this->activityLogger->log($model, 'created', ['type' => 'module_action', 'extra' => 'info']);
$this->activityLogger->log($model, 'updated', ['type' => 'module_action']);
$this->activityLogger->log($model, 'archived', ['type' => 'module_action']);
$this->activityLogger->log($model, 'restored', ['type' => 'module_action']);
```

### SEOService (Sempre Obrigatório em Portal)

```php
use V2\Services\SEO\Services\SEOService;

// No PortalController:
$this->seo->setTitle('Título da Página');
$this->seo->setDescription('Descrição para SEO');
$this->seo->setKeywords('palavras, chaves');
$this->seo->bindModel($model);  // Extrai SEO de $model->metadata['seo']
$this->seo->addSchema(['@type' => 'WebPage', ...]);  // JSON-LD
```

### ContentRichEditorRendererService (Para conteúdo rico)

```php
use V2\Services\ContentRichEditor\Services\ContentRichEditorRendererService;

// Converte AST JSON do editor para HTML:
$htmlContent = $this->renderer->toHtml($model->content);

// Converte para Markdown:
$markdown = $this->renderer->toMarkdown($model->content);
```

### GalleryService (Para imagens/mídias)

```php
use V2\Services\Gallery\Services\GalleryService;
use V2\Services\Gallery\DTOs\GalleryConfig;

// GalleryService é INJETADO no construtor (nunca chamado estaticamente):
/** @var GalleryService */
protected $gallery;

public function __construct(GalleryService $gallery)
{
    $this->gallery = $gallery;
}

// Listar mídias do módulo:
$galleryConfig = new GalleryConfig('module_name', ['image/jpeg', 'image/png'], 2048, false);
$media = $this->gallery->listMedia($galleryConfig, $searchTerm, $type, $archived);

// Upload:
$result = $this->gallery->upload($uploadedFile, $galleryConfig, $altText, $description, $isShared, $customFilename);

// Buscar mídia específica:
$mediaItem = $this->gallery->find($id, $galleryConfig, $includeArchived);

// Atualizar metadados:
$this->gallery->update($id, $galleryConfig, ['alt_text' => 'novo texto']);

// Arquivar/restaurar:
$this->gallery->archive($id, $galleryConfig);
$this->gallery->restore($id, $galleryConfig);

// Deletar fisicamente (expurgo):
$this->gallery->delete($id, $galleryConfig);
```

### SearchService (Para indexação em busca)

A indexação em busca é tipicamente chamada no **evento `saved` do Model**, não no controller:

```php
use V2\Services\Search\DTOs\PublicSearchDTO;
use V2\Services\Search\Services\PublicIndexerService;

// Indexar conteúdo público:
$dto = new PublicSearchDTO(
    $model->title,
    $model->slug,
    'module_name',
    'entity_type',
    $model->id,
    $model->summary,
    $model->content,
    ['tags' => []]
);
app(PublicIndexerService::class)->index($dto);

// Ao deletar (chamado no model boot):
app(PublicIndexerService::class)->remove('entity_type', $model->id);
```

**Padrão recomendado — no `boot()` do Model:**
```php
static::saved(function ($model) {
    try {
        app(NewsSearchSender::class)->sync($model);
    } catch (\Exception $e) {
        \Log::error('[Module] Falha ao sincronizar busca: ' . $e->getMessage());
    }
});
```

### GalleryMedia Model (Para relações)

```php
// No Model do módulo, para imagem de capa:
use V2\Services\Gallery\Models\GalleryMedia;

public function coverImage()
{
    return $this->belongsTo(GalleryMedia::class, 'cover_image_id', 'id');
}
```

---

## 11. Autorização com v2_can() e Permissões

### No Blade CMS

```blade
@v2can('ModuleName', 'write')
    <a href="{{ route('v2.{modulo}.cms.create') }}" class="btn btn-primary">
        <i class="bi bi-plus-circle"></i> Novo
    </a>
@endv2can

@v2can('ModuleName', 'read')
    @foreach($items as $item)
        {{-- mostrar item --}}
    @endforeach
@endv2can
```

### No Controller (implícito via middleware)

O middleware V2PermissionMiddleware aplica automaticamente:
- GET → permissão 'read'
- POST/PUT/DELETE → permissão 'write'

### Super Admin Bypass

Usuário com email igual a config('v2.support.email') ou role 'super-admin' bypassa todas as verificações.

---

## 12. Flash Messages

```php
flash('Mensagem de sucesso')->success();
flash('Mensagem de erro')->error();
flash('Mensagem de aviso')->warning();
flash('Mensagem informativa')->info();
```

---

## 13. Helpers Globais V2

| Helper | Retorno | Descrição |
|--------|---------|-----------|
| v2_uuid() | string | Gera UUID v4 randômico |
| v2_can($moduleKey, $action) | bool | Verifica permissão do usuário logado |
| v2_version($full, $prefix) | string | Versão do sistema do arquivo VERSION |
| v2_is_maintenance_active() | bool | Modo de manutenção ativo? |
| v2_cms_icon($icon) | string | Converte ícone FA4 (ex: 'fa-dashboard') para Bootstrap Icon (ex: 'bi-speedometer2') |

---

## 14. Layouts CMS — Estrutura Base

```blade
{{-- v2-cms::layouts.app --}}
<!DOCTYPE html>
<html lang="pt-br">
<head>
    <meta charset="UTF-8">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>@yield('title', config('app.name')) - CMS</title>
    <link href="{{ asset('assets/css/bootstrap.min.css') }}" rel="stylesheet">
    <link href="{{ asset('assets/css/bootstrap-icons.css') }}" rel="stylesheet">
    @stack('styles')
</head>
<body>
    <div class="d-flex">
        @include('v2-cms::partials.sidebar')

        <div class="flex-grow-1">
            @include('v2-cms::partials.header')

            <div class="container-fluid p-4">
                @include('v2-cms::partials.flash')
                @yield('content')
            </div>
        </div>
    </div>

    <script src="{{ asset('assets/js/bootstrap.bundle.min.js') }}"></script>
    @stack('scripts')
</body>
</html>
```

---

## 15. Checklist de Criação de Módulo

- [ ] Criar diretório V2/Modules/{Grupo}/{ModuleName}/
- [ ] Criar ModuleProvider.php implementando ModuleProviderInterface
- [ ] Criar Routes/cms.php com middleware CmsAuthenticate
- [ ] Criar Routes/web.php (se tiver páginas públicas)
- [ ] Criar Models/ com SoftDeletes, UUID, casts
- [ ] Criar Migrations/ com safe-rollback
- [ ] Criar Controllers/{Module}CmsController.php extendendo BaseController
- [ ] Criar Controllers/{Module}PortalController.php (se público)
- [ ] Criar Views/cms/ (index, create, edit)
- [ ] Criar Views/portal/ (index, show) — se público
- [ ] Injetar ActivityLoggerService e SEOService nos controllers
- [ ] Usar v2_uuid() para gerar IDs
- [ ] Usar flash() para mensagens
- [ ] Usar Sentinel::getUser() para usuário logado
- [ ] Usar v2_can() nas views para controle de acesso
- [ ] Adicionar activity log em toda operação de escrita
- [ ] Testar com phpunit --testsuite "V2 Unit"
