---
id: MOD-INSTITUCIONAL-ASSOCIESE-CMS
tipo: modulo
onda: 4
status: concluido
unidades: [MOD-INSTITUCIONAL-ASSOCIESE]
fontes:
  - V2/Modules/Institucional/AssocieSe/
depende_de: [UI-CMS, CORE-CMS, CORE-AUTH, SVC-CONTENTRICHEDITOR, SVC-MEDIA]
consumidores: [UI-CMS]
criticidade: media
dono: InstitucionalB
---

# Institucional/AssocieSe — UI CMS

> Anexo do módulo [`spec.md`](spec.md). Quatro telas Bootstrap 5 dentro de
> `v2-cms::layouts.app`: lista/detalhe de leads, lista/novo/editar de planos e
> configurações. Sem widgets próprios, sem modal de confirmação customizado; o único
> AJAX é o botão “Salvar Galeria”. A tela de configurações tem um defeito de contrato:
> o formulário envia, o banco grava e a resposta é `500`.

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Dar ao operador `AssocieSe` acesso à fila de leads, ao catálogo de planos (com validade e galeria) e às sete chaves de apresentação do portal. |
| Entrada | 13 rotas CMS; filtros `status`/`search`; formulários de status, plano e config; `media_ids[]`. |
| Saída | HTML no layout do CMS; `302` com flash; modal `creationSuccessModal`; JSON de galeria. |
| Efeitos colaterais | `flash::message`, `session('created_item')`, `confirm()` nativo, recarga após sync, `TypeError` na config. |
| Particularidade central | O formulário de configuração não exibe `$errors` e o `PUT` termina em `500` por causa do `ActivityLoggerService::log('string', ...)`; o operador não vê sucesso nem erro de validação. |
| Como se integra | `v2-cms::layouts.app`, partials `breadcrumb`, `filter-card`, `action-create`, `pagination`; `v2-service-contentricheditor` e `v2-service-media`; jQuery no edit de plano. |
| Se removido, quebra | Não há outra UI para operar leads/planos/config; os links da sidebar e o modal de criação de plano ficam órfãos. |
| Superfície pública | IDs/classes listados em §4.2; funções `syncGalleryMedias()`; nomes de campos de §5. |

## 1. Efeito

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

- **Leads**: ver o total, filtrar por status, buscar por nome/e-mail/empresa, paginar,
  abrir detalhe, trocar status, editar observações e remover com confirmação.
- **Planos**: ver todos os planos com preço, ciclo, vigência, quantidade de mídias e
  flag ativo; criar, editar e remover; no editar, abrir o seletor de mídias e salvar a
  galeria.
- **Configurações**: editar título, subtítulo, intro, mensagem de sucesso, e-mail de
  notificação e dois switches; ver versão do sistema, nomes das tabelas e contadores.
- **Estados vazios**: “Nenhum lead encontrado”, “Nenhum plano cadastrado”, com botões de
  limpar filtros/criar primeiro plano.

### 1.2 O que muda no sistema

| Ação | Efeito | Fonte |
|---|---|---|
| Filtro/busca de leads | `GET` com query; 20 linhas; links preservam query | `Views/cms/leads/index.blade.php:27-42,97-101` |
| “Atualizar” status | `POST` + `_method=PUT`; grava status/notes; recarrega a página anterior | `Views/cms/leads/show.blade.php:85-108` |
| “Remover Lead” | `confirm()`; `POST` + `_method=DELETE`; volta à lista | `Views/cms/leads/show.blade.php:117-125` |
| “Novo Plano” | link para o formulário | `Views/cms/plans/index.blade.php:17-22` |
| Criar/editar plano | `POST`; rich editor envia JSON em `description`; modal de criação no create | `Views/cms/plans/_form.blade.php:16-22`; `MembershipPlanController.php:97-101` |
| “Salvar Galeria” | `$.ajax` POST; alerta e `location.reload()` após 1,5 s | `Views/cms/plans/edit.blade.php:82-127` |
| Remover plano | `confirm()`; DELETE; soft delete | `Views/cms/plans/index.blade.php:89-98` |
| “Salvar Configurações” | `POST` + `_method=PUT`; 7 chaves; resposta `500` | `Views/cms/config/index.blade.php:26-67` |

### 1.3 Contrato observável

| Interação | Resultado na tela |
|---|---|
| Abrir `/cms/institucional/associe-se/leads` | total no topo, cartão de filtros, tabela ou estado vazio. |
| `?status=converted` | select mostra “Convertido” e a tabela só tem convertidos. |
| `?search=acme` | campo preenchido; tabela filtrada; paginação mantém `search`. |
| Sem resultados com filtro | “Nenhum lead encontrado” + “Tente ajustar os filtros ou limpar a busca.” + botão “Limpar Filtros”. |
| Sem resultados sem filtro | “Nenhum lead recebido até o momento.” |
| Detalhe de lead | duas colunas: dados (7–8 linhas) e atualização/remoção. |
| Status salvo | volta à página anterior com faixa de sucesso do layout (`flash::message`). |
| Lead removido | volta à lista com `Lead "<nome>" removido com sucesso!`. |
| Lista de planos | contagem, tabela com badges; “Fora da validade” quando aplicável. |
| Plano criado | volta à lista; modal “Plano criado com sucesso!” com botão “Voltar para Listagem”. |
| Plano atualizado | volta à lista com faixa `Plano atualizado com sucesso!`. |
| Plano removido | faixa `Plano "<nome>" removido com sucesso!`. |
| “Salvar Galeria” com IDs | spinner; em erro (`500`), alerta “Erro de comunicação ao salvar galeria.”; sem reload. |
| “Salvar Galeria” vazio | alerta “Galeria salva com sucesso!”, reload após 1,5 s. |
| Salvar config | requisição termina em `500`; a tela não mostra sucesso nem erro de validação. |

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

