---
id: MOD-CONEXOES-PARCEIROSINSTITUCIONAIS-CMS
tipo: modulo
onda: 4
status: concluido
unidades: [MOD-CONEXOES-PARCEIROSINSTITUCIONAIS]
fontes:
  - V2/Modules/Conexoes/ParceirosInstitucionais/
depende_de: [UI-CMS, CORE-CMS, SVC-MEDIA, SVC-CONTENTRICHEDITOR, CORE-AUTH]
consumidores: [UI-CMS, SVC-SEARCH]
criticidade: media
dono: ParceirosInstitucionais
---

# Conexões/ParceirosInstitucionais — UI CMS

> Anexo do módulo [`spec.md`](spec.md). Quatro telas e um partial: listagem com busca e
> dois botões de estado, formulário em duas seções (Metadados + Conteúdo), preview em
> iframe e um modal de tags compartilhado com o core. Não há editor de ordem, coluna de
> status, bulk action nem upload próprio — o logo usa o `media-input` do `SVC-MEDIA`.

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Permite criar, editar, ativar/desativar e arquivar parceiros, com tags, redes sociais, resumo, descrição rica e logo. |
| Entrada | Rotas `cms/parceiros-institucionais*`; cliques em toggle/arquivar; submits dos formulários. |
| Saída | HTML das 4 telas, modais Bootstrap, iframe de preview, JSON de toggle. |
| Efeitos colaterais | Flash de sessão, modal `created_item`, auditoria, reindexação, flush de cache. |
| Particularidade central | O formulário tem **duas fontes** para redes sociais (hidden inputs legados + JSON do builder) e o logo é apenas um `logo_id` opaco; a listagem não exibe miniatura nem ordem. |
| Como se integra | Views no namespace `v2-parceirosinstitucionais`; layout `v2-cms::layouts.app`; partials `filter-card`, `action-create`, `_confirm-modal`, `pagination`, `conexao-tags-modal`. |
| Se removido, quebra | Operação do módulo; o portal continua lendo os dados existentes. |
| Superfície pública | `cms.index`, `cms.create`, `cms.edit`, `cms.preview`; IDs de DOM `#tag-input`, `#tag-selected`, `#social-list`, `#social_media_json`, `#summary-count`, `#parceiroModal`. |

## 1. Efeito

### 1.1 O que o usuário consegue fazer

| Perfil | Ação | Resultado visível |
|---|---|---|
| Operador `read` | Abrir listagem | Tabela com Logo (ícone), Nome (link para editar + slug) e Ações; contador "N registros encontrados". |
| Operador `read` | Buscar | Filtra por nome/descrição via `?search=`; "Limpar" volta à URL atual sem query. |
| Operador `write` | Criar | Formulário vazio; salvar retorna ao índice com modal de sucesso. |
| Operador `write` | Editar | Form preenchido + preview em iframe; salvar mantém na edição e mostra flash. |
| Operador `write` | Toggle | Ícone alterna entre `bi-toggle-on` e `bi-toggle-off`; página recarrega. |
| Operador `write` | Arquivar | Botão `data-confirm-*` pretende abrir `#confirmModal` e submeter `DELETE`; **não há handler JS nesta página** (ver §3.5), então o clique não produz efeito no legado. |
| Operador `write` | Gerenciar tags | Modal `#conexaoTagModal` cria/exclui tags do core e atualiza a lista de disponíveis. |
| Operador `read` | Ver preview | Página isolada que simula o modal do portal sobre a página de parceiros. |

### 1.2 O que muda no sistema

| Efeito | Onde é produzido | Fonte |
|---|---|---|
| `created_item` na sessão (`type`, `title`, `route`) | store | `Controllers/ParceirosInstitucionaisCmsController.php:107-111` |
| Flash `Parceiro atualizado com sucesso!` | update | `:180` |
| Flash `Parceiro "X" arquivado com sucesso!` | destroy | `:198` |
| JSON de toggle + mensagem | toggle | `:245-249` |
| Modal de criação renderizado no índice | partial `creation-success-modal` | `V2/layouts/cms/partials/creation-success-modal.blade.php:1-2` |
| Tags criadas em `v2_conexao_tags` pelo modal | `ConexaoTagCmsController` | `V2/Core/Controllers/ConexaoTagCmsController.php:37-68` |
| Recarga da página no toggle | JS do índice | `Views/cms/index.blade.php:147-164` |

### 1.3 Contrato observável (entrada → saída)

| Entrada | Resultado |
|---|---|
| `GET .../novo` | HTML com `form action=store`, `csrf`, partial `_form` e bloco Conteúdo; sem preview. |
| Submit sem `name` | Volta ao form (302) com `name` inválido e mensagem `The name field is required.`; demais campos preservados por `old()`. |
| Submit com `logo_id` vazio no create | Logo não é gravado; `currentId` vazio. |
| Submit com `logo_id` vazio no edit | `logo_id = null`; preview mostra placeholder. |
| Clique em toggle | `POST` com `_token`; em sucesso, `location.reload()`; em erro, `alert('Erro ao alterar status. Tente novamente.')`. |
| Clique em arquivar | Sem handler `[data-confirm-modal]` na página, nada acontece; o `onsubmit confirm('Tem certeza que deseja realizar esta ação?')` só dispara se o form for submetido por outro caminho. |
| Clique em tag disponível | Adiciona/remove o badge e o hidden input correspondente. |

