# Módulo: Parceiros (GAP)

## 1. O que é o módulo

Gerencia a listagem pública de parceiros institucionais da SUCESU Nacional. Visualmente similar a associados, mas em grid de cards que mostram logo, nome, descrição e um link "Saiba mais". Não possui os links de redes sociais nem a data "associado desde". Com paginação.

No CMS, deve permitir CRUD completo com upload de logo, ordenação e controle de exibição.

## 2. Estrutura de dados

**Parceiro (`parceiro`)**
- `id` (UUID, PK)
- `name` (string, 255) — nome da empresa/instituição
- `slug` (string, 255, unique) — slug para URL
- `description` (text) — descrição do parceiro
- `logo` (string, 255) — caminho da imagem do logo
- `url` (string, 255, nullable) — site externo (link "Saiba mais")
- `sort_order` (integer, default 0) — ordenação manual
- `is_active` (boolean, default true)
- `created_at` (timestamp)
- `updated_at` (timestamp)
- `deleted_at` (timestamp, nullable) — soft delete

## 3. Dependências

- **Módulos:** Nenhum específico
- **Serviços:** `ActivityLoggerService`
- **Pacotes:** Nenhum adicional
- **Storage:** `public/upload/parceiros/` para logos

## 4. Tabelas necessárias

### `v2_parceiros`

| Coluna | Tipo | Restrições |
|---|---|---|
| id | uuid | PK |
| name | varchar(255) | NOT NULL |
| slug | varchar(255) | UNIQUE, NOT NULL |
| description | text | NULL |
| logo | varchar(255) | NULL |
| url | varchar(255) | NULL |
| sort_order | integer | DEFAULT 0 |
| is_active | boolean | DEFAULT true |
| created_at | timestamp | NULL |
| updated_at | timestamp | NULL |
| deleted_at | timestamp | NULL |

## 5. Arquivos necessários

```
V2/Modules/Publicacoes/Parceiros/
├── Controllers/
│   ├── ParceirosCmsController.php       # CRUD administrativo
│   └── ParceirosPortalController.php    # Listagem pública
├── Models/
│   └── Parceiro.php                     # Eloquent model
├── Requests/
│   ├── ParceiroStoreRequest.php
│   └── ParceiroUpdateRequest.php
├── Views/
│   ├── cms/
│   │   ├── index.blade.php
│   │   ├── create.blade.php
│   │   ├── edit.blade.php
│   │   └── _form.blade.php
│   └── portal/
│       └── index.blade.php              # Grid público de parceiros
├── Migrations/
│   └── 2024_01_01_000002_create_v2_parceiros_table.php
├── Routes/
│   ├── cms.php
│   └── web.php
└── ModuleProvider.php
```

**Total estimado: 14 arquivos**

## 6. Sugestão de implementação

### Migração (safe-rollback)

```php
Schema::create('v2_parceiros', function (Blueprint $table) {
    $table->uuid('id')->primary();
    $table->string('name', 255);
    $table->string('slug', 255)->unique();
    $table->text('description')->nullable();
    $table->string('logo', 255)->nullable();
    $table->string('url', 255)->nullable();
    $table->integer('sort_order')->default(0);
    $table->boolean('is_active')->default(true);
    $table->timestamps();
    $table->softDeletes();
});

// down(): Schema::rename('v2_parceiros', 'backup_v2_parceiros');
```

### Model (similar ao Associado, sem campos de redes sociais)

```php
class Parceiro extends Model
{
    public $incrementing = false;
    protected $keyType = 'string';
    protected $fillable = [
        'name', 'slug', 'description', 'logo', 'url',
        'sort_order', 'is_active',
    ];
    protected $casts = ['is_active' => 'boolean'];
}
```

### Controller (Portal)

Query: `Parceiro::where('is_active', true)->orderBy('sort_order')->paginate(12)` — retorna para view `v2-parceiros::portal.index`.

### Controller (CMS)

CRUD padrão idêntico ao de Associados:
- Store: `$request->file('logo')->store('public/upload/parceiros')`
- Flash messages: `flash('Parceiro criado com sucesso!')->success()`
- Log de atividade com `ActivityLoggerService`

### ModuleProvider

```php
return [
    'title'          => 'Parceiros',
    'description'    => 'Gerenciamento de parceiros institucionais.',
    'icon'           => 'bi bi-handshake',
    'position'       => 4,
    'active_pattern' => ['cms/parceiros*'],
    'route'          => 'v2.parceiros.cms.index',
    'subModulesLevel1' => [
        [
            'title'    => 'Todos os Parceiros',
            'icon'     => 'bi bi-list-ul',
            'route'    => 'v2.parceiros.cms.index',
            'position' => 1,
        ],
        [
            'title'    => 'Novo Parceiro',
            'icon'     => 'bi bi-plus-circle',
            'route'    => 'v2.parceiros.cms.create',
            'position' => 2,
        ],
    ],
];
```