- Não tem bulk actions, seleção múltipla, ordenação por coluna, exportação ou impressão.
- Não tem modal próprio de confirmação: usa `window.confirm`.
- Não exibe `metadata`/plano de interesse no detalhe do lead.
- Não exibe histórico de auditoria na tela (há o partial global `activity-footer`, mas
  não é incluído por estas views).
- Não valida no cliente de forma efetiva: o formulário de plano usa `required`/`min`,
  mas o de lead não tem JS; a config não tem `maxlength` nos textareas.
- Não mostra erros de validação da configuração.
- Não tem upload dentro da tela de create de plano (a galeria só existe no edit).
- Não tem preview da página pública.

## 2. Abordagem

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

Cada tela é uma view autossuficiente que estende o layout do CMS e usa partials
genéricos. O controller entrega coleções Eloquent prontas; a view decide cores, badges e
estados. O create/edit de plano compartilha `_form.blade.php`, que inclui o editor rico
como componente e alterna `PUT` via `method_field`. A galeria é um card adicional no
edit, com o componente `media-group-input` e um botão que chama um endpoint JSON.

```mermaid
flowchart LR
  A[Sidebar Institucional > Associe-se] --> B[Leads]
  A --> C[Planos]
  A --> D[Configurações]
  B --> B1[index: filter-card + tabela + paginação]
  B1 --> B2[show: dados + form status + delete]
  C --> C1[index: tabela + ação Novo Plano]
  C1 --> C2[create: _form + modal de sucesso]
  C1 --> C3[edit: _form + card de galeria + AJAX]
  D --> D1[form de 7 chaves + card de informações]
```

### 2.2 Mecânica passo a passo

1. **Layout**: `@extends('v2-cms::layouts.app', ['title' => $pageTitle])`; breadcrumbs via
   `@section('page-breadcrumbs')` (ou HTML manual no show/edit/create); conteúdo em
   `@section('content')`.
2. **Leads index**: o controller entrega `$leads` paginado; a view lê `request()` para
   pré-preencher filtros e define `$statusLabels`/`$validStatuses` direto do model
   (`Views/cms/leads/index.blade.php:1-8`).
3. **Filtros**: o partial `filter-card` monta o `<form>` com busca e slots; a view injeta
   um `select` de status (`:27-42`).
4. **Tabela**: cabeçalho fixo; cada linha tem link no nome, e-mail, empresa, badge de
   status colorido, data e botão de olho (`:59-91`).
5. **Paginação**: só se `hasPages()`; `appends(request()->except('page'))` preserva
   filtros (`:97-101`).
6. **Detalhe**: `dl` com dados; formulário de status à direita; formulário de delete
   abaixo, ambos com CSRF e `_method` (`Views/cms/leads/show.blade.php:45-125`).
7. **Planos index**: `action-create` no slot `page-actions`; tabela com
   `billing_cycle_label`, vigência formatada, badge de mídias e switch visual
   (`Views/cms/plans/index.blade.php:17-101`).
8. **Formulário de plano**: partial com nome, rich editor, preço, ciclo, switch ativo e
   datas; create só o formulário; edit acrescenta o card de galeria e o JS
   (`Views/cms/plans/_form.blade.php:1-68`; `edit.blade.php:38-128`).
9. **Config**: form com 7 campos gerados por `foreach`; switches para duas chaves,
   textareas para duas; card de informações com `v2_version()` e contadores
   (`Views/cms/config/index.blade.php:19-100`).

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

- **Partials genéricos** `[C]`: `filter-card`, `breadcrumb`, `action-create`,
  `pagination`, `flash::message` e `creation-success-modal` vêm do layout, mantendo a
  UI consistente com outros módulos.
- **Status como badges** `[C]`: mapa local `pending→warning`, `contacted→info`,
  `converted→success`, `discarded→secondary`, repetido no index e no show
  (`Views/cms/leads/index.blade.php:70-79`; `show.blade.php:28-34`).
- **Vigência legível** `[C]`: usa `Carbon::parse` na view e um badge “Fora da validade”
  via accessor (`Views/cms/plans/index.blade.php:48-68`).
- **Galeria só no edit** `[C]`: create não inclui o card; é preciso criar e reabrir para
  subir mídias (`Views/cms/plans/create.blade.php:21-33`; `edit.blade.php:38-76`).