### 1.4 O que esta UI NÃO faz

- Não edita `sort_order`, `is_active` no formulário (só via toggle) nem `slug`.
- Não define categoria/tipo nem "destaque".
- Não faz upload direto: delega ao componente de mídia (galeria/cropper).
- Não lista arquivados, não restaura, não faz bulk delete.
- Não mostra toast/alerta de erro de rede no submit (apenas o toggle tem `alert`).

## 2. Abordagem

### 2.1 Estratégia em uma página

Duas telas de formulário compartilham o partial `_form` (Metadados) e repetem o bloco Conteúdo (Resumo + Descrição + Logo + Preview). Todo o comportamento dinâmico é JS inline (jQuery para toggle, DOM API para tags/social/contador), sem build step do módulo.

```mermaid
flowchart LR
  I[index] -->|Novo| C[create]
  I -->|Nome| E[edit]
  C --> F[_form Metadados]
  C --> G[Conteúdo: summary + editor + logo]
  E --> F
  E --> G
  E --> P[preview iframe]
  I -->|toggle| T[JSON + reload]
  I -->|arquivar| D[modal confirm]
```

### 2.2 Mecânica passo a passo

1. `index()` aplica busca opcional e ordena por `sort_order`/`created_at desc`, paginando 15 (`Controllers/...CmsController.php:32-47`).
2. A view monta `filter-card` com `searchPlaceholder` e `action`, e o partial injeta um `<form method="GET" role="search">` (`Views/cms/index.blade.php:33-38`; `V2/layouts/cms/partials/filter-card.blade.php:54-70`).
3. A tabela itera `$parceiros`; a célula de Logo só decide entre `bi-image` e `bi-handshake` conforme `logo_id` (`Views/cms/index.blade.php:53-59`).
4. O botão de toggle carrega `data-id`/`data-active`; o JS constrói a URL via `route(...,['id'=>'__ID__'])` e envia `POST` com `_token` (`:137-165`).
5. O arquivamento usa `<form method=POST>` com `@method('DELETE')`, `onsubmit="return confirm(...)"` e um botão que aciona o modal genérico (`:82-100`).
6. `create()` renderiza `cms.create`; o form inclui `_form` com `parceiro = null` e o bloco Conteúdo (`Views/cms/create.blade.php:124-207`).
7. `_form` inicializa `$selectedTags` com `old('tags', ...)`, monta o JSON de redes sociais a partir das colunas legadas ou do default de três redes (`Views/cms/_form.blade.php:1-17`).
8. `edit()` carrega `galleryLogo` e `tags`, calcula `$logoUrl` e renderiza `cms.edit` com iframe de preview (`Controllers/...CmsController.php:116-124`; `Views/cms/edit.blade.php:185-193`).
9. O editor rico é o componente `v2-service-contentricheditor::components.forms.content-rich-editor`, que grava JSON em um hidden `description` (`Views/cms/create.blade.php:155-163`; `V2/Services/ContentRichEditor/Views/components/forms/content-rich-editor.blade.php:1-17`).
10. O logo usa `v2-service-media::components.media-input` com `ownerModule='parceiros-institucionais'`, `accept='image/*'` e `currentId/currentUrl` (`Views/cms/create.blade.php:170-182`; `Views/cms/edit.blade.php:172-184`).
11. O contador de resumo atualiza `#summary-count` a cada `input` (`Views/cms/create.blade.php:210-223`).
12. `preview()` renderiza o layout do portal com todas as regiões desligadas e um overlay de modal fixo (`Controllers/...CmsController.php:252-266`; `Views/cms/preview.blade.php:5-15`).

### 2.3 Decisões de design e por quê

- **Partial `_form` só com Metadados** `[C]`: create/edit repetem o bloco Conteúdo porque o layout 60/40 difere do card de metadados (`create:129-197`; `edit:131-198`).
- **Slug somente leitura** `[C]`: exibe `/{{ $parceiro->slug ?? str_slug(old('name','nome-do-parceiro')) }}` e explica que é automático (`_form:50-57`).
- **Tags como badges + lista de disponíveis** `[C]`: permite criar por Enter e reutilizar por clique, sem endpoint de autocomplete (`_form:60-101,180-272`).
- **Builder de redes sociais com 8 opções** `[C]`: só três têm hidden input legado; as demais existem apenas em `metadata` (`_form:282-313`).
- **Preview em iframe com `sandbox`** `[C]`: isola CSS/JS do portal dentro do CMS (`edit:189-191`).
- **Modal de tags reutilizado** `[C]`: o partial do core recalcula `$tags` no próprio Blade, então o `@include` sem variável funciona (`V2/layouts/cms/partials/conexao-tags-modal.blade.php:1-6`).
- **Sem validação client-side além de `required`/`maxlength`/`type=url`** `[C]`: `novalidate` no form desliga a validação nativa, deixando tudo para o servidor (`create:124`; `edit:125`).

### 2.4 Trechos de código pivô

