---
id: MOD-CONEXOES-ASSOCIADOS-CMS
tipo: modulo
onda: 4
status: concluido
unidades: [MOD-CONEXOES-ASSOCIADOS]
fontes:
  - V2/Modules/Conexoes/Associados/
depende_de: [CORE-CMS, CORE-AUTH, SVC-MEDIA, SVC-GALLERY, SVC-CONTENTRICHEDITOR, MOD-SUPPORT-SUPPORTCONFIGS]
consumidores: [UI-CMS]
criticidade: alta
dono: Associados
---

# Conexões/Associados — UI CMS

> Quatro telas (listagem reaproveitada para ativas e arquivadas, criação, edição e
> preview). O formulário é único (`_form.blade.php`) e concentra picker de tags
> textual, regional travada para operador, builder de redes sociais em JSON, lista de
> benefícios, `media-input` para logo/capa, editor Tiptap e galeria
> `media-group-input`. **Não há campo de SEO, nem checkbox de ativo, nem campo de
> ordenação.**

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Permitir que um operador cadastre, classifique, revista e arquive instituições conveniadas, com upload de imagem integrado à Galeria V2, e visualize o resultado no layout real do portal antes de salvar. |
| Entrada | 7 telas/rotas de leitura e 5 de escrita (§5); campos de §8.2; eventos `media-selected.lotep`, `change`, `keydown`, `show.bs.modal` do Bootstrap. |
| Saída | HTML com cards de seção, badges de erro, modais (`conexaoTagModal`, `bgImageModal`), iframe de preview; `302` com flash; `activity_log`. |
| Efeitos colaterais | Query de tags do Core no render do modal; query de tags próprias da listagem; montagem de URLs de storage no render; upload AJAX para `v2.gallery.store` no `media-input`; `POST` de imagem de fundo. |
| Particularidade central | O botão "Gerenciar Tags" abre um modal que escreve em `v2_conexao_tags`, mas o seletor de tags do formulário lê `v2_associado_tags`; não há `window.refreshAvailableTags` nesta tela, então o modal não atualiza nada do domínio (`index.blade.php:38-49`; `_form.blade.php:74-84`). |
| Como se integra | `v2-associados::cms.instituicoes.{index,create,edit,preview}`; `v2-cms::layouts.app`; partials `filter-card`, `pagination`, `status-badge`, `action-create`, `activity-footer`, `conexao-tags-modal`; `v2-service-media::components.{media-input,media-group-input}`; `v2-service-contentricheditor::components.forms.content-rich-editor`. |
| Se removido, quebra | O único caminho de escrita de instituições; o preview do layout; o modal de fundo; a listagem de arquivadas. |
| Superfície pública | As quatro blades e os IDs/eventos listados em §4.2. |

## 1. Efeito

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

- **Listar** instituições com busca, filtro por tag, contagem total, logo, nome, badge, até 3 tags + `+N`, regional, data, status, link para o portal, editar e arquivar/restaurar.
- **Criar**: preencher metadados, tags, regional, site, redes, badge, benefícios, logo/capa, descrição rápida e detalhada.
- **Editar**: tudo acima + galeria + preview em iframe + rodapé de atividades.
- **Gerenciar tags** (modal do Core) e **imagem de fundo** da página pública (modal próprio).
- **Arquivar/restaurar** com confirmação nativa.

### 1.2 O que muda no sistema

| Efeito | Onde | Fonte |
|---|---|---|
| Listagem aplica escopo regional e filtros no servidor | `index()` | `InstituicaoCmsController.php:47-70` |
| Formulário carrega todas as tags próprias e regionais permitidas | `create()`/`edit()` | `:106-112,183-193` |
| Escrita cria/atualiza instituição, tags, regional e galeria | `store()`/`update()` | `:114-181,195-265` |
| Modal "Gerenciar Tags" executa query em `v2_conexao_tags` no render | partial do Core | `V2/layouts/cms/partials/conexao-tags-modal.blade.php:1-5` |
| Modal de fundo envia `image_id` de mídia e grava config | `saveBackgroundImage` | `InstituicaoCmsController.php:506-526` |
| Rodapé consulta o histórico de atividades do model | `activity-footer` | `edit.blade.php:181` |

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