- **AJAX com jQuery** `[C]`: o CMS já carrega jQuery; a função é global e usa `$.ajax`,
  sem `fetch` (`Views/cms/plans/edit.blade.php:80-128`).
- **Config gerada por mapa** `[C]`: `$configKeys` no controller define rótulos e a view
  infere o tipo por listas fixas de chaves booleanas/textarea
  (`AssocieSeConfigController.php:20-28`; `Views/cms/config/index.blade.php:31-37`).
- **Sem componentes próprios** `[C]`: não há `Views/cms/components/`; tudo é partial
  inline ou serviço compartilhado.

### 2.4 Trechos de código pivô

```blade
{{-- V2/Modules/Institucional/AssocieSe/Views/cms/leads/index.blade.php:27-42 — filtros --}}
@component('v2-cms::partials.filter-card', [
    'searchPlaceholder' => 'Buscar por nome, email ou empresa...',
    'searchValue'       => $searchTerm,
    'action'            => route('v2.associe-se.cms.leads'),
    'showDateRange'     => false,
])
    <div class="col-md-3">
        <label for="status" class="form-label small fw-medium text-muted mb-1">Status</label>
        <select name="status" class="form-select form-select-sm" aria-label="Filtrar por status">
            <option value="">Todos os status</option>
            @foreach($statusLabels as $key => $label)
                <option value="{{ $key }}" @if($statusFilter === $key) selected @endif>{{ $label }}</option>
            @endforeach
        </select>
    </div>
@endcomponent
```

**Efeito.** O partial envolve o slot em um `<form method="GET">` com campo de busca e
botões “Filtrar”/“Limpar”; o `select` de status é adicionado como slot no bloco
avançado, que começa colapsado e abre automaticamente quando `search`/`date_from`/
`date_to` estão preenchidos
(`V2/layouts/cms/partials/filter-card.blade.php:41-47,95-130`). `showDateRange=false`
remove os campos de data, mas mantém o slot de status. O `action` é a rota de índice.

```blade
{{-- V2/Modules/Institucional/AssocieSe/Views/cms/plans/index.blade.php:48-68 — vigência --}}
<td>
    @if($plan->valid_from || $plan->valid_until)
        <small class="text-muted">
            @if($plan->valid_from)
                {{ \Carbon\Carbon::parse($plan->valid_from)->format('d/m/Y') }}
            @endif
            até
            @if($plan->valid_until)
                {{ \Carbon\Carbon::parse($plan->valid_until)->format('d/m/Y') }}
            @endif
        </small>
        @if(!$plan->is_currently_valid)
            <span class="badge bg-warning ms-1">Fora da validade</span>
        @endif
    @else
        <span class="text-muted">Sem prazo</span>
    @endif
</td>
```

**Efeito.** A coluna mostra o início e o fim quando presentes (os fallbacks `—`/`∞`
foram omitidos do trecho) e sinaliza plano fora da vigência, mas **não** esconde nem
desativa o plano; a listagem inclui inativos.

```blade
{{-- V2/Modules/Institucional/AssocieSe/Views/cms/plans/edit.blade.php:47-66 — galeria --}}
@php
    $galleryIds = $plan->gallery->pluck('gallery_media_id')->filter()->values()->toArray();
    $galleryUrls = [];
    foreach ($plan->gallery as $item) {
        if ($item->galleryMedia) {
            $galleryUrls[] = v2_storage_url($item->galleryMedia->file_path);
        } elseif ($item->image) {
            $galleryUrls[] = v2_storage_url($item->image);
        }
    }
@endphp
@include('v2-service-media::components.media-group-input', [
    'name' => 'associese_gallery',
    'label' => 'Imagens e documentos do plano',
    'accept' => 'image/*,application/pdf',
    'ownerModule' => 'associese',
    'existingIds' => $galleryIds,
    'existingUrls' => $galleryUrls,
])
```

**Efeito.** O componente recebe os IDs existentes e as URLs já resolvidas. Se
`gallery_media_id` tiver um UUID (schema atual), `galleryMedia` pode não resolver; o
fallback `image` mantém o preview. O componente renderiza hidden inputs
`associese_gallery_ids[]`, mas o JS do módulo lê os valores pelo container, não pelo
nome.

```blade
{{-- V2/Modules/Institucional/AssocieSe/Views/cms/plans/edit.blade.php:82-98 — sync --}}
function syncGalleryMedias() {
    var btn = document.getElementById('sync-gallery-btn');
    btn.disabled = true;
    btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Salvando...';

    var ids = [];
    $('#group-ids-container-associese_gallery input').each(function() {
        ids.push($(this).val());
    });

    $.ajax({
        url: '{{ route("v2.associe-se.cms.plans.gallery.sync", ["id" => $plan->id]) }}',
        type: 'POST',
        data: { _token: '{{ csrf_token() }}', media_ids: ids },
```

**Efeito.** O botão desabilita, coleta todos os `input` (inclusive vazios, se houver),
envia como `media_ids` e trata `success`/`error`. O alerta é inserido em
`.card-body:first` (o corpo do formulário, não o card da galeria) e o reload ocorre só
no sucesso.

