---
id: MOD-INSTITUCIONAL-SOBRENOS-WIDGETS
tipo: modulo
onda: 5
status: concluido
unidades: [MOD-INSTITUCIONAL-SOBRENOS]
fontes:
  - V2/Modules/Institucional/SobreNos/
depende_de: [MOD-INSTITUCIONAL-SOBRENOS, SVC-LANDINGSECTIONSEDITOR, SVC-GALLERY, SVC-MEDIA, SVC-CONTENTRICHEDITOR, SVC-SEO, SVC-SEARCH, MOD-SUPPORT-STRUCTURALLANDINGEDITOR, MOD-CONEXOES-ASSOCIADOS, MOD-CONEXOES-UNIDADEREGIONAL]
consumidores: [MOD-INSTITUCIONAL-SOBRENOS-CMS, MOD-INSTITUCIONAL-SOBRENOS-PORTAL]
criticidade: media
dono: SobreNos
---

# Sobre Nos — widgets e integrações

> Anexo de [`spec.md`](spec.md). Documenta os cinco widgets `sobre-nos-*` (schema,
> form, render, attachments e `prepare`), como são registrados e validados pelo
> LandingSectionsEditor, a injeção de variantes WebP, e as oito integrações do módulo
> (Associados, Unidade Regional, LSE, SLE, Search, Gallery, CRE, SEO). É o anexo que um
> reimplementador deve ler antes de criar um widget novo ou de mexer no `prepare`.

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Define os cinco tipos de seção da página Sobre Nós, seus formulários no editor, suas views públicas e os dados que cada um resolve em tempo de render (imagens, variantes, membros, contadores). |
| Entrada | `type` do payload; `parameters[...]` do POST; AJAX `GET /cms/v2/landing-sections/form?type=sobre-nos-*`; `POST /cms/v2/landing-sections/validate`; coleção de `GalleryMedia` no `prepare`. |
| Saída | HTML de form e de render; `parameters` enriquecidos (caminhos de imagem, `*_variants`, `members[]`, `value` dos contadores); JSON `{"success":true}` ou `{success:false,errors:{…}}`. |
| Efeitos colaterais | Consultas a `v2_gallery_media`, `v2_board_members`, `v2_bienniums`, `v2_associado_instituicoes`, `v2_unidade_regionais`; leitura do JSON de fatores; nenhuma escrita. |
| Particularidade central | O contrato schema↔form↔render usa nomes diferentes para a mesma imagem: o schema e o form falam `*_id` (id da Galeria); o `prepare` grava `*` (caminho) e o renderer grava `*_variants`. Quem olhar só o render não encontra os campos. |
| Como se integra | `WidgetRegistry::register('sobre-nos-<x>', new …Widget())` no construtor do `ModuleProvider`; `BaseWidget` fornece `getFormView/getRenderView/getValidationRules/sanitize`; `LandingSectionsEditorRendererService` chama `getAttachmentIds` + `prepare`. |
| Se removido, quebra | O dropdown “Adicionar Seção” perde a categoria “Personalizados”; payloads existentes com esses tipos não renderizam (view inexistente → seção omitida) e o editor não abre o form (`404 Form view … not found`). |
| Superfície pública | 5 classes de widget, 5 classes de schema (`rules()` estático), 7 views de form (5 + 2 parciais), 5 views de render. |

## 1. Efeito

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

- **CMS**: adicionar uma seção `sobre-nos-*` no dropdown, ver seu formulário carregado por
  AJAX, editar os parâmetros, validar (o JS chama o endpoint de validação), pré-visualizar
  no iframe e salvar o payload inteiro.
- **CMS**: escolher imagens por id da Galeria (image picker) em about/video/testimonial e
  ícones por item em counters; adicionar/remover contadores dinamicamente.
- **Portal**: ver cada seção com as imagens resolvidas em URL (com `<picture>` e WebP), a
  diretoria hidratada e os contadores recalculados.

### 1.2 O que muda no sistema

| Efeito | Onde é produzido | Fonte |
|---|---|---|
| Os 5 tipos ficam disponíveis no registry no boot | `ModuleProvider::__construct` | `ModuleProvider.php:18-25` |
| O editor lista os tipos com ícone/título e agrupa por prefixo | `getSectionTypes` + blade | `LandingSectionsEditorRendererService.php:81-102`; `landing-sections-editor.blade.php:36-72` |
| Validação por tipo usa `getValidationRules()` do schema | `ValidationService::getRulesFor` | `V2/Services/LandingSectionsEditor/ValidationSchemas/ValidationService.php:36-53` |
| O form de cada widget é renderizado com `$data` = parameters | `getForm` | `LandingSectionsEditorController.php:67-94` |
| IDs de anexos são coletados e as mídias carregadas em lote | `prepareSections` | `LandingSectionsEditorRendererService.php:128-178` |
| Caminhos + `*_variants` são injetados | `prepare` + renderer | `:180-202` |
| Membros do biênio corrente entram na seção team | `SobreNosDataIntegrator` | `Services/SobreNosDataIntegrator.php:39-73` |
| Valores dos contadores são recalculados | `SobreNosCountersWidget::prepare` | `SobreNosCountersWidget.php:46-79` |

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