| Ação | Resultado visível |
|---|---|
| Abrir listagem | Título "Associados Regionais"; `Total: N registro(s)`; tabela ou estado vazio |
| Salvar criação válida | Redireciona ao index e abre o modal `created_item` |
| Salvar criação sem tag | Volta ao formulário com erro em `tags` |
| Salvar edição | Volta à edição com `Instituição atualizada com sucesso!` |
| Arquivar | Confirma `Deseja arquivar "NOME"?`; depois `Instituição "NOME" arquivada com sucesso!` |
| Restaurar | Confirma `Restaurar "NOME"?`; depois `Instituição "NOME" restaurada com sucesso!` |
| Remover imagem da galeria | `Imagem removida da galeria com sucesso!` |
| Salvar fundo sem imagem | `Imagem não encontrada na galeria.` |
| Salvar fundo válido | `Imagem de fundo da página de Associados Regionais atualizada com sucesso!` |

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

- Não cria/exclui tags do catálogo de Associados.
- Não alterna `is_active` nem `sort_order`.
- Não edita SEO (`title`, `description`, `OG`, `JSON-LD`).
- Não reordena a galeria (drag-and-drop); `sort_order` é sempre `count+1`.
- Não valida nada no cliente além de `required`/`type=url`/`maxlength`.
- Não atualiza o preview em tempo real.

## 2. Abordagem

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

```mermaid
flowchart LR
  L[index] -->|Nova| C[create]
  L -->|Editar| E[edit]
  E --> P[preview iframe]
  C --> F[_form]
  E --> F
  F --> M1[media-input logo/capa]
  F --> M2[media-group-input galeria]
  F --> T[tag picker]
  F --> S[social builder]
  L --> TM[conexaoTagModal - Core]
  L --> BM[bgImageModal]
```

### 2.2 Mecânica passo a passo

1. `index()` entrega `$instituicoes` paginadas; a blade recalcula tags próprias (`AssociadoTag::orderBy('name')`) e monta os controles.
2. O filtro é um `filter-card` com `search` e `select name="category"` (id da tag). Sem `showDateRange`.
3. `create`/`edit` incluem `_form` e depois a seção de descrição (textarea + editor) e, no edit, o split com iframe.
4. O picker de tags mantém `tagNames` em memória, renderiza badges e **recria hidden inputs `tags[]`** a cada alteração.
5. O `media-input` mantém hidden `gallery_logo_id`/`gallery_cover_image_id` e emite `media-selected.lotep`; o form sincroniza para `logo_id`/`cover_image_id`.
6. O `media-group-input` da galeria já renderiza os IDs existentes em `convenios_gallery_ids[]`.
7. O builder de redes sociais mantém `socialData`, reescreve `social_media_json` a cada mudança e define `order`.
8. No submit, o navegador envia o form; o servidor valida e redireciona.

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

- **Picker de tags textual** `[C]`: aceita nomes livres e cria tags sob demanda; as tags existentes aparecem como botões (`_form.blade.php:60-90`).
- **Ordem de tags = ordem de seleção** `[I]`: os hidden são recriados na ordem do array, e o servidor usa o primeiro como primário.
- **Regional travada** `[C]`: `v2_is_regional_operator()` troca o select por um disabled + hidden; a mensagem é `Vinculada automaticamente à sua regional.` (`_form.blade.php:99-121`).
- **Redes como JSON em textarea oculta** `[C]`: evita campos dinâmicos no servidor; o servidor só decodifica e grava em `metadata` (`_form.blade.php:145-152`; `InstituicaoCmsController.php:150-153`).
- **Preview por iframe** `[C]`: reaproveita o layout real do portal em `/cms/associados/instituicoes/preview/{id}`, mas não é live (`edit.blade.php:126-134`).
- **Duas inclusões do modal de tags** `[C]`: uma no `page-actions` e outra no fim do `content` (`index.blade.php:49,232`), gerando ID duplicado.

### 2.4 Trechos de código pivô

```blade
{{-- V2/Modules/Conexoes/Associados/Views/cms/instituicoes/index.blade.php:5,38-49 — ações e modal do catálogo errado --}}
@php
    $isArchived = request()->route()->getName() === 'v2.associados.cms.instituicoes.archived';
    $allTags = \V2\Modules\Conexoes\Associados\Models\AssociadoTag::orderBy('name')->get();
@endphp
...
<button type="button" class="btn btn-sm btn-outline-info"
        data-bs-toggle="modal" data-bs-target="#conexaoTagModal">
    <i class="bi bi-tags"></i> Gerenciar Tags
</button>
...
@include('v2-cms::partials.conexao-tags-modal')
```

`$allTags` é do catálogo próprio, mas o modal incluído gerencia `v2_conexao_tags`. O botão aparece também na listagem de arquivadas.