### Rotas

- **web.php:** `Route::get('/parceiros', 'ParceirosPortalController@index')->name('v2.parceiros.portal.index');`
- **cms.php:** prefix `cms/parceiros`, middleware `CmsAuthenticate`, CRUD completo.

### Diferenças do Associados

| Aspecto | Associados | Parceiros |
|---|---|---|
| Redes sociais | Sim (LinkedIn, site, YouTube, etc.) | Não (só site) |
| Data "associado desde" | Sim | Não |
| Link "Saiba mais" | Não diretamente | Sim (usa o campo `url`) |
| Layout | Lista 2 colunas | Grid 2 colunas (mesmo visual) |

## 7. Detalhamento de Implementação V2

### a) ModuleProvider.php

```php
<?php

namespace V2\Modules\Publicacoes\Parceiros;

use V2\Core\Contracts\ModuleProviderInterface;

class ModuleProvider implements ModuleProviderInterface
{
    public function __construct()
    {
    }

    public function getConfig(): array
    {
        return [
            'title'          => 'Parceiros',
            'description'    => 'Gerenciamento de parceiros institucionais.',
            'icon'           => 'bi bi-handshake',
            'position'       => 4,
            'active_pattern' => ['cms/parceiros*'],
            'route'          => 'v2.parceiros.cms.index',
            'subModulesLevel1' => [
                [
                    'title'    => 'Todos os Parceiros',
                    'icon'     => 'bi bi-list-ul',
                    'route'    => 'v2.parceiros.cms.index',
                    'position' => 1,
                ],
                [
                    'title'    => 'Novo Parceiro',
                    'icon'     => 'bi bi-plus-circle',
                    'route'    => 'v2.parceiros.cms.create',
                    'position' => 2,
                ],
            ],
        ];
    }
}
```