| Entrada | Resultado |
|---|---|
| `GET /cms/v2/landing-sections/form?type=sobre-nos-about&section_id=…&parameters=<json>` | `200` HTML do form; `400` “Section type is required.”; `404` `Form view for section type '<type>' not found.` (`LandingSectionsEditorController.php:67-94`). |
| `POST /cms/v2/landing-sections/validate` com `type=sobre-nos-about` e `parameters[title]` vazio | `422` `{"success":false,"errors":{"parameters.title":["The parameters.title field is required."]}}` (`:103-120`). |
| `POST .../validate` com `title` preenchido | `200` `{"success":true}`. |
| `POST /cms/v2/landing-sections/preview/render-section` com tipo/parameters | `200` `{"success":true,"html":"…"}`; `404` “Section view not found.” (`:195-242`). |
| `prepare` de `sobre-nos-about` com 5 ids válidos | `parameters` ganha `breadcrumb_image`, `image`, `image_2`, `icon_1`, `icon_2` e, pelo renderer, `*_variants`. |
| `prepare` de `sobre-nos-counters` com rótulo “Associados” | `counters[i].value` substituído pela contagem; `counters[i].icon` resolvido. |
| `prepare` de `sobre-nos-team` | Nenhuma alteração (dados vêm do integrador). |

### 1.4 O que este módulo NÃO faz

- Não registra os widgets em um service provider próprio: o registro é efeito colateral do
  construtor do `ModuleProvider`.
- Não cria rotas de widget: usa as rotas do serviço LSE.
- Não salva parâmetros por seção: o save é o payload inteiro no controller do módulo.
- Não valida nada no `saveSections` (ver [`cms.md`](cms.md) §3.2).
- Não aplica `sanitize()` no save: só no preview (`LandingSectionsEditorController.php:149-153`).
- Não grava dados derivados (membros/contadores) no payload salvo.

## 2. Abordagem

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

```mermaid
flowchart TB
  P[ModuleProvider::__construct] -->|register| R[(WidgetRegistry estático)]
  R --> T[getSectionTypes → dropdown do editor]
  T -->|type| F[GET /cms/v2/landing-sections/form] --> FV[getFormView]
  T -->|type+params| V[POST /validate] --> S[Schema::rules]
  R -->|prepareSections| A[getAttachmentIds] --> G[(v2_gallery_media)]
  G --> PR[prepare]
  PR --> RV[getRenderView com data]
  I[SobreNosDataIntegrator] --> PR
```

### 2.2 Mecânica passo a passo

1. No boot, `ModuleProvider` instancia cada widget e chama
   `WidgetRegistry::register('sobre-nos-<x>', new …)` (`ModuleProvider.php:18-25`).
2. O editor chama `LandingSectionsEditorRendererService::getSectionTypes()`, que primeiro
   registra os 21 widgets legados (só os ausentes) e devolve `tipo => {icon,title}`
   (`LandingSectionsEditorRendererService.php:36-102`).
3. O dropdown separa tipos por prefixo (`sucesu-`, `sobre-nos-`) em “Personalizados”.
4. Ao adicionar/editar, o JS chama `getForm`; o controller resolve o widget e renderiza
   `getFormView()` com `$data` (parameters) (`LandingSectionsEditorController.php:85-93`).
5. Ao salvar/validar no editor, o JS chama `validateSection`, que resolve as regras do
   schema do widget (`ValidationService.php:24-53`).
6. No portal, `prepareSections` (1) coleta `getAttachmentIds` de cada seção, (2) carrega
   todas as mídias em uma query `whereIn`, (3) chama `prepare($params, $anexos)`, (4) para
   cada attachment, injeta `fieldPath . '_variants'`
   (`LandingSectionsEditorRendererService.php:128-202`).
7. O viewer chama `prepareSections` novamente e inclui `getRenderView()` com
   `['section' => $section, 'data' => $section['parameters']]`
   (`landing-sections-viewer.blade.php:11-48`).

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

- **Registry estático por slug** `[C]`: permite que módulos adicionem tipos sem tocar no
  serviço; o custo é que o último registro vence e não há desregistro
  (`WidgetRegistry.php:13-33`).
- **Schema separado do widget** `[C]`: as regras vivem em classes com `rules()` estático e
  são resolvidas por `schemaClass` (`BaseWidget.php:43-52`), o que permite testá-las sem
  instanciar o widget.
- **Form e render em views do módulo** `[C]`: o módulo controla 100% do HTML dos seus
  tipos; o LSE só orquestra (`SobreNosAboutWidget.php:15-19`).
- **Contrato `*_id` → `*`** `[C]`: o banco guarda ids (portáveis), o render recebe
  caminhos (prontos para `picture`); a conversão fica no `prepare`
  (`SobreNosAboutWidget.php:49-70`).
- **Contadores auto-calculados no widget** `[C]`: mantém o schema simples (só strings) e
  evita uma tela de configuração de números (`SobreNosCountersWidget.php:46-79`).

### 2.4 Trechos de código pivô

```php
// V2/Modules/Institucional/SobreNos/LseWidgets/Widgets/SobreNosAboutWidget.php:13-20
public function __construct()
{
    $this->icon = 'bi bi-file-earmark-image';
    $this->title = 'Sobre Nós — Apresentação';
    $this->schemaClass = SobreNosAboutSchema::class;
    $this->formView = 'v2-sobrenos::lse.forms.about';
    $this->renderView = 'v2-sobrenos::lse.render.about';
}
```