```blade
{{-- V2/Modules/Conexoes/Associados/Views/cms/instituicoes/_form.blade.php:268-303 — estado das tags e hidden inputs --}}
function renderTags() {
    tagSelected.innerHTML = '';
    tagNames.forEach(function(name, idx) {
        var span = document.createElement('span');
        span.className = 'badge bg-info tag-badge d-inline-flex align-items-center gap-1';
        span.innerHTML = name + ' <button type="button" class="btn-close btn-close-white tag-remove" aria-label="Remover tag" ... data-idx="' + idx + '"></button>';
        tagSelected.appendChild(span);
    });

    tagHiddenInputs.innerHTML = '';
    tagNames.forEach(function(name) {
        var input = document.createElement('input');
        input.type = 'hidden';
        input.name = 'tags[]';
        input.value = name;
        tagHiddenInputs.appendChild(input);
    });
    ...
}
```

A ordem do array é a ordem dos hidden inputs — logo é a ordem de seleção que define a tag primária no servidor. Remover uma tag só a tira do array; não apaga o catálogo.

```blade
{{-- V2/Modules/Conexoes/Associados/Views/cms/instituicoes/_form.blade.php:99-121 — regional travada --}}
@if(v2_is_regional_operator())
    @php $lockedUr = !empty($unidadesRegionais) ? collect($unidadesRegionais)->first() : null; @endphp
    <input type="text" class="form-control" value="{{ $lockedUr ? $lockedUr->name . ' (' . $lockedUr->acronym . ')' : '' }}" disabled aria-disabled="true">
    <input type="hidden" name="unidade_regional_id" value="{{ $lockedUr ? $lockedUr->id : '' }}">
    <small class="text-muted d-block mt-1">Vinculada automaticamente à sua regional.</small>
@else
    <select id="unidade_regional_id" name="unidade_regional_id" class="form-select" required>
        <option value="">Selecione a unidade regional...</option>
        @foreach($unidadesRegionais as $ur)
            <option value="{{ $ur->id }}" {{ $selectedUnidadeRegionalId === $ur->id ? 'selected' : '' }}>{{ $ur->name }} ({{ $ur->acronym }})</option>
        @endforeach
    </select>
    <small class="text-muted">Todos os associados devem estar vinculados a uma unidade regional.</small>
@endif
```

O hidden garante o `required|exists` do servidor; mesmo que o operador adultere o POST, `syncUnidadeRegional` sobrescreve com a regional dele.

```blade
{{-- V2/Modules/Conexoes/Associados/Views/cms/instituicoes/_form.blade.php:254-255 — estado sem controle --}}
<input type="hidden" name="is_active" value="{{ old('is_active', $inst->is_active ?? 1) }}">
<input type="hidden" name="sort_order" value="{{ old('sort_order', $inst->sort_order ?? 0) }}">
```

Não existe checkbox/numérico visível: o usuário não ativa/desativa nem ordena. Numa instituição arquivada, o hidden vale `0` e o update a mantém inativa.

```blade
{{-- V2/Modules/Conexoes/Associados/Views/cms/instituicoes/edit.blade.php:159-166 — galeria com nome que vira _ids --}}
@include('v2-service-media::components.media-group-input', [
    'name' => 'convenios_gallery',
    'label' => 'Adicionar imagens',
    'accept' => 'image/*',
    'ownerModule' => 'associados',
    'existingIds' => $galleryIds,
    'existingUrls' => $galleryUrls,
])
```

O componente publica `convenios_gallery_ids[]`; é exatamente o nome lido no `update()` (`InstituicaoCmsController.php:251`). Não há galeria na tela de criação.

```js
// V2/Modules/Conexoes/Associados/Views/cms/instituicoes/_form.blade.php:358-373 — sincronização do media-input
var mediaSync = {
    'wrapper-gallery_logo_id': 'logo_id_sync',
    'wrapper-gallery_cover_image_id': 'cover_image_id_sync',
};

Object.keys(mediaSync).forEach(function(wrapperId) {
    var $wrapper = $('#' + wrapperId);
    if ($wrapper.length) {
        $wrapper.on('media-selected.lotep', function(e, data) {
            if (data && data.id) {
                document.getElementById(mediaSync[wrapperId]).value = data.id;
            }
        });
    }
});
```

Depende de jQuery (global do CMS). Sem jQuery, o `logo_id` não é sincronizado, mas o `media-input` ainda envia `gallery_logo_id` e o controller aceita como fallback.

### 2.5 Algoritmos, consultas e custos

