# Módulo Parceiros — Cadastro de Parceiros Institucionais

> **Namespace raiz:** `V2\Modules\Publicacoes\Parceiros`
> **Blade namespace:** `v2-parceiros::`
> **Tabela:** `v2_parceiros`

---

## Sumário

1. [Visão Geral](#visão-geral)
2. [Estrutura de Diretórios](#estrutura-de-diretórios)
3. [Model — Parceiro](#model--parceiro)
4. [Controllers](#controllers)
5. [Views CMS](#views-cms)
6. [Views Portal](#views-portal)
7. [Rotas](#rotas)
8. [Funcionalidades Implementadas](#funcionalidades-implementadas)
   - [ContentRichEditor na descrição](#contentricheditor-na-descrição)
   - [Split-screen editor com preview](#split-screen-editor-com-preview)
   - [Ativar/Desativar rápido na listagem](#ativardesativar-rápido-na-listagem)
   - [Modal no portal com detalhes](#modal-no-portal-com-detalhes)
   - [Logo via GalleryService](#logo-via-galleryservice)
9. [Auditoria e Activity Log](#auditoria-e-activity-log)

---

## Visão Geral

O módulo **Parceiros** gerencia os parceiros institucionais exibidos no portal. Cada registro representa um parceiro com nome, slug automático, logo (via GalleryService), descrição rica (ContentRichEditor), endereço, links de redes sociais e site.

### Fluxo de trabalho

```
CMS (CRUD) → v2_parceiros → Portal (/parceiros) → Modal com detalhes
```

---

## Estrutura de Diretórios

```
V2/Modules/Publicacoes/Parceiros/
├── Controllers/
│   ├── ParceirosCmsController.php         # CRUD + toggle + preview
│   └── ParceirosPortalController.php      # Página pública + detalhe AJAX
├── Migrations/
│   ├── 2024_01_01_000002_create_v2_parceiros_table.php
│   ├── 2024_01_01_000010_add_social_and_logo_fields_to_v2_parceiros_table.php
│   └── 2024_01_01_000020_add_address_fields_to_v2_parceiros_table.php
├── Models/
│   └── Parceiro.php
├── ModuleProvider.php
├── Routes/
│   ├── cms.php                            # Rotas administrativas
│   └── web.php                            # Rotas públicas
└── Views/
    ├── cms/
    │   ├── _form.blade.php                # Card único: identificação + links + localização
    │   ├── create.blade.php               # Criação com split-screen editor
    │   ├── edit.blade.php                 # Edição com split-screen + preview iframe (720px)
    │   ├── index.blade.php                # Listagem com toggle ativo/inativo
    │   └── preview.blade.php              # Preview no layout real do portal
    └── portal/
        ├── detalhe.blade.php              # Conteúdo do modal (carregado via AJAX)
        └── index.blade.php                # Grid de parceiros + modal interativo
```

---

## Model — Parceiro

**Arquivo:** `Models/Parceiro.php`

| Coluna | Tipo | Descrição |
|--------|------|-----------|
| `id` | string (UUID) | PK, não incrementável |
| `name` | string (255) | Nome do parceiro |
| `slug` | string (255) | Gerado automaticamente via `str_slug()` com dedup |
| `description` | text/null | JSON AST do ContentRichEditor |
| `logo_id` | string (36)/null | FK para `v2_gallery_media.id` (GalleryService) |
| `url` | string (255)/null | Site do parceiro |
| `linkedin_url` | string (255)/null | LinkedIn |
| `instagram_url` | string (255)/null | Instagram |
| `facebook_url` | string (255)/null | Facebook |
| `address` | text/null | Endereço completo (texto corrido) |
| `google_maps_url` | string(500)/null | Link do Google Maps |
| `sort_order` | integer | Ordenação automática (1-based) |
| `is_active` | boolean | Visível no portal? |
| `created_by` | string (36)/null | ID do usuário Sentinel |
| `created_at` / `updated_at` | timestamp | — |
| `deleted_at` | timestamp | Soft delete |

### Relacionamentos

| Método | Tipo | FK |
|--------|------|----|
| `galleryLogo()` | `belongsTo(GalleryMedia)` | `logo_id` |

> **Importante:** O método `logo()` foi renomeado para `galleryLogo()` para evitar conflito com a coluna legada `logo` (varchar) na tabela `v2_parceiros`. O acesso via `getAttribute('logo')` retornaria o valor NULL da coluna, não a relação.

### Scopes

| Scope | Query |
|-------|-------|
| `active()` | `WHERE is_active = true` |
| `ordered()` | `ORDER BY sort_order ASC, name ASC` |

### Eventos do Model

| Evento | Ação |
|--------|------|
| `creating` | Gera UUID via `v2_uuid()` |
| `saving` | Gera slug via `str_slug(name)` com dedup (`slug-N`) |
| `saved` | Sincroniza com índice público via `PublicIndexerService` |
| `deleted` | Remove do índice público |

---

## Controllers

### ParceirosCmsController

**Serviços injetados:** `ActivityLoggerService`, `SEOService`, `StorageService`

| Método | Rota | Ação |
|--------|------|------|
| `index()` | `GET /cms/parceiros` | Lista paginada com busca |
| `create()` | `GET /cms/parceiros/novo` | Formulário de criação |
| `store()` | `POST /cms/parceiros/novo` | Persiste novo parceiro |
| `edit($id)` | `GET /cms/parceiros/editar/{id}` | Formulário de edição com `galleryLogo` |
| `update()` | `PUT|POST /cms/parceiros/editar/{id}` | Atualiza parceiro |
| `destroy($id)` | `DELETE /cms/parceiros/{id}` | Soft delete |
| `reorder()` | `POST /cms/parceiros/reorder` | Reordena por array de IDs |
| `toggleActive($id)` | `POST /cms/parceiros/{id}/toggle` | Ativa/desativa + reindexa |
| `preview($id)` | `GET /cms/parceiros/preview/{id}` | Renderiza preview no layout do portal |

### ParceirosPortalController

**Serviços injetados:** `SEOService`, `StorageService`

| Método | Rota | Ação |
|--------|------|------|
| `index()` | `GET /parceiros` | Grid de parceiros ativos |
| `detalhe($id)` | `GET /parceiros/detalhe/{id}` | HTML do modal (AJAX) |

---

## Views CMS

### `index.blade.php` — Listagem com toggle

- Tabela com colunas: Logo (ícone), Nome (+ slug), Ações
- **Toggle ativo/desativo** com `bi-toggle-on` (verde) / `bi-toggle-off` (cinza) via AJAX
- Colunas "Ordem" e "Ativo" removidas (ativo migrado para toggle nas ações)
- Botão "Ver página" no header (abre `/parceiros` em nova aba)
- Busca por nome ou descrição

### `_form.blade.php` — Card único com seções

Card único dividido por `<hr>`:

| Seção | Campos |
|-------|--------|
| **Identificação** | Nome (obrigatório) + Slug (read-only, gerado automático) |
| **Links** | Site, LinkedIn, Instagram, Facebook com `input-group` e ícones |
| **Localização** | Endereço (textarea) + Google Maps (com `bi-map`) |

### `create.blade.php` — Split-screen editor

- **Topo:** `_form` + Logo (`media-input` em card separado)
- **Split (3fr 2fr):**
  - **Esquerda:** ContentRichEditor (Tiptap) para descrição
  - **Direita:** Preview header + "Salve para visualizar"
- **Rodapé:** Salvar + Cancelar

### `edit.blade.php` — Split-screen editor com preview real

- **Topo:** `_form` + Logo (`media-input` em card separado)
- **Split (3fr 2fr):**
  - **Esquerda:** ContentRichEditor
  - **Direita:** Preview header + iframe (720px, `min-height: 85vh`)
- **Rodapé:** Atualizar + Cancelar

### `preview.blade.php` — Preview no layout real do portal

Renderiza o parceiro dentro do layout `v2-portal::app-layout` simulando o modal:
- Fundo da página de Parceiros do portal (atenuado)
- Overlay semi-transparente escuro
- Card modal centralizado com logo, nome, endereço, maps, descrição rica e ícones sociais
- Barra fixa "Preview — o modal é exibido sobre a página de Parceiros do portal"

---

## Views Portal

### `portal/index.blade.php` — Grid de parceiros + modal

- Hero com breadcrumb
- Grid de cards (`col-md-6`) com:
  - Logo via componente `v2-service-media::components.picture` (com variantes WebP)
  - Nome
  - Descrição limitada (strip_tags + `str_limit(150)`)
  - Ícones de redes sociais (com `event.stopPropagation()`)
- Click no card abre modal Bootstrap (`#parceiroModal`)
- Modal carrega conteúdo via AJAX (`GET /parceiros/detalhe/{id}`)
- Fallback: placeholder com `bi-building` quando sem logo

### `portal/detalhe.blade.php` — Conteúdo do modal

Fragmento HTML (sem layout) retornado pelo AJAX:
- Logo (`picture` component, 160×160)
- Nome
- Endereço + link do Google Maps
- `<hr>` decorativo laranja
- Descrição rica renderizada via `ContentRichEditorRendererService`
- Ícones sociais: Site, LinkedIn, Instagram, Facebook (círculos `btn-outline-primary`)

---

## Rotas

### CMS (`Routes/cms.php`)

| Método | Rota | Nome |
|--------|------|------|
| GET | `/cms/parceiros` | `v2.parceiros.cms.index` |
| GET | `/cms/parceiros/novo` | `v2.parceiros.cms.create` |
| POST | `/cms/parceiros/novo` | `v2.parceiros.cms.store` |
| GET | `/cms/parceiros/editar/{id}` | `v2.parceiros.cms.edit` |
| PUT/POST | `/cms/parceiros/editar/{id}` | `v2.parceiros.cms.update` |
| DELETE | `/cms/parceiros/{id}` | `v2.parceiros.cms.destroy` |
| POST | `/cms/parceiros/reorder` | `v2.parceiros.cms.reorder` |
| POST | `/cms/parceiros/{id}/toggle` | `v2.parceiros.cms.toggle` |
| GET | `/cms/parceiros/preview/{id}` | `v2.parceiros.cms.preview` |

### Portal (`Routes/web.php`)

| Método | Rota | Nome |
|--------|------|------|
| GET | `/parceiros` | `v2.parceiros.portal.index` |
| GET | `/parceiros/detalhe/{id}` | `v2.parceiros.portal.detalhe` |

---

## Funcionalidades Implementadas

### ContentRichEditor na descrição

O campo `description` utiliza o componente `v2-service-contentricheditor::components.forms.content-rich-editor` (Tiptap) no split-screen. O valor é armazenado como JSON AST na coluna `description` (text). A renderização no portal usa `ContentRichEditorRendererService::toHtml()`.

### Split-screen editor com preview

Na criação/edição, o layout divide-se em:
- **Esquerda (3fr):** ContentRichEditor
- **Direita (2fr):** Logo (media-input) + preview iframe

O preview carrega `GET /cms/parceiros/preview/{id}` que renderiza o parceiro no layout real do portal com overlay simulando o modal.

### Ativar/Desativar rápido na listagem

Botão na coluna "Ações" que alterna entre:
- **Ativo:** `btn-outline-success` + `bi-toggle-on`
- **Inativo:** `btn-outline-secondary` + `bi-toggle-off`

Ao desativar:
- `is_active = false`, `sort_order = -1`
- Parceiros ativos com `sort_order > oldOrder` decrementam em 1

Ao reativar:
- `is_active = true`, `sort_order = max(sort_order dos ativos) + 1`

### Modal no portal com detalhes

Ao clicar num card de parceiro no portal:
1. Modal Bootstrap abre com spinner
2. AJAX GET para `/parceiros/detalhe/{id}` carrega o conteúdo
3. Modal exibe: logo, nome, endereço, maps, descrição rica, redes sociais
4. Links de redes sociais têm `event.stopPropagation()` para não abrir o modal

### Logo via GalleryService

A logo é gerenciada pelo `GalleryService` (tabela `v2_gallery_media`):
- Upload via `media-input` → gallery picker → AJAX para `POST /cms/v2/gallery`
- `logo_id` armazenado como FK string na `v2_parceiros`
- Relacionamento `galleryLogo()` → `belongsTo(GalleryMedia::class, 'logo_id')`
- URL gerada via `V2\Services\Storage\StorageService::url()`
- Portal renderiza com componente `v2-service-media::components.picture` (suporte a WebP)

### Slug automático

O slug é gerado automaticamente no evento `saving` do model via `str_slug($model->name)` com dedup (`slug-1`, `slug-2`, ...). Campo removido do formulário; exibido como read-only.

---

## Auditoria e Activity Log

| Operação | Tipo |
|----------|------|
| Criar parceiro | `parceiro_create` |
| Atualizar parceiro | `parceiro_update` |
| Arquivar (soft delete) | `parceiro_archive` |
| Ativar/Desativar | `parceiro_toggle` |