> Cinco propriedades definem o widget: ícone/título no dropdown, schema de validação e as
> duas views. `BaseWidget` infere o tipo pela classe se `formView`/`renderView` forem
> nulos (`BaseWidget.php:72-93,154-159`), mas aqui tudo é explícito.

```php
// V2/Modules/Institucional/SobreNos/LseWidgets/Widgets/SobreNosAboutWidget.php:49-70
public function prepare(array $parameters, Collection $anexos = null): array
{
    if (!$anexos || $anexos->isEmpty()) {
        return $parameters;
    }
    $map = [
        'breadcrumb_image_id' => 'breadcrumb_image',
        'image_id'            => 'image',
        'image_2_id'          => 'image_2',
        'icon_1_id'           => 'icon_1',
        'icon_2_id'           => 'icon_2',
    ];
    foreach ($map as $idKey => $targetKey) {
        if (!empty($parameters[$idKey]) && ($m = $anexos->get($parameters[$idKey]))) {
            $parameters[$targetKey] = $m->file_path;
        }
    }
    return $parameters;
}
```

> A coleção `$anexos` é keyed por id (`keyBy('id')`) e contém objetos de
> `DB::table('v2_gallery_media')` (stdClass), não models — por isso
> `$m->file_path`/`$m->variants`. Se a mídia não existir, a chave de caminho simplesmente
> não é criada e o render pula a imagem.

```php
// V2/Modules/Institucional/SobreNos/LseWidgets/Widgets/SobreNosCountersWidget.php:30-52
public function getAttachmentIds(array $parameters): array
{
    $ids = [];
    foreach ($parameters['counters'] ?? [] as $i => $counter) {
        if (!empty($counter['icon_id'])) {
            $ids["counters.{$i}.icon"] = $counter['icon_id'];
        }
    }
    return $ids;
}

public function prepare(array $parameters, Collection $anexos = null): array
{
    if ($anexos && !$anexos->isEmpty()) {
        foreach ($parameters['counters'] ?? [] as $i => $counter) {
            if (!empty($counter['icon_id']) && ($m = $anexos->get($counter['icon_id']))) {
                $parameters['counters'][$i]['icon'] = $m->file_path;
            }
        }
    }
    /* … heurística de valores … */
}
```

> O `fieldPath` é aninhado (`counters.0.icon`), então o renderer grava
> `counters.0.icon_variants` via `array_set` (`LandingSectionsEditorRendererService.php:188-197`).
> Esse é o único widget do módulo com attachments em lista.

```php
// V2/Services/LandingSectionsEditor/Services/LandingSectionsEditorRendererService.php:180-197
foreach ($sections as &$section) {
    $type = $section['type'] ?? '';
    $params = $section['parameters'] ?? [];
    if (isset($widgets[$type])) {
        $section['parameters'] = $widgets[$type]->prepare($params, $anexos);
        $attachmentIds = $widgets[$type]->getAttachmentIds($params);
        foreach ($attachmentIds as $fieldPath => $mediaId) {
            if ($m = $anexos->get($mediaId)) {
                $variants = json_decode($m->variants ?? '{}', true) ?: [];
                if (!empty($variants)) {
                    array_set($section['parameters'], $fieldPath . '_variants', $variants);
                }
            }
        }
    } else {
        $section['parameters'] = $this->legacyPrepare($params, $anexos);
    }
}
```

> O renderer usa os **params originais** para coletar attachments (antes do `prepare`),
> o que é importante para widgets que apagam o id no prepare — não é o caso destes cinco.
> Se o tipo não está registrado, aplica o `legacyPrepare` (campos `background_image_id`,
> `image_id`, `steps`, `items`, `cards`).

```php
// V2/Services/LandingSectionsEditor/ValidationSchemas/ValidationService.php:24-53
public function validate(Request $request): void
{
    $type = $request->input('type');
    if (!$type) { return; }
    $rules = $this->getRulesFor($type);
    if (!empty($rules)) {
        Validator::make($request->all(), $rules)->validate();
    }
}

public function getRulesFor(string $type): array
{
    $widget = WidgetRegistry::get($type);
    if ($widget) {
        return $widget->getValidationRules();
    }
    /* fallback: V2\Services\LandingSectionsEditor\ValidationSchemas\<Studly>Schema */
}
```

> A validação dos schemas `sobre-nos-*` só acontece por aqui. Como o JS do editor chama o
> endpoint, um POST direto em `saveSections` pode gravar parâmetros fora das regras.

### 2.5 Algoritmos, consultas e custos

- `prepareSections`: 1 query `whereIn` em `v2_gallery_media` para todas as seções
  (`LandingSectionsEditorRendererService.php:169-177`); custo O(seções × attachments) em
  PHP para montar o mapa.
- `getSectionTypes()` chama `registerLegacyWidgets()` a cada invocação (idempotente por
  `has()`), o que é barato mas acontece em cada request do editor.
- `SobreNosCountersWidget::prepare`: 1 `count()` de associados e 1+N de empresas (ver
  [`spec.md`](spec.md) §2.5).
- `SobreNosDataIntegrator`: 3 queries (biênio, membros, mídias) por render da seção team.
- Todas as views de render usam `picture`, que resolve URL via `StorageService` (sem query
  quando o caminho já está pronto).