- Listagem: 1 query paginada + `with(tags, unidadesRegionais)`; a blade faz `$instituicao->tags->take(3)` em memória.
- Formulário: 1 query de tags + 1 de regionais; `edit` + 1 da instituição com gallery/tags/regionais; cada `galleryMedia` é lazy-loaded no blade (`$item->galleryMedia`), gerando N+1 na galeria da edição (`edit.blade.php:147-156`).
- Modal de tags: 1 query com `withCount` de parceiros e unidades regionais, executada **duas vezes** por render (duas inclusões).
- Builder social: sem chamadas de rede; tudo em memória e um textarea.
- `media-input`/`media-group-input`: upload AJAX para `/cms/v2/gallery` com CSRF do meta tag; Cropper.js é carregado uma vez (`media-group-input.blade.php:23-26`).

## 3. Particularidades

### 3.1 O que só existe aqui

- **Modal de tags "emprestado"** de outro domínio, sem adaptação.
- **Dois modais na mesma página** com o mesmo `id` (`conexaoTagModal`), sendo o segundo inerte.
- **Preview iframe com layout do portal** dentro de rota CMS.
- **Botão de imagem de fundo** que grava configuração global do portal a partir da listagem.

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

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| `RN-ASSOCIADOS-C01` | O picker não cria tag no servidor até o submit; tags novas só existem após salvar | abandonar o form descarta tags digitadas | `_form.blade.php:332-356`; `InstituicaoCmsController.php:161` |
| `RN-ASSOCIADOS-C02` | A primeira tag do array é a primária | ordem de clique/Enter importa | `_form.blade.php:281-289` |
| `RN-ASSOCIADOS-C03` | Regional do operador é sempre a dele | hidden adulterado é sobrescrito | `InstituicaoCmsController.php:377-379` |
| `RN-ASSOCIADOS-C04` | `is_active`/`sort_order` são hidden | não há como publicar/despublicar pela tela | `_form.blade.php:254-255` |
| `RN-ASSOCIADOS-C05` | Galeria só na edição; create não tem `media-group-input` | não se cadastra galeria no create | `create.blade.php:15-58` |
| `RN-ASSOCIADOS-C06` | Preview não é live | alterações só aparecem após salvar/recarregar | `edit.blade.php:131-133` |
| `RN-ASSOCIADOS-C07` | `novalidate` desliga a validação nativa do browser | erros só aparecem após round-trip | `create.blade.php:16`; `edit.blade.php:91-92` |
| `RN-ASSOCIADOS-C08` | Modal de tags grava no catálogo de Conexões | não afeta o seletor nem o portal de Associados | `index.blade.php:38-49`; `ConexaoTagCmsController.php:37-54` |

### 3.3 Casos-limite e comportamentos surpreendentes

- **Instituição sem tags** nunca é criada; a mensagem é o erro padrão de `required` em `tags`.
- **Tag duplicada** no picker é bloqueada com `Tag já adicionada.` por 2 s.
- **Tag existente com outro nome** é reutilizada pelo slug (nome antigo prevalece).
- **Descrição > 250** é aceita pelo servidor (só o contador client-side limita).
- **Upload de imagem > 4 MB** falha no `media-input` (validação client) e/ou no endpoint da Galeria.
- **Arquivada**: o botão "Editar" continua disponível; o formulário abre com `is_active=0`, e salvar não reativa.
- **Regional operator** não vê o botão de arquivar/restaurar (protegido por `@v2can`), mas vê "Gerenciar Tags" e "Imagem de Fundo" (o segundo dará erro de middleware).
- **Listagem de arquivadas** exibe "Total: N registro(s) arquivados" e o filtro de tag continua ativo.
- **Botão "Ver no portal"** usa o slug e abre em nova aba, mesmo para inativas (levará 404).

### 3.4 Armadilhas de reimplementação

- **Recriar o picker como multi-select nativo** perde a noção de ordem/primária e o visual de badges.
- **Usar o modal do Core no formulário** (em vez da listagem) continuaria sem efeito no catálogo.
- **Depender do `media-selected.lotep`** exige jQuery; a alternativa é ler `gallery_logo_id` no submit.
- **Omitir `novalidate`** muda o comportamento de erros (browser bloqueia antes do servidor).
- **Reaproveitar o blade de listagem para arquivadas** exige manter a detecção pelo nome da rota, não por uma variável do controller.
- **Não incluir `activity-footer`** remove o histórico da edição, que é o único lugar onde ele aparece.

### 3.5 Divergências documentação × código e dívidas

- **Botão "Gerenciar Tags" em tela de domínio diferente** — dívida funcional; o CoreCms já registra a mesma conclusão (`01_arquitetura/07-controllers-core.md:350-420`).
- **Modal duplicado** (`index.blade.php:49,232`) — duplicação de DOM e de query.
- **Botão de imagem de fundo visível para operador regional**, mas a rota o bloqueia — UX inconsistente.
- **Sort order e is_active sem controle** — funcionalidade perdida em relação ao modelo.
- **Editor "live preview"** sem sincronização — rótulo e status `Pronto` enganosos.
- **`benefits_list`** validado mas nunca atribuído no CMS — campo morto.