```blade
{{-- V2/Modules/Conexoes/ParceirosInstitucionais/Views/cms/_form.blade.php:1-17 — duas fontes
     de redes sociais: colunas legadas viram JSON inicial do builder --}}
@php
    $isEditing = isset($parceiro) && $parceiro->exists;
    $allTags = \V2\Core\Models\ConexaoTag::orderBy('name')->get();
    $selectedTags = old('tags', $isEditing && $parceiro->relationLoaded('tags') ? $parceiro->tags->pluck('name')->toArray() : []);

    $defaultSocial = [
        ['network' => 'linkedin', 'icon' => 'bi-linkedin', 'url' => '', 'order' => 1, 'active' => true],
        ['network' => 'facebook', 'icon' => 'bi-facebook', 'url' => '', 'order' => 2, 'active' => true],
        ['network' => 'instagram', 'icon' => 'bi-instagram', 'url' => '', 'order' => 3, 'active' => true],
    ];
    $socialInit = [];
    if ($parceiro->linkedin_url ?? '')  $socialInit[] = ['network'=>'linkedin', 'url'=>$parceiro->linkedin_url,  'icon'=>'bi-linkedin', 'order'=>1, 'active'=>true];
    if ($parceiro->facebook_url ?? '')  $socialInit[] = ['network'=>'facebook', 'url'=>$parceiro->facebook_url,  'icon'=>'bi-facebook', 'order'=>2, 'active'=>true];
    if ($parceiro->instagram_url ?? '') $socialInit[] = ['network'=>'instagram','url'=>$parceiro->instagram_url, 'icon'=>'bi-instagram','order'=>3, 'active'=>true];
    $socialInitJson = json_encode(!empty($socialInit) ? $socialInit : $defaultSocial);
@endphp
```

Na criação, `$parceiro` é `null`; o `??` evita o erro e o builder nasce com as três redes padrão **vazias**, que o controller filtra para `[]`. Na edição, qualquer rede nova (twitter etc.) que exista apenas em `metadata` **não** é pré-carregada aqui — o JSON inicial vem só das colunas legadas, mas é sobrescrito no primeiro `renderSocial()` pelo conteúdo do `textarea`, que já contém `$socialInitJson` (também só colunas legadas). `[C]` com consequência `[I]`: uma rede Twitter salva anteriormente não reaparece no builder até ser recriada.

```blade
{{-- V2/Modules/Conexoes/ParceirosInstitucionais/Views/cms/_form.blade.php:125-134 — o JSON
     enviado e os hidden inputs que sobrevivem sem JS --}}
<textarea name="social_media_json" id="social_media_json" style="display:none;" aria-label="JSON das redes sociais">{{ $socialInitJson }}</textarea>

<input type="hidden" name="linkedin_url" id="social_hidden_linkedin" value="{{ old('linkedin_url', $parceiro->linkedin_url ?? '') }}">
<input type="hidden" name="instagram_url" id="social_hidden_instagram" value="{{ old('instagram_url', $parceiro->instagram_url ?? '') }}">
<input type="hidden" name="facebook_url" id="social_hidden_facebook" value="{{ old('facebook_url', $parceiro->facebook_url ?? '') }}">
<div class="form-text mb-3"><i class="bi bi-info-circle me-1"></i> Links para as redes sociais do parceiro. Todos opcionais.</div>
```

O `<textarea>` fica `display:none` e é a fonte de `metadata.social_media`; os hidden inputs alimentam as colunas. Com JS desligado, o JSON nunca é atualizado, mas os hidden mantêm os valores antigos — resultado assimétrico descrito em RN-05/§3.3.

```blade
{{-- _form.blade.php:300-313 — sincronização JSON -> hidden (apenas 3 redes) --}}
function syncHiddenFields() {
    Object.keys(fieldMap).forEach(function(k) {
        var f = fieldMap[k];
        if (f.field) document.getElementById(f.field).value = '';
    });
    socialData.forEach(function(item) {
        var map = fieldMap[item.network];
        if (map && map.field) {
            document.getElementById(map.field).value = item.url || '';
        }
    });
}
```

O `fieldMap` declara `field: null` para twitter/youtube/whatsapp/telegram/tiktok: essas redes não têm coluna e existem apenas no JSON. O loop limpa os três hidden a cada render, então remover a linha do LinkedIn no builder limpa `linkedin_url` no próximo save.

```blade
{{-- V2/Modules/Conexoes/ParceirosInstitucionais/Views/cms/index.blade.php:137-165 — toggle
     com URL montada por placeholder --}}
var toggleUrlBase = '{{ route('v2.parceiros-institucionais.cms.toggle', ['id' => '__ID__']) }}';
var csrfToken = '{{ csrf_token() }}';

function buildToggleUrl(id) {
    return toggleUrlBase.replace('__ID__', id);
}

$(document).on('click', '.btn-toggle-active', function() {
    var $btn = $(this);
    var id = $btn.data('id');
    $.ajax({
        url: buildToggleUrl(id),
        method: 'POST',
        data: { _token: csrfToken },
        success: function() { location.reload(); },
        error: function() { alert('Erro ao alterar status. Tente novamente.'); }
    });
});
```

O `route()` com `'__ID__'` evita hardcode de prefixo; `data-id` vem do UUID. O reload descarta qualquer feedback do JSON (a mensagem do servidor nunca é exibida nesta tela). Delegação em `document` permite funcionar após qualquer re-render.