## 3. Particularidades

### 3.1 O que só existe aqui

- **Cinco widgets de módulo** num serviço compartilhado, registrados no boot, com schema,
  form e render próprios.
- **Heurística de contador por label** — nenhum outro widget altera valores do payload.
- **Prefixos `sobre-nos-`** como critério de agrupamento visual no editor.
- **Contrato de attachments com caminho aninhado** (`counters.N.icon`).

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

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| RN-SOBRENOS-W01 | O tipo é o slug registrado; renomear o slug órfã os payloads existentes | Seção deixa de renderizar/editar | `ModuleProvider.php:20-24`; payload histórico |
| RN-SOBRENOS-W02 | `title` só é obrigatório no schema about | Team/video/counters/testimonial aceitam título vazio e usam defaults no Blade | `Schemas/SobreNosAboutSchema.php:14`; `Schemas/SobreNosTeamSchema.php:13-15` |
| RN-SOBRENOS-W03 | Imagens são ids (`*_id`) no payload; caminhos (`*`) só existem após `prepare` | Salvar o payload preparado gravaria caminhos em vez de ids | `SobreNosAboutWidget.php:49-70` |
| RN-SOBRENOS-W04 | Variantes são injetadas como `<fieldPath>_variants` a partir de `GalleryMedia.variants` (JSON) | Render espera `image_variants`, `counters.N.icon_variants` | `LandingSectionsEditorRendererService.php:188-197` |
| RN-SOBRENOS-W05 | Sem anexos (`null`/vazia), `prepare` devolve os params intactos | Nenhuma chave de caminho é criada; render não mostra imagem | `SobreNosAboutWidget.php:51-53` |
| RN-SOBRENOS-W06 | `getAttachmentIds` do team é vazio | O renderer não injeta nada; dados vêm do integrador | `SobreNosTeamWidget.php:28-31` |
| RN-SOBRENOS-W07 | Contadores resolvem ícone e depois sobrescrevem o valor por rótulo | Ordem importa: o valor do CMS nunca vence quando o rótulo casa | `SobreNosCountersWidget.php:46-79` |
| RN-SOBRENOS-W08 | O schema dos contadores não limita o número de itens nem o valor a numérico | `value` é string livre até 50 chars (antes de ser sobrescrito) | `Schemas/SobreNosCountersSchema.php:13-18` |
| RN-SOBRENOS-W09 | O preview aplica `sanitize()` (strip_tags em todo string) | `<br>` dos títulos de ícone some no preview, mas permanece no payload salvo | `BaseWidget.php:57-65`; `LandingSectionsEditorController.php:149-153` |
| RN-SOBRENOS-W10 | O registro legado só adiciona tipos ausentes | Um módulo pode sobrescrever um tipo legado, mas não o contrário | `LandingSectionsEditorRendererService.php:62-73` |
| RN-SOBRENOS-W11 | `prepare()` é chamado 2× no portal | Queries dobradas; resultado idempotente | `SobreNosPortalController.php:67`; `landing-sections-viewer.blade.php:13` |

### 3.3 Casos-limite e comportamentos surpreendentes

- **Payload com `type` não registrado**: o viewer tenta `v2-service-landingsectionseditor::render.<type>`;
  se não existir, nada é renderizado (sem erro visível).
- **Widget desregistrado em runtime** (outro código sobrescrevendo a chave): o módulo perde
  form/render e o editor passa a dar 404 ao abrir a seção.
- **`parameters` ausente**: `prepare` recebe `[]`; contadores não fazem nada; about vira
  render mínimo com defaults.
- **`counters` não-array** (string): `foreach` sobre string emite warning no PHP 7; a
  validação do editor impede, mas o save direto não.
- **`icon_id` de mídia inexistente**: attachment é coletado, `$anexos->get()` devolve null,
  nenhuma chave `icon`; o render omite o ícone.
- **Vídeo sem `youtube_url`**: href `#`; o lightbox não abre nada.
- **Título do about vazio**: default “Sobre a SUCESU”; o schema impede (required), mas um
  save direto pode gravar.
- **`bio` não é parâmetro de widget**: pertence ao membro, não ao payload.

### 3.4 Armadilhas de reimplementação

- **Não** troque `formView`/`renderView` por views do LSE: os widgets dependem de `$data`
  já preparado e de estruturas CSS do tema.
- **Não** use `parameters[image]` como fonte: o payload guarda `image_id`.
- **Não** reaproveite o `prepare` do about para counters: o caminho das chaves é aninhado.
- **Não** remova o `try/catch` do integrador: a seção team deve degradar para vazio, não
  derrubar a página.
- **Não** valide parâmetros apenas no JS: o endpoint de validação é o contrato do editor,
  mas o save não o repete.
- **Não** registre widgets fora do boot: `getSectionTypes()` é chamado no render do editor
  e pode ocorrer antes de um registro tardio.

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

- O nome do campo no form (`icon_1_title`) sugere HTML (`<br>` é aceito), mas o `sanitize`
  do preview remove as tags — comportamento inconsistente entre preview e portal.
- `SobreNosTestimonialWidget` é completo (form/render/schema), mas o portal filtra o tipo;
  o trabalho de manutenção é inútil hoje.