## 4. Integrabilidade

### 4.1 Superfície pública

| Símbolo | Tipo | Contrato |
|---|---|---|
| `v2-associados::cms.instituicoes.index` | view | espera `$instituicoes` (paginator) |
| `v2-associados::cms.instituicoes.create` | view | espera `$instituicao` (novo), `$allTags`, `$unidadesRegionais` |
| `v2-associados::cms.instituicoes.edit` | view | espera `$instituicao` (com `gallery`, `tags`, `unidadesRegionais`), `$allTags`, `$unidadesRegionais` |
| `v2-associados::cms.instituicoes.preview` | view | espera `$instituicao`, `$htmlContent`, `$benefits` |
| `_form` | partial | espera `$instituicao`, `$allTags`, `$unidadesRegionais`; usa `old()` |

### 4.2 Contrato de UI/JS

- **IDs**: `tag-input`, `btn-add-tag`, `tag-selected`, `tag-available`, `tag-hidden-inputs`, `tag-input-error`, `benefits-container`, `add-benefit`, `social-list`, `btn-add-social`, `social_media_json`, `logo_id_sync`, `cover_image_id_sync`, `wrapper-gallery_logo_id`, `wrapper-gallery_cover_image_id`, `live-preview-iframe`, `associadoModal` (portal), `bgImageModal`, `conexaoTagModal`.
- **Eventos**: `keydown` (Enter adiciona tag), `click` (adicionar/remover), `change` (select de rede, filtros), `media-selected.lotep` (jQuery, payload `{id, url, alt}`), `show.bs.modal` (Bootstrap, modal público).
- **Hidden enviados**: `tags[]`, `benefits[i][name]`, `benefits[i][link]`, `social_media_json`, `logo_id`, `cover_image_id`, `is_active`, `sort_order`, `convenios_gallery_ids[]`, `gallery_logo_id`, `gallery_cover_image_id`.
- **Sem JS**: o form ainda funciona (hidden de tags é gerado no load? Não — `renderTags()` roda no `DOMContentLoaded`, então **sem JS não há `tags[]`** e o submit falha na validação).

### 4.3 Ordem e ciclo de vida

1. Bootstrap do layout carrega Bootstrap, jQuery e os JS de mídia.
2. `DOMContentLoaded` do `_form` inicializa picker, sync de mídia, benefícios e social.
3. `DOMContentLoaded` do index inicializa o modal de tags do Core e o submit implícito dos filtros (portal).
4. `media-group-input.js` inicializa por wrapper e usa `v2.gallery.store`.

### 4.4 Receitas de uso

**(a) Adicionar um campo ao formulário sem quebrar o layout**

```blade
{{-- _form.blade.php, dentro da seção "Metadados" --}}
<div class="col-md-6">
    <label for="meu_campo" class="form-label fw-semibold">Meu campo</label>
    <input type="text" id="meu_campo" name="meu_campo" class="form-control" value="{{ old('meu_campo', $inst->meu_campo ?? '') }}">
</div>
```

Depois adicione a regra em `store()` **e** `update()` — não há FormRequest compartilhado.

**(b) Reaproveitar o preview em outra tela**

```blade
<iframe src="{{ route('v2.associados.cms.instituicoes.preview', ['id' => $instituicao->id]) }}"
        style="width:100%;height:500px;border:0"
        sandbox="allow-scripts allow-same-origin"
        title="Preview da instituição"></iframe>
```

**(c) Abrir o modal de fundo**

```blade
<button type="button" class="btn btn-sm btn-outline-info" data-bs-toggle="modal" data-bs-target="#bgImageModal">
    <i class="bi bi-image"></i> Imagem de Fundo
</button>
```

O modal inclui `media-input` com `name="image_id"`, `ownerModule="associados"` e ação `POST save-bg`.

### 4.5 Anti-uso

- **Esperar que "Gerenciar Tags" crie uma tag selecionável no formulário**: ela vai para outro catálogo.
- **Adicionar `name="images[]"` esperando o endpoint de upload**: a tela usa o picker da Galeria; o endpoint `uploadGallery` é outro fluxo.
- **Remover o hidden `is_active`** para "simplificar": `update()` passa a gravar `false` e a instituição some do portal.
- **Usar `$inst->tags->pluck('name')` sem `relationLoaded`**: em telas que não carregam a relação, o picker inicia vazio.

### 4.6 Custos e limites da integração