```blade
{{-- V2/Modules/Institucional/AssocieSe/Views/cms/config/index.blade.php:31-60 — campos --}}
@foreach($configKeys as $key => $label)
    @php
        $formKey = 'config_' . str_replace('.', '_', $key);
        $value = isset($configs[$key]) ? $configs[$key] : '';
        $isBool = in_array($key, ['associese_enabled', 'associese_show_plans']);
        $isTextarea = in_array($key, ['associese_intro_text', 'associese_success_message']);
    @endphp
    @if($isBool)
        <input type="hidden" name="{{ $formKey }}" value="0">
        <input type="checkbox" id="{{ $formKey }}" name="{{ $formKey }}" value="1"
               class="form-check-input" {{ $value == '1' ? 'checked' : '' }}>
        <label for="{{ $formKey }}" class="form-check-label">{{ $label }}</label>
    @elseif($isTextarea)
        <textarea id="{{ $formKey }}" name="{{ $formKey }}" class="form-control" rows="3">{{ $value }}</textarea>
    @else
        <input type="text" id="{{ $formKey }}" name="{{ $formKey }}" class="form-control"
               value="{{ $value }}" maxlength="1000">
    @endif
@endforeach
```

**Efeito.** Switches enviam `0` (hidden) ou `1`; por isso o servidor sempre vê a chave.
As textareas não têm `maxlength`; o servidor limita a 1000 e, se falhar, a view não
mostra os erros. A comparação visual `== '1'` aceita `1`/`1.0`, mas o portal usa `===`.

### 2.5 Algoritmos, consultas e custos

- Leads: `COUNT` + `SELECT` paginado; 20 por página; sem eager loading (não há relações).
- Planos: `SELECT` de todos + eager `gallery` (1 query extra); a view faz
  `Carbon::parse` por linha.
- Config: 2 `COUNT` + 1 `SELECT` de configs; os contadores ignoram soft-deleted.
- Galeria: `pluck` + `v2_storage_url` por item (pode gerar URLs em massa).
- Sem cache de tela; cada visita executa as queries.

## 3. Particularidades

### 3.1 O que só existe aqui

- **Config com `TypeError`**: a tela mais simples do módulo é a única que sempre falha
  no salvamento (embora persista).
- **Galeria só no editar**: não há criação de plano com mídia.
- **Dois formulários de status/remoção no detalhe**: sem modal, com `confirm()`.
- **Status de lead sem transições impostas**: qualquer um dos quatro pode ir para
  qualquer outro.
- **Contadores de plano/lead na tela de config**: visão de “saúde” do módulo.

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

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| `RN-ASSOCIESE-C01` | O formulário de config usa `PUT` via `_method`. | A rota aceita `PUT|POST`; sem `_method`, o POST também funciona. | `Views/cms/config/index.blade.php:26-28`; `Routes/cms.php:72` |
| `RN-ASSOCIESE-C02` | Switches de config têm hidden `0` + checkbox `1`. | O valor nunca é omitido; “desligado” grava `'0'`. | `Views/cms/config/index.blade.php:42-44` |
| `RN-ASSOCIESE-C03` | O switch de plano **não** tem hidden. | Desmarcado não envia `is_active`; o controller decide por `has()`. | `Views/cms/plans/_form.blade.php:43-45`; `MembershipPlanController.php:89,154` |
| `RN-ASSOCIESE-C04` | O create de plano não tem card de galeria. | Mídias só podem ser adicionadas após criar. | `Views/cms/plans/create.blade.php:21-33` |
| `RN-ASSOCIESE-C05` | O modal de criação depende de `session('created_item')`. | Se a auditoria falhar antes do flash, o modal não aparece. | `MembershipPlanController.php:97-101`; `V2/layouts/cms/partials/creation-success-modal.blade.php:1-22` |
| `RN-ASSOCIESE-C06` | A view de config não renderiza `$errors`. | Erros de validação ficam invisíveis. | `Views/cms/config/index.blade.php:17-100` |
| `RN-ASSOCIESE-C07` | A view de leads usa `request()` diretamente. | O estado do filtro é derivado da query, não de variáveis do controller. | `Views/cms/leads/index.blade.php:3-4` |
| `RN-ASSOCIESE-C08` | O detalhe do lead não mostra o plano de interesse. | `metadata` não é exibido em nenhum lugar do CMS. | `Views/cms/leads/show.blade.php:45-73` |
| `RN-ASSOCIESE-C09` | O único `confirm()` é nativo. | Sem modal Bootstrap para delete; sem `aria` específico. | `Views/cms/leads/show.blade.php:121`; `plans/index.blade.php:94` |
| `RN-ASSOCIESE-C10` | O sync usa jQuery global. | Sem jQuery, `syncGalleryMedias` quebra (`$` indefinido). | `Views/cms/plans/edit.blade.php:88-92` |

### 3.3 Casos-limite e comportamentos surpreendentes

- **Salvar config**: banco atualizado, resposta `500`; recarregar a tela mostra os novos
  valores (prova de que o commit ocorreu).