- `SobreNosVideoWidget` não valida que `youtube_url` seja do YouTube (só `url`).
- `SobreNosTeamWidget` declara `description` com `max:2000`, mas o form usa `textarea`
  de 2 linhas e o render imprime como texto puro.
- `BaseWidget::inferType()` não é usado por nenhum dos cinco widgets, já que todos definem
  views explicitamente.

## 4. Integrabilidade

### 4.1 Superfície pública

| Símbolo | Assinatura | Papel |
|---|---|---|
| `SobreNosAboutWidget` | `getAttachmentIds(array): array`, `prepare(array, ?Collection): array` | 5 imagens, mapa `*_id` → `*` |
| `SobreNosTeamWidget` | idem (ambos vazios/identidade) | placeholders de título/descrição |
| `SobreNosVideoWidget` | `background_image_id` → `background_image` | fundo do vídeo |
| `SobreNosCountersWidget` | `counters.N.icon_id` → `counters.N.icon`; recalcula `value` | contadores |
| `SobreNosTestimonialWidget` | 5 imagens (`image`, `client_image`, `icon`, `shape_1`, `shape_2`) | depoimento (não renderizado) |
| `SobreNos*Schema::rules()` | `static rules(): array` | validação no endpoint LSE |
| `WidgetRegistry::{register,get,all,has}` | estático | descoberta/registro |
| `LandingSectionsEditorRendererService::{getWidgets,getSectionTypes,prepareSections,sanitizeContent}` | estático/instância | orquestração |

### 4.2 Contrato de UI/JS

- **Form (props recebidas)**: `$data` (parameters), `$sectionId`, `$type`
  (`LandingSectionsEditorController.php:93`). Os forms do módulo só usam `$data`.
- **Nomes de campo**: `parameters[<chave>]`; contadores usam
  `parameters[counters][N][<campo>]` e image-picker com `inputId`
  `sobrenos-counter-N-<campo>` (`_item-card.blade.php:30-40`).
- **DOM de contadores**: `#counters-items-container`, `#btn-add-counter`,
  `#counters-item-template`, `#counters-no-items-msg`; template clonado com
  `__INDEX__` substituído (`counters.blade.php:8-52`; `_item-list-js.blade.php:71-81`).
- **Reindexação**: o JS reescreve `name`, `id`, `for` e `data-input-id` dos itens após
  remover/adicionar (`_item-list-js.blade.php:32-69`).
- **IDs fixos do about/video/testimonial**: `sobrenos-about-*`, `sobrenos-video-background`,
  `sobrenos-testimonial-*`.

### 4.3 Ordem e ciclo de vida

1. Boot → registro dos 5 widgets.
2. Editor → `getSectionTypes()` (registra legados ausentes) → dropdown.
3. Form → `getForm` → `getFormView()`.
4. Validate → `ValidationService` → `getValidationRules()` → schema.
5. Preview → `savePreview` (sanitiza e cacheia 1 h) → `renderPreview`/`renderSection`
   (`prepareSections`).
6. Portal → `prepareSections` no controller + no viewer → `getRenderView()`.

### 4.4 Receitas de uso

```php
// Registrar um widget novo do módulo (mesmo padrão dos cinco)
WidgetRegistry::register('sobre-nos-faq', new class extends \V2\Services\LandingSectionsEditor\Widgets\BaseWidget {
    protected $icon = 'bi bi-question-circle';
    protected $title = 'Sobre Nós — FAQ';
    protected $schemaClass = \V2\Modules\Institucional\SobreNos\LseWidgets\Schemas\SobreNosTeamSchema::class;
    protected $formView = 'v2-sobrenos::lse.forms.team';
    protected $renderView = 'v2-sobrenos::lse.render.team';
});
```

```php
// Resolver a view de render de um tipo qualquer (com fallback do LSE)
$widget = \V2\Services\LandingSectionsEditor\Services\WidgetRegistry::get('sobre-nos-counters');
$view = $widget ? $widget->getRenderView() : 'v2-service-landingsectionseditor::render.sobre-nos-counters';
```

```blade
{{-- Renderizar um form de widget fora do editor (com parameters já carregados) --}}
@include('v2-sobrenos::lse.forms.about', ['data' => $parameters])
```

### 4.5 Anti-uso

- **Chamar `prepare()` sem a coleção de mídias** esperando caminhos resolvidos: ele
  devolve os params intactos (`SobreNosAboutWidget.php:51-53`).
- **Passar `$section['parameters']` do payload salvo direto para o render**: faltam
  `image`, `*_variants` e `members`.
- **Registrar o mesmo slug em dois módulos**: o último vence silenciosamente
  (`WidgetRegistry.php:20-22`).

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

- 1 query em lote de mídias por `prepareSections` (×2 no portal).
- Validação é por requisição AJAX por seção; sem debounce documentado no módulo.
- Preview cacheia o payload em `landing_preview_<token>` por 1 h
  (`LandingSectionsEditorController.php:153`).
- Upload de mídia no editor: 10 MB por arquivo (`:268-272`).

### 4.7 Mapa das oito integrações