- Upload por arquivo: 4 MB; Cropper processa no cliente.
- Galeria da edição: 1 query por item para `galleryMedia` (N+1).
- Modal de tags duplicado: 2 queries com `withCount`.
- `activity-footer`: consulta o histórico a cada abertura da edição.
- Preview: carrega o layout completo do portal dentro do CMS (peso de CSS/JS do portal).

## 5. Rotas e contratos HTTP

| Tela | Rota | Método | Permissão | Objetivo |
|---|---|---|---|---|
| Listagem | `GET /cms/associados/instituicoes` | GET | `Associados:read` | ativas |
| Listagem (arquivadas) | `GET /cms/associados/instituicoes/arquivadas` | GET | read, não regional | soft-deleted |
| Criação | `GET/POST /cms/associados/instituicoes/nova` | GET/POST | read/write | form/create |
| Edição | `GET /cms/associados/instituicoes/editar/{id}` | GET | read | form |
| Edição | `PUT/POST /cms/associados/instituicoes/editar/{id}` | PUT/POST | write | update |
| Preview | `GET /cms/associados/instituicoes/preview/{id}` | GET | read | iframe |
| Arquivar | `DELETE /cms/associados/instituicoes/{id}` | DELETE | write | archive |
| Restaurar | `POST /cms/associados/instituicoes/{id}/restaurar` | POST | write, não regional | restore |
| Fundo | `POST /cms/associados/salvar-imagem-fundo` | POST | write, não regional | config |
| Galeria | `POST .../gallery`, `POST .../gallery/sync`, `DELETE /cms/associados/gallery/{id}` | POST/DELETE | write | mídias |

Detalhes de validação e mensagens em [rotas.md](rotas.md).

## 6. Dados e modelo de domínio

O formulário expõe: `name`, `tags[]`, `unidade_regional_id`, `url`, `discount_badge`, `description`, `convenio_description`, `benefits[]`, `social_media_json`, `logo_id`, `cover_image_id`, `is_active` (hidden), `sort_order` (hidden) e, na edição, `convenios_gallery_ids[]`. Colunas e casts em [dados.md](dados.md).

### 6.1 Campos do formulário (visão detalhada)

| Campo | Controle | Obrigatório | Default | Validação server | Ajuda/observação |
|---|---|---|---|---|---|
| Nome | text max255 | sim | — | `required|string|max:255` | `Nome da instituição` |
| Tags | picker textual (`tags[]`) | sim (≥1) | tags atuais | `required|array|min:1`, `tags.* string|max:100` | `Digite o nome da tag e pressione Enter`; `Tag já adicionada.` |
| Unidade Regional | select ou disabled+hidden | sim | primeira do registro ou vazio | `required|string|max:36|exists:v2_unidade_regionais,id` | `Todos os associados devem estar vinculados...`; operador vê `Vinculada automaticamente à sua regional.` |
| Site | `type=url` | não | — | `nullable|url|max:255` | placeholder `https://www.exemplo.com.br` |
| Redes Sociais | builder select+url, serializado em `social_media_json` | não | LinkedIn, Facebook, Instagram vazios | `nullable|string` | 9 redes: facebook, instagram, twitter, youtube, linkedin, whatsapp, telegram, tiktok, flickr |
| Badge de Desconto | text max255 | não | — | `nullable|string|max:255` | placeholder `Ex: 15% OFF` |
| Benefícios | linhas nome + link | não | `metadata.benefits` | `update`: `benefits array`, `name max255`, `link url|max:500`; `store`: sem regra | remove linhas sem `name` |
| Logo | `media-input` + hidden `logo_id` | não | `metadata.logo_id` | `logo_id nullable|string|max:36` | upload/picker |
| Capa | `media-input` + hidden `cover_image_id` | não | `metadata.cover_image_id` | idem | `Recomendado: 1920×400px` |
| Descrição Rápida | textarea maxlength 250 | não | — | `nullable|string` (sem max) | contador `0/250 caracteres` |
| Descrição Detalhada | editor Tiptap | não | `{"type":"doc","content":[]}` | `nullable|string` | renderizada no portal se tiver `type` |
| `is_active` | hidden | — | valor atual ou `1` | `nullable|boolean` | sem controle visível |
| `sort_order` | hidden | — | valor atual ou `0` | `nullable|integer|min:0` | sem controle visível |
| Galeria | `media-group-input` | não | mídias atuais | `convenios_gallery_ids nullable|array` | só na edição |
| Imagem de fundo | `media-input` (modal) | sim (no modal) | — | `image_id required|string|max:36` | grava `theme_associados_bg` |

## 7. Fluxos e estados