```blade
{{-- V2/Modules/Conexoes/ParceirosInstitucionais/Views/cms/preview.blade.php:5-15 — preview
     usa o layout do portal com regiões desligadas --}}
@extends('v2-portal::app-layout', [
    'pageTitle'             => 'Preview — ' . $parceiro->name,
    'fullWidthMain'         => true,
    'showHeader'            => false,
    'showMainHeader'        => false,
    'showMainFooter'        => false,
    'showFooter'            => false,
    'showAccessibilityBar'  => false,
    'showMainLeftArea'      => false,
    'showMainRightArea'     => false,
])
```

O preview é uma página inteira, não um fragmento: o `edit` o carrega em iframe. Ele inclui CSS próprio, overlay fixo de modal e o aviso inferior "Preview — o modal é exibido sobre a página de Parceiros do portal" (`:19-65,158-160`).

### 2.5 Algoritmos, consultas e custos

| Item | Custo | Observação |
|---|---|---|
| `ConexaoTag::orderBy('name')->get()` no `_form` | 1 query por render | sem cache. |
| `$allTags->isNotEmpty()` + dois `foreach` | O(n) DOM | tags disponíveis podem ser muitas. |
| `renderTags()` / `renderSocial()` | re-render total do container | O(n) por alteração, com rebind de listeners. |
| Preview em iframe | request autenticado completa | sem cache; renderiza rich text. |
| Listagem | 15 linhas | ícones, sem imagens (logo não é carregado). |
| `filter-card` | 1 script inline por render | ids derivados de `md5(action+searchName)`. |

## 3. Particularidades

### 3.1 O que só existe aqui

- **Bloco Conteúdo duplicado em create/edit** em vez de um segundo partial (CSS idêntico em ambos, ~106 linhas cada).
- **Preview de modal do portal dentro do CMS**, com layout de portal em modo "todas as regiões desligadas".
- **Dois caminhos para redes sociais** no mesmo formulário, com colunas legadas e JSON.
- **Listagem sem miniatura e sem status textual**: o estado do toggle só é visível pelo ícone.

### 3.2 Invariantes e regras não óbvias

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| RN-PARCEIROSINSTITUCIONAIS-29 | O formulário tem `novalidate`; `required`/`maxlength` são apenas dicas de UI. | Todo limite é revalidado no servidor (ou não existe, como `description`). | `Views/cms/create.blade.php:124`; `edit:125` |
| RN-PARCEIROSINSTITUCIONAIS-30 | `logo_id` é um hidden preenchido pelo `media-input`; não há `<input type=file name=logo>`. | Sem o JS do serviço de mídia, o campo chega vazio e limpa o logo no update. | `_form:170-182`; `Controllers/...CmsController.php:164-168` |
| RN-PARCEIROSINSTITUCIONAIS-31 | O slug exibido usa `str_slug(old('name'))` quando não há model. | O texto pode não corresponder ao slug final (desambiguação por contador). | `_form:54` |
| RN-PARCEIROSINSTITUCIONAIS-32 | A tabela lista todos (ativos e inativos), sem badge de status; o único sinal é o ícone do toggle. | Operador pode arquivar um já inativo sem perceber. | `Views/cms/index.blade.php:42-104` |
| RN-PARCEIROSINSTITUCIONAIS-33 | O contador de resumo lê `textarea.value.length`, mas o limite real é validado no servidor. | Colar 251 caracteres mostra `251/250` e só falha no submit. | `create:210-223`; `Controllers/...CmsController.php:59` |
| RN-PARCEIROSINSTITUCIONAIS-34 | O botão de arquivar é `type=button` com `data-confirm-modal`/`data-confirm-form-id`; o handler genérico do modal só existe em `Views/cms/includes/scripts.blade.php` da unidade Notícias. | Na tela de parceiros o botão fica inerte; o `DELETE` só ocorre por submit manual/JS externo. | `index:88-100`; `V2/Modules/Publicacoes/Noticias/Views/cms/includes/scripts.blade.php:159-190` |
| RN-PARCEIROSINSTITUCIONAIS-35 | `#conexaoTagModal` é incluído fora do `@section('content')`. | O modal é renderizado no fim do documento, dependendo do stack do layout. | `index:132-135` |
| RN-PARCEIROSINSTITUCIONAIS-36 | Tags disponíveis são recarregadas por `fetch` só quando o modal chama `window.refreshAvailableTags()`. | Criar tag no modal não atualiza a lista automaticamente se o contrato do modal não chamar a função. | `_form:247-272` |

### 3.3 Casos-limite

- **Edição sem tags**: `$selectedTags = []`, container vazio; o picker funciona.
- **Título com aspas**: usado em `data-confirm-body='Deseja arquivar "{{ $item->name }}"?'` e em `title` — Blade escapa HTML, mas aspas simples no nome podem quebrar o atributo (não há `e()` explícito com `ENT_QUOTES` garantido no atributo).
- **Muitas tags**: a lista de disponíveis cresce sem busca/paginação.
- **Rich text vazio**: hidden envia `{"type":"doc","content":[]}`; o portal renderiza string vazia? `getHtmlDescription` retorna o HTML do documento vazio (provavelmente `<p></p>`), e o bloco é renderizado porque `!empty($description)` é verdadeiro.
- **Falha de rede no toggle**: `alert` fixo, sem retry.
- **JS desabilitado**: `tags[]` não é enviado (os hidden são gerados em `#tag-hidden-inputs`) e o save desvincula todas as tags; `social_media_json` fica no default vazio, mas os hidden `linkedin_url`/`facebook_url`/`instagram_url` preservam as colunas legadas.
- **Duplo clique em Salvar**: dois POSTs; o segundo cria um parceiro duplicado com slug `-1` (store não é idempotente).
- **Preview de parceiro com descrição inválida**: 500 dentro do iframe (o iframe mostra erro do navegador).