| Integração | Unidade dona | O que o módulo consome | Ponto de entrada | Falha / comportamento | Risco de reimplementação |
|---|---|---|---|---|---|
| **Associados** | `MOD-CONEXOES-ASSOCIADOS` | `AssociadoInstituicao::active()->count()` como valor de contador | `SobreNosCountersWidget::prepare` (`:61-63`) | Exceção de query **não** é capturada no widget → propaga e derruba a renderização (fail-closed); `is_active` é o único filtro | Contar `is_active` é diferente de contar linhas: associados arquivados/inativos não entram |
| **Unidade Regional** | `MOD-CONEXOES-UNIDADEREGIONAL` | `UnidadeRegional::active()` (campo `state`) + `UnidadeRegionalEmpresa` com `empresa.is_active=true` | `EmpresasImpactadasCalculator::getAssociadosPorEstado` (`:32-50`) | N+1 por regional; UF sem fator no JSON é ignorada; regional sem `state` é pulada; exceção propaga | Duas regionais na mesma UF somam; o vínculo é por `unidade_regional_id`, não por UF |
| **LSE** | `SVC-LANDINGSECTIONSEDITOR` | `WidgetRegistry`, `BaseWidget`, `LandingSectionsEditorRendererService::{getSectionTypes,prepareSections,sanitizeContent}`, componentes de editor/viewer, endpoints de form/validate/preview | `ModuleProvider` (registro); `portal.index` (viewer) | Tipo não registrado → fallback `legacyPrepare` e view `v2-service-landingsectionseditor::render.<type>`; se não existir, seção omitida | O módulo não controla a ordem de registro nem o `sanitize`; mudanças no serviço afetam os 5 widgets |
| **SLE** | `MOD-SUPPORT-STRUCTURALLANDINGEDITOR` | `LayoutBaseConfig` com `key='sobre_nos_sections'`, cast `array`, `SoftDeletes` | `SobreNosCmsController` e `SobreNosPortalController` | Se a linha não existir, portal renderiza alerta de vazio; se estiver soft-deleted, o portal não a vê (`where` sem `withTrashed`) e o save a restaura | A `key` não é unique; uma segunda linha quebraria o `first()` |
| **Search** | `SVC-SEARCH` | `CMSIndexDTO('diretoria', editUrl, name, texto)`; `PublicSearchDTO(name, str_slug(name), 'diretoria', 'board_member', id, description, [diretoria, board_member, position], deep)`; `remove()` no delete; `SearchService::publicResultUrl` mapeia `diretoria` → `/equipe/{slug}` | hooks `saved`/`deleted` (`Models/BoardMember.php:101-116,218-257`) | Toda falha é capturada e logada (`[BoardMember] Falha ...`); save do membro continua | `slug` do índice é `str_slug(name)` recomputado — não é o `slug` persistido; se o banco tiver slug diferente, a URL de busca divergirá da real |
| **Gallery** | `SVC-GALLERY` | `v2_gallery_media` por `photo_id` (foto do membro) e por ids nos parâmetros (`*_id`); `variants` JSON; `file_path`; upload via `media-input`/editor | `BoardMemberController::resolvePhotoFromMedia`; `prepareSections` | Sem FK: id órfão → `photo=null` (membro) ou imagem omitida (seção); upload pode falhar silenciosamente no picker | `photo_id` é string(36) apontando para id inteiro; não validar `owner_module` permite reutilizar mídia de qualquer módulo |
| **CRE** | `SVC-CONTENTRICHEDITOR` | `bio` (string JSON ou AST) → `toHtml` | `SobreNosPortalController::show` e `BoardMember::getHtmlBio` | Falha → `Log::warning`/`Log::error` e HTML vazio; `ensureArray` aceita string (decodifica) ou array | O cast `array` do model grava o JSON como string (dupla codificação); reimplementar com cast `json` muda o formato armazenado |
| **SEO** | `SVC-SEO` | `setTitle/setDescription/setKeywords/setCoverImage/addSchema`; `trackAccess` (composer); `getCoverImage` (fallback logo); sitemap `addBoardMembers` | `SobreNosPortalController::index/show`; `SeoPortalController` | Sempre definido no controller; sitemap ignora `is_active` e usa `/diretoria*` (301); `trackAccess` é best-effort | O canonical é `url()->current()`; os dois JSON-LD vivem em pontos diferentes (controller e view) |

> Regra de ouro: **nenhuma dessas integrações é opcional**. O módulo não tem fallback
> próprio para Associados, Unidade Regional, LSE ou SLE; só Search, Gallery e CRE têm
> degradação tolerante (logs + valor ausente).

## 5. Rotas e contratos HTTP

Rotas do módulo em [`spec.md`](spec.md) §5. As rotas do serviço usadas pelos widgets:

| Método | URI | Nome | Uso pelos widgets | Fonte |
|---|---|---|---|---|
| GET | `/cms/v2/landing-sections/form` | `v2.landing-sections.form` | Carregar form do tipo | `V2/Services/LandingSectionsEditor/Routes/cms.php:12` |
| POST | `/cms/v2/landing-sections/validate` | `v2.landing-sections.validate` | Validar parameters pelo schema | `:13` |
| POST | `/cms/v2/landing-sections/preview/save` | `v2.landing-sections.preview.save` | Cachear payload sanitizado | `:14` |
| GET | `/cms/v2/landing-sections/preview/render` | `v2.landing-sections.preview.render` | Página de preview | `:15` |
| POST | `/cms/v2/landing-sections/preview/render-section` | `v2.landing-sections.render-section` | Render de uma seção | `:16` |
| GET/POST/PUT/DELETE | `/cms/v2/landing-sections/media*` | `v2.landing-sections.media.*` | Image picker/upload | `:19-23` |