- **Erro de validação na config**: só ocorre se um campo passar de 1000 via cliente
  adulterado; a tela volta sem mensagem.
- **Plano sem mídia**: coluna “Mídias” mostra `—`; o portal usa o ícone de fallback.
- **Plano fora da validade**: badge amarelo na lista; o link de edição continua.
- **Plano inativo**: badge cinza “Não”; não há ação rápida de ativar.
- **Lead com `company` nula**: coluna mostra `-`; no detalhe, idem.
- **Lead com mensagem longa**: o detalhe usa `{{ }}` e quebra linha apenas se houver
  quebra no texto; não há `nl2br`.
- **Status legado desconhecido**: badge cai em `secondary` e exibe o valor bruto
  (`Views/cms/leads/index.blade.php:76-80`).
- **Paginação com filtro**: o link preserva `status`/`search`; limpar pode ser feito
  pelo “Limpar” sempre presente no `filter-card` ou pelo “Limpar Filtros” do estado
  vazio.
- **Sync com lista vazia**: apaga a galeria e mostra sucesso.
- **Sync com erro**: alerta no topo do formulário; o botão volta ao normal; não há
  reload. Se o `500` veio depois de apagar, a galeria já mudou.
- **Imagem quebrada**: o preview não tem `onerror`; o card fica com espaço vazio.
- **Rich editor**: se o serviço não estiver registrado, o `@include` falha e a tela de
  plano não renderiza.
- **`media-group-input`**: dependências de Cropper/JS do serviço; sem elas, o upload não
  funciona, mas os hidden inputs existentes continuam.

### 3.4 Armadilhas de reimplementação

- **Não remova o hidden `0` dos switches de config**: sem ele, desmarcar não envia a
  chave e o valor antigo permanece.
- **Não adicione hidden `0` no switch de plano** sem mudar o controller: hoje a ausência
  é o sinal de “desmarcado” no update.
- **Não use `$plan->is_active` como string** na view: é cast boolean.
- **Não troque `request()->input()` por variáveis do controller** sem replicar
  `statusFilter`/`searchTerm`.
- **Não exiba `metadata` no detalhe** sem antes criar a coluna; o valor é sempre nulo.
- **Não coloque o card de galeria no create** sem rota/controller para salvar no ato;
  o create não sincroniza.
- **Não confie no alerta de erro do sync**: ele só cobre falha HTTP; um `200` com
  `success:false` tem outro branch, mas o controller nunca devolve esse formato.
- **Não recarregue imediatamente após o sync**: o código espera 1,5 s para o alerta ser
  visto; remover o timeout muda a UX.
- **Não use `aria-label` só no `<table>`**: os botões de ícone dependem de `title`.
- **Não exiba dados do lead sem escape**: a view usa `{{ }}`; manter.

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

- O plano do módulo não descreve a tela de configurações; ela existe e tem 7 chaves.
- O plano fala em “exportar”; a UI não tem botão.
- O plano fala em filtros por data; `showDateRange=false`.
- O plano menciona modal de confirmação; o código usa `confirm()` nativo.
- O `$seo` injetado nos controllers CMS não é usado nas views.
- O `GalleryService`/`GalleryConfig` injetados no controller de planos não são usados
  pela UI.
- O card de informações da config lista as três tabelas, mas não a
  `v2_portal_configs`, que é onde as chaves vivem.
- A view de config não tem bloco de erros nem de flash local (depende do layout).

## 4. Integrabilidade

### 4.1 Superfície pública

| Tela | View | Variáveis | Controller |
|---|---|---|---|
| Leads (lista) | `v2-associese::cms.leads.index` | `$leads` (LengthAwarePaginator) | `MembershipLeadController@index` |
| Lead (detalhe) | `v2-associese::cms.leads.show` | `$lead` | `@show` |
| Planos (lista) | `v2-associese::cms.plans.index` | `$plans` (Collection com `gallery`) | `MembershipPlanController@index` |
| Plano (novo) | `v2-associese::cms.plans.create` | — | `@create` |
| Plano (editar) | `v2-associese::cms.plans.edit` | `$plan` (com `gallery`) | `@edit` |
| Formulário | `v2-associese::cms.plans._form` | `$plan` opcional | include |
| Config | `v2-associese::cms.config.index` | `$configs` (array), `$configKeys` (map) | `AssocieSeConfigController@index` |

### 4.2 Contrato de UI/JS

| Elemento | ID/classe | Contrato |
|---|---|---|
| Botão salvar galeria | `#sync-gallery-btn` | `onclick="syncGalleryMedias()"`; desabilita e troca o HTML |
| Container de IDs | `#group-ids-container-associese_gallery` | contém `input` com os IDs; o JS lê `value` de todos |
| Mensagens | `#creationSuccessModal` | modal Bootstrap global, aberto no `DOMContentLoaded` |
| Confirmação | `confirm()` | texto “Tem certeza que deseja remover este lead/plano?” |
| Formulários | `.associese-form` (portal), forms do CMS | CSRF sempre; `_method` quando necessário |
| Status | `.badge.bg-{warning|info|success|secondary}` | cor derivada do status |

### 4.3 Ordem e ciclo de vida