```mermaid
stateDiagram-v2
  [*] --> Lista
  Lista --> FormCriacao: Nova Instituição
  Lista --> FormEdicao: Editar
  Lista --> Arquivadas: Arquivados
  FormCriacao --> Lista: POST válido
  FormCriacao --> FormCriacao: erro de validação
  FormEdicao --> FormEdicao: PUT válido
  FormEdicao --> Preview: iframe
  Lista --> Lista: DELETE
  Arquivadas --> Arquivadas: POST restaurar
```

Estados de tela: **vazio** (ícone `bi-building`, `Nenhum registro encontrado` ou `Nenhum registro arquivado`; se há filtro, `Tente ajustar os filtros ou limpar a busca.`; se não, `Comece criando o primeiro registro.`); **carregando** apenas no iframe/preview (sem skeleton); **erro** por `is-invalid` + `invalid-feedback`.

## 8. UI CMS (árvore e detalhes de tela)

### 8.1 Listagem (`index.blade.php`)

- Breadcrumb: `Conexões › Associados Regionais [— Arquivados]`.
- Ações: `Nova Instituição`, `Arquivados`/`Ativos`, `Gerenciar Tags`, `Imagem de Fundo`.
- Texto: `Gerencie as instituições associadas. Total: N registro(s)[ arquivados].`
- Filtros: busca (`Buscar por nome...`), tag (`Todas`), botão de submit do `filter-card`.
- Colunas: Logo (36×36), Nome (+ badge), Tags (até 3 + `+N`), Unidade Regional, Criação (`d/m/Y`), Ativo (status-badge), Ações (portal, editar, arquivar/restaurar).
- Paginação: `links('v2-cms::partials.pagination')` com `appends` de query.
- Sem bulk actions; sem ordenação clicável.

### 8.2 Criação/edição

- Seções visuais: **Metadados** (nome, tags, regional, site, redes, badge, benefícios), **Conteúdo** (logo, capa), **Descrição** (rápida + detalhada) e, na edição, **Galeria** + split preview.
- Edição tem ações de topo: `Arquivar` (só se ativa) dentro de `@v2can('Associados','write')`.
- Rodapé: `activity-footer` com o histórico do model.

### 8.3 Preview (`preview.blade.php`)

- Layout do portal, card central, logo, nome, tags, ícones de site/LinkedIn/Instagram/Facebook, badge, conteúdo HTML, benefícios.
- Aviso fixo no rodapé: `Preview — NOME`.
- Não renderiza galeria nem regional.

### 8.4 Modais e mensagens exatas

| Modal | ID | Conteúdo |
|---|---|---|
| Gerenciar Tags | `conexaoTagModal` | input `Nova tag...` (maxlength 100), botão criar, lista com `(N vínculos)`, badge `Reservada`, botão excluir; confirmação `Excluir a tag "X"? Os itens vinculados serão transferidos para "Geral".` |
| Imagem de Fundo | `bgImageModal` | `media-input` `image_id`, ajuda `Recomendado: 1920×400px.` |
| Confirmação de arquivamento | nativa | `Deseja arquivar "NOME"?` (listagem) / `Arquivar "NOME"?` (edição) |
| Confirmação de restauração | nativa | `Restaurar "NOME"?` |
| Criação | `created_item` | `type: Instituição`, `title`, `route` |

### 8.5 Acessibilidade

- Labels explícitos em todos os campos; `aria-label` em selects de filtro e botões de ícone.
- `invalid-feedback` ligado por classe, sem `aria-describedby` no campo com erro (exceto contador).
- Botões de ação têm `title`, mas nem sempre `aria-label`.
- Modais do Bootstrap com `aria-labelledby`/`aria-hidden`; o modal do Core usa `role="dialog"`.
- O picker de tags usa `<button>` para remover, com `aria-label="Remover tag"`.
- O iframe de preview tem `title`.
- Não há foco programático ao abrir o modal de tags; o modal público (portal) move o foco para o primeiro focável.

## 9. UI Portal

Ver [portal.md](portal.md).

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

- `@v2can('Associados','write')` protege arquivar/restaurar em `index` e `edit`; o link "Editar" não é protegido.
- O botão de fundo não é protegido por `v2can` (só pelo middleware nacional).
- Módulo desativado: qualquer rota CMS responde `404 Modulo desativado.` (HTML) ou `404 {"error":"Modulo desativado: Associados","code":404}` (AJAX/JSON); não há redirect ao dashboard porque o prefixo real é `cms/associados`.
- Tema: classes do CMS (`card`, `table`, `badge`) e variáveis do layout; o preview usa o CSS do portal.

## 11. Observabilidade, erros e limites