## 6. Dados e modelo de domínio

Parâmetros esperados (schema → form → render):

| Widget | Parâmetros do schema/form | Chaves produzidas por `prepare` | Consumidas no render |
|---|---|---|---|
| about | `subtitle`, `title`, `description_1`, `description_2`, `cta_text`, `cta_anchor`, `breadcrumb_image_id`, `image_id`, `image_2_id`, `icon_1_id`, `icon_1_title`, `icon_2_id`, `icon_2_title`, `counter_value`, `counter_label` | `breadcrumb_image`, `image`, `image_2`, `icon_1`, `icon_2` (+ `*_variants`) | todas |
| team | `subtitle`, `title`, `description` | — (integrador adiciona `members[]`, `biennium_label`) | `subtitle`, `title`, `description`, `members[]`, `biennium_label` |
| video | `subtitle`, `title`, `youtube_url`, `watch_label`, `background_image_id` | `background_image` (+ variants) | todas |
| counters | `counters[].icon_id`, `value`, `suffix`, `label` | `counters[].icon` (+ `icon_variants`) e `value` recalculado | `icon`, `icon_variants`, `value`, `suffix`, `label` |
| testimonial | `text`, `client_name`, `client_role`, `image_id`, `client_image_id`, `icon_id`, `shape_1_id`, `shape_2_id` | `image`, `client_image`, `icon`, `shape_1`, `shape_2` (+ variants) | não renderizado no portal |

Tipos e limites por schema:

| Campo | Regra | Fonte |
|---|---|---|
| about `title` | required, string, 255 | `SobreNosAboutSchema.php:14` |
| about `counter_value` | string, 50 | `:26` |
| team `description` | string, 2000 | `SobreNosTeamSchema.php:15` |
| video `youtube_url` | url, 500 | `SobreNosVideoSchema.php:15` |
| video `title` | string, 500 | `:14` |
| counters `counters` | array (itens sem regra de obrigatoriedade) | `SobreNosCountersSchema.php:13-17` |
| testimonial `text` | string, 2000 | `SobreNosTestimonialSchema.php:13` |
| todos os `*_id` | string, 36 | about `:19-25`, testimonial `:16-20`, video `:17`, counters `:14` |

## 7. Fluxos e estados

```mermaid
sequenceDiagram
  participant J as JS do editor
  participant L as LandingSectionsEditorController
  participant W as Widget/Schema
  J->>L: GET /form?type=sobre-nos-counters
  L->>W: WidgetRegistry::get(type)->getFormView()
  L-->>J: HTML do form
  J->>L: POST /validate (type, parameters)
  L->>W: getValidationRules()
  L-->>J: 200 ou 422
  J->>L: POST /preview/render-section
  L->>W: prepareSections → prepare
  L-->>J: {"success":true,"html":"…"}
```

- `FL-SOBRENOS-W01` — adicionar seção (form por AJAX).
- `FL-SOBRENOS-W02` — validar parâmetros.
- `FL-SOBRENOS-W03` — preview de seção (sanitiza + prepara).
- `FL-SOBRENOS-W04` — render no portal (prepara 2× e inclui a view).

## 8. UI CMS

Os forms dos widgets são descritos em [`cms.md`](cms.md) §8 (UI-SOBRENOS-01 e UI-SOBRENOS-04
para o contexto). Campos e ids:

| Widget | Campos e ids |
|---|---|
| about | subtitle, title, description_1, description_2, cta_text, cta_anchor; image-pickers `sobrenos-about-breadcrumb`, `sobrenos-about-image`, `sobrenos-about-image-2`, `sobrenos-about-icon-1`, `sobrenos-about-icon-2`; títulos de ícone (aceitam `<br>`); counter_value/label |
| team | subtitle, title, description; aviso “Os membros da diretoria são gerenciados na seção Membros e aparecem automaticamente.” |
| video | subtitle, title, youtube_url, watch_label; picker `sobrenos-video-background` |
| counters | lista dinâmica com ícone (picker), valor, sufixo, label; botão “Adicionar Contador”; vazio: “Nenhum contador cadastrado. Clique em "Adicionar Contador" para começar.” |
| testimonial | text, client_name, client_role; pickers `sobrenos-testimonial-image`, `-client`, `-icon`, `-shape-1`, `-shape-2` |

## 9. UI Portal

Views de render e seus contratos estão em [`portal.md`](portal.md) §9. Resumo do uso de
`$data`:

- about: `$data['breadcrumb_image']`, `image`, `image_2`, `icon_1/2`, `*_variants`,
  `subtitle`, `title`, `description_1/2`, `cta_text/anchor`, `counter_value/label`.
- team: `subtitle`, `title`, `description`, `members[]`, `biennium_label`.
- video: `subtitle`, `title`, `youtube_url`, `watch_label`, `background_image`.
- counters: `counters[]` com `icon`, `icon_variants`, `value`, `suffix`, `label`.
- testimonial: nunca incluído no portal.

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

- Sem config própria; o registro depende apenas do boot do módulo.
- Permissão: as rotas do serviço LSE passam por `CmsAuthenticate` (não por
  `V2PermissionMiddleware` com a chave `SobreNos`), portanto um usuário com acesso ao CMS
  e papéis pode usar os endpoints de form/validate/preview
  (`V2/Services/LandingSectionsEditor/Routes/cms.php:9`).