### b) Migration (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 CreateV2ParceirosTable extends Migration
{
    public function up()
    {
        DB::beginTransaction();
        DB::statement("SET lock_timeout = '3s';");

        try {
            Schema::create('v2_parceiros', function (Blueprint $table) {
                $table->uuid('id')->primary();
                $table->string('name', 255);
                $table->string('slug', 255)->unique();
                $table->text('description')->nullable();
                $table->string('logo', 255)->nullable();
                $table->string('url', 255)->nullable();
                $table->integer('sort_order')->default(0);
                $table->boolean('is_active')->default(true);
                $table->string('created_by', 36)->nullable();
                $table->timestamps();
                $table->softDeletes();
                $table->index('is_active', 'idx_v2_parceiros_active');
                $table->index('sort_order', 'idx_v2_parceiros_sort');
            });

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

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

### c) Model (Parceiro.php)

```php
<?php

namespace V2\Modules\Publicacoes\Parceiros\Models;

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

class Parceiro extends Model
{
    use SoftDeletes;

    protected $table = 'v2_parceiros';

    public $incrementing = false;

    protected $keyType = 'string';

    protected $fillable = [
        'name', 'slug', 'description', 'logo', 'url',
        'sort_order', 'is_active',
    ];

    protected $casts = [
        'is_active' => 'boolean',
        'metadata'  => 'array',
    ];

    protected $dates = ['created_at', 'updated_at', 'deleted_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->name)) {
                $model->slug = str_slug($model->name);
            }
        });

        static::saving(function ($model) {
            // Garantir unicidade do slug
            $baseSlug = $model->slug ?: str_slug($model->name);
            $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;
        });

        static::saved(function ($model) {
            try {
                app(\V2\Services\Search\Services\PublicIndexerService::class)->index(
                    new \V2\Services\Search\DTOs\PublicSearchDTO(
                        $model->title ?? $model->name,
                        $model->slug,
                        'Parceiros',
                        'parceiro',
                        $model->id,
                        $model->description ?? '',
                        null,
                        ['tags' => []]
                    )
                );
            } catch (\Exception $e) {
                \Log::error('[Module] Falha ao sincronizar busca: ' . $e->getMessage());
            }
        });

        static::deleted(function ($model) {
            try {
                app(\V2\Services\Search\Services\PublicIndexerService::class)->remove('parceiro', $model->id);
            } catch (\Exception $e) {
                \Log::error('[Module] Falha ao remover da busca: ' . $e->getMessage());
            }
        });
    }

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

    public function scopeOrdered($query)
    {
        return $query->orderBy('sort_order')->orderBy('name');
    }
}
```

### d) CMS Controller (ParceirosCmsController.php)

```php
<?php

namespace V2\Modules\Publicacoes\Parceiros\Controllers;

use V2\Core\Controllers\BaseController;
use Illuminate\Http\Request;
use V2\Modules\Publicacoes\Parceiros\Models\Parceiro;
use V2\Services\ActivityLogger\ActivityLoggerService;
use V2\Services\SEO\Services\SEOService;
use Sentinel;
use Str;

class ParceirosCmsController 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 = Parceiro::query();

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

        if ($request->has('filter_active')) {
            $query->where('is_active', $request->input('filter_active') === '1');
        }

        $items = $query->orderBy('sort_order')->orderBy('name')->paginate(15);

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

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

    public function store(Request $request)
    {
        $data = $request->validate([
            'name'        => 'required|string|max:255',
            'slug'        => 'nullable|string|max:255',
            'description' => 'nullable|string',
            'logo'        => 'nullable|image|mimes:jpeg,png,jpg,webp|max:2048',
            'url'         => 'nullable|url|max:255',
            'sort_order'  => 'nullable|integer|min:0',
            'is_active'   => 'nullable|boolean',
        ]);

        $model = new Parceiro();
        $model->id = v2_uuid();
        $model->name = $data['name'];
        $model->slug = !empty($data['slug']) ? $data['slug'] : Str::slug($data['name']);
        $model->description = $data['description'] ?? null;
        $model->url = $data['url'] ?? null;
        $model->sort_order = $data['sort_order'] ?? 0;
        $model->is_active = $data['is_active'] ?? true;
        $model->author_id = Sentinel::getUser()->id ?? null;

        if ($request->hasFile('logo')) {
            $model->logo = $request->file('logo')->store('public/upload/parceiros');
        }

        $model->save();

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

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

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

    public function update(Request $request, $id)
    {
        $model = Parceiro::findOrFail($id);

        $data = $request->validate([
            'name'        => 'required|string|max:255',
            'slug'        => 'nullable|string|max:255|unique:v2_parceiros,slug,' . $id . ',id',
            'description' => 'nullable|string',
            'logo'        => 'nullable|image|mimes:jpeg,png,jpg,webp|max:2048',
            'url'         => 'nullable|url|max:255',
            'sort_order'  => 'nullable|integer|min:0',
            'is_active'   => 'nullable|boolean',
        ]);

        $model->name = $data['name'];
        if (!empty($data['slug'])) {
            $model->slug = $data['slug'];
        }
        $model->description = $data['description'] ?? null;
        $model->url = $data['url'] ?? null;
        $model->sort_order = $data['sort_order'] ?? 0;
        $model->is_active = $data['is_active'] ?? true;

        if ($request->hasFile('logo')) {
            $model->logo = $request->file('logo')->store('public/upload/parceiros');
        }

        $model->save();

        $this->activityLogger->log($model, 'updated', ['type' => 'parceiro_update']);

        flash('Parceiro atualizado com sucesso!')->success();
        return redirect()->route('v2.parceiros.cms.edit', ['id' => $model->id]);
    }

    public function destroy($id)
    {
        $model = Parceiro::findOrFail($id);
        $name = $model->name;
        $model->delete();

        $this->activityLogger->log($model, 'archived', ['type' => 'parceiro_archive']);

        flash('"' . $name . '" arquivado com sucesso!')->success();
        return redirect()->route('v2.parceiros.cms.index');
    }
}
```

### e) Portal Controller (ParceirosPortalController.php)

```php
<?php

namespace V2\Modules\Publicacoes\Parceiros\Controllers;

use V2\Core\Controllers\BaseController;
use V2\Modules\Publicacoes\Parceiros\Models\Parceiro;
use V2\Services\SEO\Services\SEOService;

class ParceirosPortalController extends BaseController
{
    /** @var SEOService */
    protected $seo;

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

    public function index()
    {
        $items = Parceiro::active()->ordered()->paginate(12);

        $this->seo->setTitle('Parceiros — ' . config('app.name'));
        $this->seo->setDescription('Parceiros institucionais da SUCESU Nacional.');

        return view('v2-parceiros::portal.index', compact('items'));
    }
}
```

### f) Rotas CMS (Routes/cms.php)

```php
<?php

Route::group([
    'middleware' => [\V2\Core\Middleware\CmsAuthenticate::class],
    'prefix'     => 'cms/parceiros',
], function () {

    Route::get('/', [
        'as'   => 'v2.parceiros.cms.index',
        'uses' => 'ParceirosCmsController@index',
    ]);

    Route::get('/novo', [
        'as'   => 'v2.parceiros.cms.create',
        'uses' => 'ParceirosCmsController@create',
    ]);

    Route::post('/novo', [
        'as'   => 'v2.parceiros.cms.store',
        'uses' => 'ParceirosCmsController@store',
    ]);

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

    Route::match(['PUT', 'POST'], '/editar/{id}', [
        'as'   => 'v2.parceiros.cms.update',
        'uses' => 'ParceirosCmsController@update',
    ]);

    Route::delete('/{id}', [
        'as'   => 'v2.parceiros.cms.destroy',
        'uses' => 'ParceirosCmsController@destroy',
    ]);
});
```

### g) Rotas Web (Routes/web.php)

```php
<?php

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

    Route::get('/parceiros', [
        'as'   => 'v2.parceiros.portal.index',
        'uses' => 'ParceirosPortalController@index',
    ]);
});
```

### h) View CMS (index.blade.php)

```blade
@extends('v2-cms::layouts.app')

@section('title', 'Parceiros')
@section('header-title', 'Parceiros')
@section('header-description', 'Gerenciamento de parceiros institucionais')

@section('content')
    <div class="card">
        <div class="card-header d-flex justify-content-between align-items-center">
            <form method="GET" class="d-flex gap-2">
                <input type="text" name="search" class="form-control form-control-sm"
                       placeholder="Buscar..." value="{{ request('search') }}">
                <button type="submit" class="btn btn-sm btn-outline-secondary">
                    <i class="bi bi-search"></i>
                </button>
            </form>
            @v2can('Parceiros', 'write')
                <a href="{{ route('v2.parceiros.cms.create') }}" class="btn btn-sm btn-primary">
                    <i class="bi bi-plus-circle"></i> Novo
                </a>
            @endv2can
        </div>
        <div class="card-body">
            @if(count($items) > 0)
                <table class="table table-hover">
                    <thead>
                        <tr>
                            <th>Logo</th>
                            <th>Nome</th>
                            <th>Ordem</th>
                            <th>Ativo</th>
                            <th>Ações</th>
                        </tr>
                    </thead>
                    <tbody>
                        @foreach($items as $item)
                        <tr>
                            <td>
                                @if($item->logo)
                                    <img src="{{ Storage::url($item->logo) }}" alt="{{ $item->name }}"
                                         style="max-height: 40px;">
                                @endif
                            </td>
                            <td>{{ $item->name }}</td>
                            <td>{{ $item->sort_order }}</td>
                            <td>
                                <span class="badge bg-{{ $item->is_active ? 'success' : 'secondary' }}">
                                    {{ $item->is_active ? 'Ativo' : 'Inativo' }}
                                </span>
                            </td>
                            <td>
                                <a href="{{ route('v2.parceiros.cms.edit', ['id' => $item->id]) }}"
                                   class="btn btn-sm btn-outline-primary">
                                    <i class="bi bi-pencil"></i>
                                </a>
                                <form action="{{ route('v2.parceiros.cms.destroy', ['id' => $item->id]) }}"
                                      method="POST" class="d-inline"
                                      onsubmit="return confirm('Arquivar este parceiro?');">
                                    {{ csrf_field() }}
                                    {{ method_field('DELETE') }}
                                    <button type="submit" class="btn btn-sm btn-outline-danger">
                                        <i class="bi bi-archive"></i>
                                    </button>
                                </form>
                            </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 parceiro encontrado.</p>
                </div>
            @endif
        </div>
    </div>
@endsection
```

### i) View Portal (index.blade.php)

```blade
@extends('v2-portal::layouts.app')

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

@section('content')
    <section class="parceiros-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="parceiro-card">
                        <div class="text-center">
                            @if($item->logo)
                                <img src="{{ Storage::url($item->logo) }}" alt="{{ $item->name }}"
                                     class="img-fluid parceiro-logo" style="max-height: 80px;">
                            @endif
                        </div>
                        <div class="mt-3">
                            <h3 class="text-center">{{ $item->name }}</h3>
                            <p class="text-muted">{{ Str::limit($item->description, 150) }}</p>
                            <div class="text-center mt-3">
                                @if($item->url)
                                    <a href="{{ $item->url }}" target="_blank" class="btn btn-outline-primary btn-sm">
                                        Saiba Mais <i class="fa-light fa-arrow-right-long"></i>
                                    </a>
                                @endif
                            </div>
                        </div>
                    </div>
                </div>
                @endforeach
            </div>
            {{ $items->links() }}
        </div>
    </section>
@endsection
```

### j) Dependências e Integrações

- **ActivityLoggerService**: Injetado no `ParceirosCmsController` para auditoria nas operações de escrita.
- **SEOService**: Injetado em ambos os controllers para metadados de página.
- **Storage**: Upload de logos para `public/upload/parceiros/`. Usar `Storage::url($model->logo)` na exibição pública.
- **Permissões**: `@v2can('Parceiros', 'write')` nas views CMS para controle de acesso.
- **Slug**: Gerado automaticamente via `Str::slug()` no boot do model.