1. O layout do CMS inclui `flash::message` e o modal de criação no fim do body
   (`V2/layouts/cms/layouts/app.blade.php:90,138`).
2. A view de plano carrega o editor rico e, no edit, o componente de mídia; os scripts
   do componente são empilhados em `@push('scripts')`.
3. O botão de sync depende de jQuery já carregado pelo layout; a função é definida no
   `@push('scripts')` da view.
4. O modal de criação depende de `session('created_item')`; é “one-shot”.
5. Nenhuma tela registra `teardown`; o reload do sync reinicializa tudo.

### 4.4 Receitas de uso

**1. Adicionar uma coluna à lista de leads**

Edite `Views/cms/leads/index.blade.php` no `<thead>` e no `<tbody>`; use
`$lead-><campo>`. Se o campo for uma relação, adicione `with()` no controller para
evitar N+1.

**2. Abrir o formulário de plano em modo edição**

```blade
@include('v2-associese::cms.plans._form', ['plan' => $plan])
```

O partial detecta `$plan->exists` e inclui `@method('PUT')`; sem `$plan`, é criação.

**3. Disparar o sync manualmente**

```js
syncGalleryMedias(); // coleta #group-ids-container-associese_gallery e envia POST
```

**4. Renderizar o select de status**

```blade
<select name="status">
  @foreach(\V2\Modules\Institucional\AssocieSe\Models\MembershipLead::$statusLabels as $key => $label)
    <option value="{{ $key }}" @if($lead->status === $key) selected @endif>{{ $label }}</option>
  @endforeach
</select>
```

**5. Exibir o total de leads**

```blade
{{ $leads->total() }} registro(s)
```

### 4.5 Anti-uso

- **Não use `{!! $lead->message !!}`**: a view atual escapa; manter o escape evita XSS
  armazenado.
- **Não inclua o card de galeria no create sem endpoint**: o formulário de create não
  envia `media_ids`.
- **Não troque o `confirm()` por um modal que dependa de JS não carregado**: o CMS pode
  não ter o bundle.
- **Não altere o `name` do container da galeria**: o JS e o componente precisam do
  mesmo `associese_gallery`.
- **Não remova o `maxlength=1000` dos inputs de config**: é a única barreira client-side.

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

- Lista de planos sem paginação; cresce com o catálogo.
- Lista de leads com `ILIKE` sem índice; 20 por página.
- Config: 2 contadores a cada abertura.
- Galeria: N URLs resolvidas na view; N queries no sync.
- Rich editor: bundle pesado do serviço, carregado no create/edit.
- Sem cache de tela.

## 5. Rotas e contratos HTTP

As telas consomem as rotas de [`rotas.md`](rotas.md) §5.2. Resumo:

| Tela | Rotas |
|---|---|
| Leads lista/detalhe | `GET .../leads`, `GET .../leads/{id}`, `PUT|POST .../leads/{id}/status`, `DELETE .../leads/{id}` |
| Planos | `GET .../plans`, `GET .../plans/novo`, `POST .../plans/novo`, `GET .../plans/editar/{id}`, `PUT|POST .../plans/editar/{id}`, `DELETE .../plans/{id}`, `POST .../plans/{id}/gallery/sync` |
| Config | `GET .../configuracoes`, `PUT|POST .../configuracoes` |

## 6. Dados e modelo de domínio

As views leem `MembershipLead` (todos os campos, exceto `metadata`), `MembershipPlan`
(nome, preço, ciclo, vigência, `is_active`, `gallery`), `MembershipPlanGallery`
(`gallery_media_id`, `image`, `galleryMedia`) e o array `$configs` de
`v2_portal_configs`. Ver [`dados.md`](dados.md) §6.

## 7. Fluxos e estados

```mermaid
stateDiagram-v2
  [*] --> LeadsLista
  LeadsLista --> LeadDetalhe: clique no nome/olho
  LeadDetalhe --> LeadsLista: status salvo / back
  LeadDetalhe --> LeadsLista: lead removido
  LeadsLista --> PlanosLista: menu
  PlanosLista --> PlanoCreate: Novo Plano
  PlanosLista --> PlanoEdit: Editar
  PlanoCreate --> PlanosLista: salvar (modal)
  PlanoEdit --> PlanosLista: salvar
  PlanoEdit --> PlanoEdit: salvar galeria (reload)
  PlanosLista --> Config: menu
  Config --> Config: salvar (500)
```

## 8. UI CMS

### 8.1 Leads — lista

- **Rota/permissão**: `GET .../leads`, `read`.
- **Layout**: `v2-cms::layouts.app`, breadcrumb `Institucional › Associe-se › Leads`.
- **Objetivo**: triagem da fila.
- **Estados**: com resultados (tabela + paginação), vazio com filtro (botão limpar),
  vazio sem filtro.
- **Filtros**: `search` (nome/e-mail/empresa, placeholder “Buscar por nome, email ou
  empresa...”), `status` (Todos + 4 rótulos), sem data.
- **Colunas**: Nome (link), Email, Empresa (ou `-`), Status (badge), Data
  (`d/m/Y H:i`), ação (olho).