### 3.4 Armadilhas de reimplementação

1. **Não trocar `logo_id` por upload direto**: o `<input hidden>` é preenchido pelo `SVC-MEDIA`; um `<input type=file>` exigiria endpoint e storage próprios.
2. **Não remover os hidden inputs legados** sem remover a validação `linkedin_url`/`instagram_url`/`facebook_url` e a escrita correspondente no controller.
3. **`old('tags')` é uma lista de nomes, não de IDs**: o servidor espera nomes e resolve por slug; enviar IDs criaria tags com slug numérico.
4. **`$parceiro` pode ser `null` no `_form`**: qualquer acesso direto (`$parceiro->x`) precisa de `??`; o partial já segue esse padrão.
5. **A ordem dos cards no DOM do builder de redes sociais define `order`**: `syncSocialJson` reatribui `order = idx+1`; o portal usa a ordem do array.

### 3.5 Divergências e dívidas

- **CSS do bloco Conteúdo duplicado** em `create.blade.php:7-113` e `edit.blade.php:8-114` (idêntico).
- **`pageTitle` do preview** não é o `<title>` real: o layout do portal usa `SEOService` (`preview:6`).
- **Comentário "Replica a estrutura do módulo de Notícias"** nos dois arquivos, sem partial compartilhado (`create:9-12`).
- **`$logoUrl` do controller de preview** é frágil: lê a coluna `logo` como objeto; nula → `null`, preenchida → notice/`TypeError`. A view nem usa a variável (`Controllers/...CmsController.php:262-266`).
- **`_form` busca todas as tags no Blade**, não no controller — acoplamento view→model (`_form:3`).
- **Arquivamento sem confirmação funcional**: o partial `v2-cms::partials._confirm-modal` é só markup; o único JS que trata `[data-confirm-modal]` está na página de Notícias (`V2/Modules/Publicacoes/Noticias/Views/cms/includes/scripts.blade.php:159-190`). O índice de parceiros inclui o partial e usa os atributos, mas não inclui o handler — o botão `type=button` não submete nada.

## 4. Integrabilidade

### 4.1 Superfície de UI

| Componente | Namespace | Props obrigatórias | Props opcionais | Consumidor |
|---|---|---|---|---|
| `cms._form` | `v2-parceirosinstitucionais` | `parceiro` (model ou `null`) | — | create, edit |
| `cms.create` | idem | — | — | rota create |
| `cms.edit` | idem | `parceiro`, `logoUrl` | — | rota edit |
| `cms.index` | idem | `parceiros` (paginator) | `searchTerm` via request | rota index |
| `cms.preview` | idem | `parceiro`, `htmlContent`, `logoUrl` | — | rota preview |
| `media-input` | `v2-service-media` | `name` | `label`, `accept`, `required`, `currentId`, `currentUrl`, `currentAlt`, `ownerModule` | create/edit |
| `content-rich-editor` | `v2-service-contentricheditor` | `name` | `label`, `value`, `placeholder`, `height`, `required` | create/edit |
| `filter-card` | `v2-cms` | — | `searchPlaceholder`, `searchValue`, `action` | index |
| `conexao-tags-modal` | `v2-cms` | — | — (recalcula `$tags`) | index |

### 4.2 Contrato de UI/JS

| Elemento | ID/classe | Evento | Efeito |
|---|---|---|---|
| Picker de tags | `#tag-input`, `#btn-add-tag`, `#tag-selected`, `#tag-hidden-inputs`, `#tag-input-error` | `keydown Enter`, `click` | adiciona/remove nomes; regenera `input[name="tags[]"]` |
| Tag disponível | `.tag-available[data-name]` | `click` | alterna inclusão |
| Builder social | `#social-list`, `#btn-add-social`, `#social_media_json` | `click`, `change`, `input` | mantém JSON e hidden sincronizados |
| Contador | `#summary`, `#summary-count` | `input` | atualiza `N/250` |
| Toggle da lista | `.btn-toggle-active[data-id]` | `click` (jQuery delegado) | `POST` + reload |
| Modal de arquivar | `[data-confirm-modal]` + `data-confirm-*` | `click` | contrato pretendido: abrir `#confirmModal` e submeter o form; **sem inicializador nesta página** |
| Modal de tags | `#conexaoTagModal`, `#new-tag-name`, `#btn-create-tag`, `.btn-delete-tag` | `click` | cria/exclui tag via core |

O partial do modal de tags depende de um contrato do core: `POST /cms/conexao-tags`, `GET /cms/conexao-tags/json`, `DELETE /cms/conexao-tags/{id}` (`V2/Core/Routes/cms.php:30-33`).

### 4.3 Ordem e ciclo de vida