- Não há log de UI; erros de validação aparecem em `invalid-feedback`/texto vermelho.
- `created_item` é consumido pelo layout para o modal de sucesso; se o layout não o consumir, o flash fica na sessão.
- Upload e galeria dependem de CSRF (`meta[name=csrf-token]` no `media-input`).
- `media-group-input` usa `route('v2.gallery.store')` e pode falhar com 422/500 da Galeria sem feedback customizado nesta tela.

## 12. Seeds e dados iniciais

As tags disponíveis no picker vêm do catálogo (3 semeadas por migration + as criadas em uso). As instituições de demonstração aparecem na listagem e são criadas por `DemoContentSeeder` (ver [dados.md](dados.md)).

## 13. Lacunas

| ID | Lacuna | Impacto | Onde procurar |
|---|---|---|---|
| `LG-ASSOCIADOS-C01` | Não há indicativo de que o modal de tags seja de outro domínio na UI. | Usuário acredita que criou uma tag de Associados. | decisão de produto/UX |
| `LG-ASSOCIADOS-C02` | Não há tela para gerenciar/excluir tags de Associados. | Catálogo sem curadoria. | — |
| `LG-ASSOCIADOS-C03` | Não há controle de `is_active`/`sort_order`. | Publicação e ordenação dependem de banco/API. | — |
| `LG-ASSOCIADOS-C04` | O CSS `portal-associados.css` não foi analisado linha a linha. | Paridade visual do card/modal. | `public/assets/css/modules/associados/portal-associados.css` |

## 14. Checklist de paridade

- [ ] `PC-ASSOCIADOS-C01` — listagem mostra total, colunas e ações descritas; arquivadas usam o mesmo blade com título alternativo. Fonte: `index.blade.php:5,56-198`.
- [ ] `PC-ASSOCIADOS-C02` — filtro por tag usa `AssociadoTag` e envia `category`. Fonte: `:7,66-70`.
- [ ] `PC-ASSOCIADOS-C03` — picker mantém ordem, bloqueia duplicata e envia `tags[]`. Fonte: `_form.blade.php:268-356`.
- [ ] `PC-ASSOCIADOS-C04` — operador regional vê regional travada e hidden. Fonte: `_form.blade.php:99-121`.
- [ ] `PC-ASSOCIADOS-C05` — `media-input` envia `gallery_logo_id` e sincroniza `logo_id`. Fonte: `:220-251,358-373`.
- [ ] `PC-ASSOCIADOS-C06` — galeria usa `convenios_gallery` → `convenios_gallery_ids[]`. Fonte: `edit.blade.php:159-166`.
- [ ] `PC-ASSOCIADOS-C07` — preview carrega o layout do portal e mostra aviso fixo. Fonte: `preview.blade.php:14-33,134-138`.
- [ ] `PC-ASSOCIADOS-C08` — modal "Gerenciar Tags" não altera o catálogo de Associados. Fonte: `index.blade.php:38-49`; `ConexaoTagCmsController.php:37-54`.
- [ ] `PC-ASSOCIADOS-C09` — modal de fundo grava `theme_associados_bg` e é nacional-only. Fonte: `index.blade.php:200-228`; `Routes/cms.php:70-74`.
- [ ] `PC-ASSOCIADOS-C10` — confirmações e mensagens exatas de arquivar/restaurar. Fonte: `index.blade.php:145-172`; `InstituicaoCmsController.php:100-102,288`.

## 15. Evidências consultadas

- `V2/Modules/Conexoes/Associados/Views/cms/instituicoes/index.blade.php:1-232`
- `V2/Modules/Conexoes/Associados/Views/cms/instituicoes/_form.blade.php:1-516`
- `V2/Modules/Conexoes/Associados/Views/cms/instituicoes/create.blade.php:1-73`
- `V2/Modules/Conexoes/Associados/Views/cms/instituicoes/edit.blade.php:1-198`
- `V2/Modules/Conexoes/Associados/Views/cms/instituicoes/preview.blade.php:1-139`
- `V2/Modules/Conexoes/Associados/Controllers/InstituicaoCmsController.php:47-113,114-291,506-583`
- `V2/layouts/cms/partials/conexao-tags-modal.blade.php:1-203`
- `V2/Core/Controllers/ConexaoTagCmsController.php:21-99`
- `V2/Services/Media/Views/components/media-input.blade.php:1-80`
- `V2/Services/Media/Views/components/media-group-input.blade.php:1-80`
- `V2/Services/Gallery/Routes/cms.php:6-9`; `V2/Services/Gallery/Controllers/GalleryController.php:63-109`
- `01_arquitetura/07-controllers-core.md:350-420`