- **Paginação**: 20/página; componente `v2-cms::partials.pagination`; `appends`.
- **Ordenação**: `created_at desc` fixa; sem controle na UI.
- **Bulk**: não.
- **A11y**: `<table aria-label="Leads">`, `scope="col"` nos headers, label do status,
  `title="Visualizar"` no botão de olho, ícone `aria-hidden`.

### 8.2 Lead — detalhe

- **Rota/permissão**: `GET .../leads/{id}`, `read`.
- **Layout**: breadcrumb manual (sem partial).
- **Dados**: nome, e-mail (link `mailto:`), telefone, empresa, estado, cidade, mensagem,
  recebido em; `convertido em` só se `converted_at`.
- **Status**: badge no topo.
- **Formulário**: `PUT .../status`; `status` (select das 4 opções), `notes` (textarea 4
  linhas); botão “Atualizar”.
- **Remoção**: `DELETE`; botão outline-danger; `confirm()`.
- **Estados**: lead inexistente → `404`; sem permissão de escrita → o form aparece (GET
  read) e o POST é negado.
- **A11y**: labels `for`, `aria-hidden` nos ícones; sem `aria-describedby` para notas;
  botão de remover tem texto visível.

### 8.3 Planos — lista

- **Rota/permissão**: `GET .../plans`, `read`.
- **Ação de topo**: `action-create` “Novo Plano” (`page-actions`).
- **Colunas**: Nome, Preço (`R$ 0,00`), Ciclo (`billing_cycle_label`), Vigência
  (intervalo + badge), Mídias (badge com contagem ou `—`), Ativo (Sim/Não), ações
  (editar/lixeira).
- **Estados**: vazio “Nenhum plano cadastrado” + “Criar Primeiro Plano”.
- **Sem filtros/paginação**.
- **A11y**: `aria-label="Planos"`, `scope="col"`, `title` nos botões de ícone.

### 8.4 Plano — novo/editar

- **Rotas**: `GET/POST .../plans/novo` e `GET/PUT|POST .../plans/editar/{id}`; create
  exige `read` no GET e `write` no POST.
- **Campos**: nome (req, ≤255), descrição (rich editor, 300px), preço (number, step
  0.01, min 0, req), ciclo (`monthly`/`yearly`, req), ativo (checkbox), `valid_from`
  (date), `valid_until` (date).
- **Ajuda**: “Deixe em branco para vigência imediata.” e “...para sem data de
  expiração.”
- **Layout do edit**: formulário em `col-lg-8`; card de galeria em `col-lg-4`.
- **Galeria**: `media-group-input` (`accept=image/*,application/pdf`, owner
  `associese`), botão “Salvar Galeria”.
- **Estados**: erro de validação (volta com `$errors`, mas nem a view nem o layout do
  CMS os renderizam); exceção → flash de erro global.
- **Mensagens**: `Plano atualizado com sucesso!`, `Erro ao criar plano: ...`,
  `Erro ao atualizar plano: ...`, `Galeria salva com sucesso!`, `Erro ao salvar
  galeria`, `Erro de comunicação ao salvar galeria.`
- **A11y**: labels; switch com label; help text; o editor e o componente trazem suas
  próprias pendências (ver serviços).

### 8.5 Configurações

- **Rota/permissão**: `GET`, `read`; `PUT|POST`, `write`.
- **Formulário**: 7 campos derivados de `$configKeys`:
  - `associese_form_title` (texto)
  - `associese_form_subtitle` (texto)
  - `associese_intro_text` (textarea)
  - `associese_success_message` (textarea)
  - `associese_notify_email` (texto)
  - `associese_enabled` (switch)
  - `associese_show_plans` (switch)
- **Card de informações**: versão (`v2_version()`), “Associe-se (V2)”, tabelas
  `v2_membership_plans`, `v2_membership_leads`, `v2_membership_plan_gallery`, total de
  planos, total de leads.
- **Estados**: valores ausentes → strings vazias; switch desligado → hidden `0`.
- **Erros**: não renderizados; o `500` é a resposta visível do navegador (ou página de
  erro do Laravel).
- **A11y**: labels `for`; switches com label; sem `aria-describedby`; sem resumo de
  erros.

## 9. UI Portal

Não se aplica à UI CMS; ver [`portal.md`](portal.md).

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

- Todas as telas exigem `CmsAuthenticate` e a chave `AssocieSe`; a flag
  `module.active:AssocieSe` bloqueia o grupo inteiro com `404`.
- `read` abre lista/detalhe/formulários; `write` salva.
- O menu aparece na sidebar para quem tem permissão; a ordem dos submenus é
  Leads (1), Planos (2), Configurações (3) (`ModuleProvider.php:19-38`).
- Não há `v2can` nas views; a UI não esconde botões de escrita para `read` — o usuário
  só descobre no submit.

## 11. Observabilidade, erros e limites

- Flash global do layout para `success`/`error`; o módulo usa `flash()` (laracasts).
- `created_item` abre o modal; sem ele, nada.
- Erros de config: `500`; erros de galeria: alerta genérico; erros de validação: o
  layout não tem bloco de erros, então o usuário vê o form sem explicação (exceto os
  `is-invalid` do portal).