1. `@push('css')` e `@push('scripts')` do `_form` são resolvidos pelo layout `v2-cms::layouts.app`; o `_form` é incluído **depois** do `@section('page-breadcrumbs')`.
2. `DOMContentLoaded` inicializa tags, social e contador; scripts jQuery do índice rodam em `$(document).ready`.
3. O modal `#conexaoTagModal` é incluído após `@endsection`, no escopo do layout.
4. O iframe de preview dispara uma requisição autenticada a cada abertura da tela de edição.

### 4.4 Receitas de uso

**Adicionar um campo ao formulário (padrão do módulo):**

```blade
{{-- em _form.blade.php, dentro do card Metadados --}}
<div class="col-md-4">
    <label for="fundacao" class="form-label fw-semibold">Fundação</label>
    <input type="text" id="fundacao" name="fundacao" class="form-control"
           value="{{ old('fundacao', $parceiro->fundacao ?? '') }}">
</div>
```

Requer coluna/migration, regra em `store`/`update`, `$fillable` (ou atribuição explícita) e exibição no portal.

**Renderizar o formulário em outro contexto:**

```blade
@include('v2-parceirosinstitucionais::cms._form', ['parceiro' => $parceiro])
```

**Criar tag a partir de um botão externo:**

```js
fetch('{{ route("v2.cms.conexao-tags.store") }}', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
  body: JSON.stringify({ name: 'Platina' })
}).then(r => r.json()).then(d => { if (!d.success) console.warn(d.message); });
```

### 4.5 Anti-uso

1. **Enviar `tags` como CSV (`"a,b"`)**: a regra `tags => array` falha com `The tags must be an array.`
2. **Contar com o padrão `select multiple` para tags**: não existe; o servidor espera `tags[]` de strings, gerados por JS.
3. **Usar `#parceiroModal` (portal) dentro do CMS**: esse modal existe apenas na view pública; no CMS o preview usa overlay próprio.

### 4.6 Custos e limites

- Render do `_form`: 1 query de tags + O(n) elementos.
- Preview: request completa por abertura.
- Upload de logo: limites do `SVC-MEDIA` (não definidos aqui).
- Sem paginação de tags disponíveis.

## 5. Rotas e contratos HTTP

| Tipo | Método | URI | Nome | Middleware | Controller@ação | Permissão | Efeito | Fonte |
|---|---|---|---|---|---|---|---|---|
| tela | GET | `/cms/parceiros-institucionais` | `...cms.index` | `CmsAuthenticate` + `module.active` | `@index` | `read` | listagem + filtros + modais | `Routes/cms.php:8-11` |
| tela | GET | `/cms/parceiros-institucionais/novo` | `...cms.create` | idem | `@create` | `read` | form vazio | `:13-16` |
| ação | POST | `/cms/parceiros-institucionais/novo` | `...cms.store` | idem + CSRF | `@store` | `write` | grava + 302 | `:18-21` |
| tela | GET | `/cms/parceiros-institucionais/editar/{id}` | `...cms.edit` | idem | `@edit` | `read` | form + preview | `:23-26` |
| ação | PUT/POST | `/cms/parceiros-institucionais/editar/{id}` | `...cms.update` | idem + CSRF | `@update` | `write` | grava + flash | `:28-31` |
| ação | DELETE | `/cms/parceiros-institucionais/{id}` | `...cms.destroy` | idem + CSRF | `@destroy` | `write` | soft delete | `:33-36` |
| ação | POST | `/cms/parceiros-institucionais/{id}/toggle` | `...cms.toggle` | idem + CSRF | `@toggleActive` | `write` | JSON | `:43-46` |
| tela | GET | `/cms/parceiros-institucionais/preview/{id}` | `...cms.preview` | idem | `@preview` | `read` | página de preview | `:48-51` |
| tela (core) | GET/POST/DELETE | `/cms/conexao-tags*` | `v2.cms.conexao-tags.*` | `CmsAuthenticate` | `ConexaoTagCmsController` | ≥1 papel | CRUD de tags | `V2/Core/Routes/cms.php:30-33` |

## 6. Dados e modelo de domínio

Mapeamento formulário → coluna está em [`dados.md`](dados.md) §6.1. Resumo: `name`, `summary`, `description`, `url`, `address`, `google_maps_url`, `logo_id`, `linkedin_url`, `instagram_url`, `facebook_url` → colunas; `social_media_json` → `metadata.social_media`; `tags[]` → pivô; `sort_order`/`is_active`/`created_by`/`slug` não vêm do formulário.

## 7. Fluxos e estados

```mermaid
sequenceDiagram
  participant U as Operador
  participant V as edit.blade
  participant C as CmsController
  participant M as Parceiro
  U->>V: abre /editar/{id}
  V->>C: GET (edit)
  C->>M: with(galleryLogo,tags)->findOrFail
  C-->>V: parceiro + logoUrl
  V->>V: iframe GET /preview/{id}
  U->>V: altera campos + Salvar
  V->>C: PUT /editar/{id}
  C->>M: valida, atribui, save(), syncTags
  C-->>U: 302 /editar/{id} + flash
```