- Flag `module.active:SobreNos` não afeta as rotas do serviço LSE.

## 11. Observabilidade, erros e limites

- Erros do serviço: `[LandingSectionsEditor] Section validation failed: …` (error),
  `[LandingSectionsEditor] Failed to fetch attachments: …` (error).
- 400/404/422/500 descritos em §1.3.
- Limites: string 255/500/2000/50/20/36 conforme schema; upload 10 MB.
- Nenhum log próprio dos widgets; falhas silenciosas em `prepare` (mídia inexistente).

## 12. Seeds e dados iniciais

O seeder preenche `parameters` exatamente nos formatos acima, com ids de mídia retornados
por `processImage(...)['id']` (`Seeds/SobreNosDefaultSectionsSeeder.php:26-39,41-118`).
Os contadores são semeados com valores que serão sobrescritos no render (`45`, `5000`,
`100`).

## 13. Lacunas

- `LG-SOBRENOS-W01` — Não há teste de contrato entre schema e form (nomes de campos
  divergentes passariam despercebidos).
- `LG-SOBRENOS-W02` — Não foi possível confirmar se o JS do editor valida **antes** de
  cada save ou apenas ao editar a seção; o fluxo exato está no serviço LSE (fora do
  escopo desta unidade).
- `LG-SOBRENOS-W03` — O comportamento de `array_set` para índices numéricos em PHP 7.0–7.2
  não foi testado; pode haver diferença de versão.
- `LG-SOBRENOS-W04` — Não há consumidor identificado para o widget testimonial; a intenção
  original é desconhecida.

## 14. Checklist de paridade

- [ ] `PC-SOBRENOS-W01` — Os 5 tipos aparecem no dropdown sob “Personalizados” (`ModuleProvider.php:18-25`; `landing-sections-editor.blade.php:36-72`).
- [ ] `PC-SOBRENOS-W02` — Cada tipo resolve um form e um render próprios (`getFormView`/`getRenderView`).
- [ ] `PC-SOBRENOS-W03` — `title` é obrigatório só no about (`SobreNosAboutSchema.php:14`).
- [ ] `PC-SOBRENOS-W04` — `prepare` do about converte 5 ids em caminhos e o renderer adiciona `*_variants` (`SobreNosAboutWidget.php:49-70`).
- [ ] `PC-SOBRENOS-W05` — `prepare` do video converte `background_image_id` (`SobreNosVideoWidget.php:40-48`).
- [ ] `PC-SOBRENOS-W06` — `prepare` do testimonial converte 5 ids (mesmo não sendo renderizado) (`SobreNosTestimonialWidget.php:49-67`).
- [ ] `PC-SOBRENOS-W07` — Counters aninham attachments em `counters.N.icon` e injetam `icon_variants` (`SobreNosCountersWidget.php:30-39`; renderer `:188-197`).
- [ ] `PC-SOBRENOS-W08` — Contadores recalculam `value` por rótulo (`:58-76`).
- [ ] `PC-SOBRENOS-W09` — Sem mídias, `prepare` não cria chaves de caminho (`SobreNosAboutWidget.php:51-53`).
- [ ] `PC-SOBRENOS-W10` — Validação do editor usa `Schema::rules()` e devolve 422 com erros (`ValidationService.php:24-53`).
- [ ] `PC-SOBRENOS-W11` — Form inexistente devolve 404 com a mensagem literal.
- [ ] `PC-SOBRENOS-W12` — Tipo desconhecido não renderiza nada (view ausente é ignorada).

## 15. Evidências consultadas

| Arquivo | Faixas |
|---|---|
| `LseWidgets/Widgets/SobreNosAboutWidget.php` | 9-71 |
| `LseWidgets/Widgets/SobreNosTeamWidget.php` | 9-44 |
| `LseWidgets/Widgets/SobreNosVideoWidget.php` | 9-50 |
| `LseWidgets/Widgets/SobreNosCountersWidget.php` | 11-80 |
| `LseWidgets/Widgets/SobreNosTestimonialWidget.php` | 9-68 |
| `LseWidgets/Schemas/SobreNosAboutSchema.php` | 5-30 |
| `LseWidgets/Schemas/SobreNosTeamSchema.php` | 5-18 |
| `LseWidgets/Schemas/SobreNosVideoSchema.php` | 5-20 |
| `LseWidgets/Schemas/SobreNosCountersSchema.php` | 5-20 |
| `LseWidgets/Schemas/SobreNosTestimonialSchema.php` | 5-23 |
| `Views/lse/forms/*` | conforme citado |
| `V2/Services/LandingSectionsEditor/Services/LandingSectionsEditorRendererService.php` | 29-205 |
| `V2/Services/LandingSectionsEditor/Services/WidgetRegistry.php` | 13-45 |
| `V2/Services/LandingSectionsEditor/Widgets/BaseWidget.php` | 7-160 |
| `V2/Services/LandingSectionsEditor/ValidationSchemas/ValidationService.php` | 10-54 |
| `V2/Services/LandingSectionsEditor/Controllers/LandingSectionsEditorController.php` | 67-242 |
| `V2/Services/LandingSectionsEditor/Routes/cms.php` | 9-25 |