- Limites: 20 leads/página; 1000 caracteres por config; preço ≥ 0; UF ≤ 2; sem limite
  de descrição/mensagem.
- Sem logs de UI (a não ser via servidor).

## 12. Seeds e dados iniciais

Nenhum seeder de UI. O menu do CMS é montado a partir do `ModuleProvider` e das
permissões; não há registro em banco para “tela”. As telas assumem as tabelas migradas.

## 13. Lacunas

| ID | Lacuna | Onde procurar | Impacto |
|---|---|---|---|
| `LG-ASSOCIESE-C01` | Não há bloco de erros nas views de plano/config. | Layout do CMS/partials. | Feedback de validação. |
| `LG-ASSOCIESE-C02` | O comportamento esperado do switch de config após o `500` não é documentado. | Produto. | Confusão do operador. |
| `LG-ASSOCIESE-C03` | Não se sabe se o card de galeria deveria existir no create. | Histórico/UX. | Fluxo de cadastro. |
| `LG-ASSOCIESE-C04` | O texto do alerta de erro do sync não distingue falha de tipo de falha de rede. | Código/UX. | Diagnóstico. |
| `LG-ASSOCIESE-C05` | A ausência de `v2can` nas views pode ser intencional ou dívida. | Padrão de outros módulos. | Paridade de UI. |
| `LG-ASSOCIESE-C06` | Não há captura de `XSRF`/419 na UI. | JS. | Recuperação de sessão. |

## 14. Checklist de paridade

- [ ] `PC-ASSOCIESE-C01` — a lista de leads mostra `$leads->total()` e 20 linhas por
  página, com filtros preservados nos links. Fonte:
  `Views/cms/leads/index.blade.php:24,52,97-101`.
- [ ] `PC-ASSOCIESE-C02` — `status` e `search` vêm da query string e pré-preenchem os
  controles. Fonte: `Views/cms/leads/index.blade.php:3-4,27-42`.
- [ ] `PC-ASSOCIESE-C03` — o detalhe tem formulário `PUT` de status e formulário
  `DELETE` com `confirm()`. Fonte: `Views/cms/leads/show.blade.php:85-125`.
- [ ] `PC-ASSOCIESE-C04` — a lista de planos mostra badge “Fora da validade” quando o
  accessor é falso e “Sem prazo” quando não há datas. Fonte:
  `Views/cms/plans/index.blade.php:48-68`.
- [ ] `PC-ASSOCIESE-C05` — create de plano não tem card de galeria; edit tem. Fonte:
  `Views/cms/plans/create.blade.php:21-33`; `edit.blade.php:38-76`.
- [ ] `PC-ASSOCIESE-C06` — “Salvar Galeria” envia `_token` e `media_ids` e recarrega
  1,5 s após sucesso. Fonte: `Views/cms/plans/edit.blade.php:82-127`.
- [ ] `PC-ASSOCIESE-C07` — config tem 7 campos; switches enviam hidden `0`; textareas
  sem `maxlength`. Fonte: `Views/cms/config/index.blade.php:31-60`.
- [ ] `PC-ASSOCIESE-C08` — config exibe versão e contadores. Fonte:
  `Views/cms/config/index.blade.php:79-98`.
- [ ] `PC-ASSOCIESE-C09` — o modal de criação aparece só com `session('created_item')`
  e mostra `type`, `title`, `route`. Fonte:
  `V2/layouts/cms/partials/creation-success-modal.blade.php:1-22`.
- [ ] `PC-ASSOCIESE-C10` — `read` vê os botões de escrita, mas o POST é negado.
  Fonte: `V2PermissionMiddleware.php:88-110`.

## 15. Evidências consultadas

- `V2/Modules/Institucional/AssocieSe/Views/cms/leads/index.blade.php:1-116`;
  `Views/cms/leads/show.blade.php:1-129`.
- `V2/Modules/Institucional/AssocieSe/Views/cms/plans/index.blade.php:1-115`;
  `create.blade.php:1-35`; `edit.blade.php:1-129`; `_form.blade.php:1-68`.
- `V2/Modules/Institucional/AssocieSe/Views/cms/config/index.blade.php:1-102`.
- `V2/Modules/Institucional/AssocieSe/Controllers/MembershipLeadController.php:35-129`;
  `MembershipPlanController.php:45-248`; `AssocieSeConfigController.php:45-110`.
- `V2/layouts/cms/layouts/app.blade.php:90,138`;
  `V2/layouts/cms/partials/creation-success-modal.blade.php:1-22`;
  `V2/layouts/cms/partials/filter-card.blade.php`; `action-create.blade.php`;
  `pagination.blade.php`.
- `V2/Services/Media/Views/components/media-group-input.blade.php:1-70`;
  `V2/Services/ContentRichEditor/Views/components/forms/content-rich-editor.blade.php`.
- `V2/Core/Middleware/V2PermissionMiddleware.php:74-110`;
  `V2/Core/Middleware/ModuleAccessMiddleware.php:40-66`.