`UI-PARCEIROSINSTITUCIONAIS-01` (estado vazio da lista): sem registros → ícone `bi-handshake`, título "Nenhum parceiro encontrado", CTA "Criar Primeiro Parceiro".
`UI-PARCEIROSINSTITUCIONAIS-02` (busca sem resultado): mesma tela com "Tente ajustar os filtros ou limpar a busca." e botão "Limpar Filtros".

## 8. UI CMS

### 8.1 `cms.index`

| Item | Valor |
|---|---|
| Rota/permissão | `GET /cms/parceiros-institucionais`; `read` |
| Layout | `v2-cms::layouts.app`, breadcrumbs "Conexões › Parceiros" |
| Ações de topo | "Ver página" (`target=_blank` para o portal), "Gerenciar Tags" (modal), "Novo Parceiro" |
| Contador | `{{ $parceiros->total() }} registros encontrados` |
| Filtro | GET `search` com placeholder "Buscar por nome ou descrição..."; botões Filtrar/Limpar |
| Colunas | Logo (ícone), Nome (link + slug em `<small>`), Ações |
| Ações por linha | Toggle (`bi-toggle-on/off`), Editar (`bi-pencil`), Arquivar (`bi-archive`, inerte sem handler do modal) |
| Ordenação | `sort_order` ASC, `created_at` DESC; sem controle na UI |
| Paginação | 15, partial `v2-cms::partials.pagination`, preserva query |
| Bulk actions | nenhuma |
| Modais | `#confirmModal` (genérico), `#conexaoTagModal` |
| Mensagens | vazio: "Nenhum parceiro encontrado"; busca vazia: "Tente ajustar os filtros ou limpar a busca."; erro de toggle: `alert('Erro ao alterar status. Tente novamente.')` |
| A11y | `aria-label="Lista de parceiros"` na tabela; `title` nos botões; `aria-label` no fechar do modal |

### 8.2 `cms.create` / `cms.edit`

| Campo | Tipo | Obrigatório | Default | Validação server | Ajuda/exibição |
|---|---|---|---|---|---|
| `name` | text (maxlength 255) | sim | — | `required|string|max:255` | "Nome oficial do parceiro ou empresa." |
| slug | div somente leitura | — | `str_slug(old('name'))` | — | "Gerado automaticamente a partir do nome." |
| `tags` | picker | não | tags atuais | `array`, itens `string|max:100` | "Clique em uma tag disponível para adicioná-la." |
| `url` | url | não | — | `nullable|string|max:255` | placeholder `https://www.exemplo.com.br` |
| `social_media_json` | textarea oculto | não | 3 redes vazias | sem regra | builder com 8 redes |
| `linkedin_url`/`instagram_url`/`facebook_url` | hidden | não | coluna | `nullable|string|max:255` | sincronizados pelo JS |
| `address` | textarea (2 linhas) | não | — | `nullable|string` | "Endereço completo do parceiro..." |
| `google_maps_url` | url | não | — | `nullable|string|max:500` | "Link da localização no Google Maps." |
| `summary` | textarea (3 linhas, 250) | não | — | `nullable|string|max:250` | contador N/250 + "Texto puro corrido..." |
| `description` | rich editor (400px) | não | `{"type":"doc","content":[]}` | `nullable|string` | "Descreva o parceiro..." |
| `logo_id` | media-input (`image/*`) | não | vazio / atual | `nullable|string|max:36` | label "Logo"; preview 40% |

Layout: card Metadados (largura total) → card Conteúdo com grid `3fr 2fr` (Resumo+Descrição | Logo+Preview) → botões Salvar/Atualizar + Cancelar. Em `edit`, o preview é um iframe imediatamente carregado; em `create`, placeholder "Salve o parceiro para ver o preview completo.".

### 8.3 `cms.preview`

Layout do portal com `fullWidthMain=true` e todas as regiões desligadas, overlay escuro, card central com logo 160×160 (ou ícone), nome, endereço + "Ver no mapa", divisor laranja, descrição rica centralizada, ícones sociais (site, LinkedIn, Instagram, Facebook) e aviso fixo inferior. Não há botão funcional (o `btn-close` é `disabled`).

### 8.4 Acessibilidade

- Labels explícitos para todos os campos visíveis; `aria-label` no textarea de JSON e no input de tag.
- `aria-live="polite"` no contador de resumo.
- `aria-label` nos botões de remover tag e no seletor de rede social; `aria-expanded` no filtro avançado (não usado aqui).
- Tabela com `aria-label`; ícones decorativos sem texto alternativo (usam `title` no botão).
- Foco: o modal de tags do core não implementa trap de foco próprio (depende do Bootstrap).
- Contraste: badges `bg-info` com texto branco; placeholders em `#94a3b8`.

## 9. UI Portal

Não se aplica a este anexo; ver [`portal.md`](portal.md).

## 10. Configuração, permissões e flags

- Permissão `ParceirosInstitucionais` (`none`/`read`/`write`) aplicada por `V2PermissionMiddleware`; `read` bloqueia POST/PUT/DELETE (`V2/Core/Middleware/V2PermissionMiddleware.php:80-127`).
- `module.active:ParceirosInstitucionais` aplicado pelo provider (`V2/Core/Providers/V2ServiceProvider.php:303-307`).
- `ownerModule = 'parceiros-institucionais'` no `media-input` separa os arquivos na galeria (`Views/cms/create.blade.php:177`).
- Não há config própria do módulo.

## 11. Observabilidade, erros e limites

- Flash messages: `Parceiro atualizado com sucesso!`, `Parceiro "X" arquivado com sucesso!`.
- Modal `created_item`: `type=Parceiro`, `title=name`, `route=index`.
- Erros de validação inline via `$errors->has()` para `name`, `summary`, `address`, `google_maps_url`.
- Erro de toggle só no `alert`.
- Sem log de UI; escritas auditadas no servidor.
- Limites: `name` 255, `summary` 250, `google_maps_url` 500, tag 100.

## 12. Seeds e dados iniciais

Não se aplica: a UI não semeia dados. Os registros iniciais vêm de `DemoContentSeeder` (8 parceiros) e os logos de `PopulateMissingImages` — ver [`dados.md`](dados.md) §12.

## 13. Lacunas

- `LG-PARCEIROSINSTITUCIONAIS-13` — Não há evidência de que o builder social pré-carregue redes que existam apenas em `metadata.social_media` (twitter etc.) na edição; o código sugere que não. Onde procurar: teste manual em ambiente com dados. Impacto: médio (perda de rede ao salvar).
- `LG-PARCEIROSINSTITUCIONAIS-14` — O contrato exato do modal `conexao-tags-modal` (se chama `refreshAvailableTags`) está no core; não foi lido integralmente aqui. Onde procurar: `V2/layouts/cms/partials/conexao-tags-modal.blade.php:60-200`. Impacto: baixo.
- `LG-PARCEIROSINSTITUCIONAIS-15` — Não há testes de UI; comportamento de colagem/quebra do editor rico não documentado. Impacto: baixo.

## 14. Checklist de paridade

- [ ] `PC-PARCEIROSINSTITUCIONAIS-38` — Listagem mostra `N registros encontrados`, três colunas e paginação 15. Fonte: `Views/cms/index.blade.php:31-113`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-39` — Busca `?search=pb` filtra por nome/descrição e o botão Limpar remove a query. Fonte: `Controllers/...CmsController.php:34-42`; `filter-card.blade.php:79-83`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-40` — Form create contém exatamente um `textarea[name=summary]`, um hidden `description` e um hidden `logo_id`. Fonte: `Views/cms/create.blade.php:142-182`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-41` — Editar preenche o iframe com `/cms/.../preview/{id}`. Fonte: `Views/cms/edit.blade.php:188-191`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-42` — Adicionar tag por Enter cria um `input[name="tags[]"]` correspondente. Fonte: `_form.blade.php:190-197,230-237`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-43` — Adicionar rede social cria `<li class="social-row">` com select + url + remover. Fonte: `_form.blade.php:315-342`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-44` — Remover LinkedIn no builder e salvar limpa `linkedin_url`. Fonte: `_form.blade.php:300-313`; `Controllers/...CmsController.php:145`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-45` — Contador exibe `0/250` no vazio e acompanha a digitação. Fonte: `create.blade.php:210-223`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-46` — O botão Arquivar emite `DELETE` (por submit programático ou handler restaurado) e produz o flash `Parceiro "NOME" arquivado com sucesso!`; no legado o handler do modal não está presente nesta tela. Fonte: `index.blade.php:88-100`; `Controllers/...CmsController.php:198-200`; `V2/Modules/Publicacoes/Noticias/Views/cms/includes/scripts.blade.php:159-190`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-47` — Preview renderiza aviso "Preview — o modal é exibido sobre a página de Parceiros do portal". Fonte: `preview.blade.php:158-160`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-48` — Estado vazio oferece "Criar Primeiro Parceiro". Fonte: `index.blade.php:123-128`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-49` — Editar com `logo_id` vazio remove o logo e o preview mostra placeholder. Fonte: `Controllers/...CmsController.php:164-168`; `preview.blade.php:93-98`.

## 15. Evidências consultadas

| Arquivo | Faixas |
|---|---|
| `V2/Modules/Conexoes/ParceirosInstitucionais/Views/cms/_form.blade.php` | 1-389 |
| `V2/Modules/Conexoes/ParceirosInstitucionais/Views/cms/create.blade.php` | 1-223 |
| `V2/Modules/Conexoes/ParceirosInstitucionais/Views/cms/edit.blade.php` | 1-224 |
| `V2/Modules/Conexoes/ParceirosInstitucionais/Views/cms/index.blade.php` | 1-167 |
| `V2/Modules/Conexoes/ParceirosInstitucionais/Views/cms/preview.blade.php` | 1-161 |
| `V2/Modules/Conexoes/ParceirosInstitucionais/Controllers/ParceirosInstitucionaisCmsController.php` | 32-307 |
| `V2/layouts/cms/partials/filter-card.blade.php` | 54-132 |
| `V2/layouts/cms/partials/action-create.blade.php` | 1-15 |
| `V2/layouts/cms/partials/conexao-tags-modal.blade.php` | 1-60 |
| `V2/Services/Media/Views/components/media-input.blade.php` | 1-50 |
| `V2/Services/ContentRichEditor/Views/components/forms/content-rich-editor.blade.php` | 1-17 |
| `V2/Core/Controllers/ConexaoTagCmsController.php` | 1-111 |
| `V2/Core/Routes/cms.php` | 25-35 |
