---
id: MOD-INSTITUCIONAL-SOBRENOS-CMS
tipo: modulo
onda: 5
status: concluido
unidades: [MOD-INSTITUCIONAL-SOBRENOS]
fontes:
  - V2/Modules/Institucional/SobreNos/
depende_de: [MOD-INSTITUCIONAL-SOBRENOS, CORE-CMS, CORE-AUTH, SVC-LANDINGSECTIONSEDITOR, SVC-MEDIA, SVC-CONTENTRICHEDITOR, SVC-ACTIVITYLOGGER]
consumidores: [UI-CMS]
criticidade: media
dono: SobreNos
---

# Sobre Nos — UI CMS

> Anexo de [`spec.md`](spec.md). Cobre as três telas reais do CMS — editor de seções
> (LandingSectionsEditor), listagem de membros com modal de biênios e formulário de
> membro — com campos, validações, mensagens literais, JS inline e lacunas de
> acessibilidade. O ponto delicado é que o **estado de publicação de um membro**
> (`is_active`) e a **ordem** (`sort_order`) não existem no formulário: são derivados do
> padrão a cada gravação.

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Permite editar o JSON de seções da página, manter membros da diretoria (com foto, bio e slug automático) e configurar biênios, tudo sob RBAC `SobreNos`. |
| Entrada | Cliques e submits em `cms/institucional/sobre-nos*`; JSON serializado pelo editor; campos de `_form`; AJAX do editor LSE e do `media-input`. |
| Saída | HTML `v2-sobrenos::cms.editor` e `v2-sobrenos::cms.members.{index,create,edit}`; `302` com flash/`created_item`; `activity_log`; `page:*` limpo. |
| Efeitos colaterais | Salvamento do payload, criação/edição/arquivamento de membros e biênios, reindexação de busca, auditoria, limpeza de cache. |
| Particularidade central | O editor de seções não valida nada além de “string”; quem valida parâmetros é o endpoint `/cms/v2/landing-sections/validate`, chamado pelo JS. Já o formulário de membro omite `sort_order`/`is_active` e reseta ambos a cada update. |
| Como se integra | Layout `v2-cms::layouts.app`; partials `v2-cms::partials.{breadcrumb,status-badge,pagination,activity-footer}`; componentes `v2-service-landingsectionseditor`, `v2-service-contentricheditor`, `v2-service-media`; diretiva `@v2can('SobreNos','write')`. |
| Se removido, quebra | Não há como editar seções, membros ou biênios; o portal continua servindo os dados existentes e o menu CMS fica apontando para rotas inexistentes. |
| Superfície pública | 3 views de página, 4 parciais de membro, 7 views de form LSE; funções JS globais `editBiennium`/`resetBienniumForm`; nenhum módulo JS exportado. |

## 1. Efeito

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

- **Seções** (`GET /cms/institucional/sobre-nos/`): ver o editor em duas colunas (lista de
  seções + preview em iframe), adicionar seção pelo dropdown (“Padrão” e
  “Personalizados”), editar parâmetros, reordenar, remover e salvar o conjunto; ao final,
  ver o histórico de alterações (`Views/cms/editor.blade.php:1-35`).
- **Membros** (`GET .../members`): ver foto (miniatura 40×40), nome, cargo, biênio (badge),
  ativo, ordem e ações; paginar de 15 em 15 mantendo a query string
  (`Views/cms/members/index.blade.php:32-98`).
- **Membros**: criar/editar com nome, cargo, biênio (select com “[Atual]”), LinkedIn,
  e-mail, descrição, biografia rica e foto da Galeria (`Views/cms/members/_form.blade.php:11-79`).
- **Membros**: excluir com confirmação nativa (`onsubmit="return confirm('Remover <nome>?')"`).
- **Biênios** (modal na listagem): listar em tabela, criar, editar via `editBiennium()`,
  cancelar edição via `resetBienniumForm()` e excluir com confirmação
  (`Views/cms/members/index.blade.php:111-243`).
- **Somente leitura**: todas as telas são visíveis, mas os botões de escrita estão dentro
  de `@v2can('SobreNos','write')` e somem; POSTs forjados são barrados no middleware.

### 1.2 O que muda no sistema

| Efeito | Onde é produzido | Fonte |
|---|---|---|
| Payload do editor é validado apenas como required · string e normalizado | `saveSections` | `Controllers/SobreNosCmsController.php:56-66` |
| `created_item` (alerta verde) é exibido **apenas** para membros e biênios criados | sessão + view | `BoardMemberController.php:81-85,183-187`; `Views/cms/members/index.blade.php:23-30` |
| Flash textual de sucesso aparece para update/delete e para seções | `flash()` | `SobreNosCmsController.php:84`; `BoardMemberController.php:125,151,221,246` |
| Histórico do payload é mostrado no rodapé do editor | `activity-footer` | `Views/cms/editor.blade.php:31-34` |
| Foto do membro é resolvida e o caminho é espelhado em `photo` | `resolvePhotoFromMedia` | `BoardMemberController.php:304-324` |
| Modal de biênios troca o `action`/`_method` do form sem nova requisição | JS `editBiennium` | `Views/cms/members/index.blade.php:214-243` |

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

| Entrada | Resultado |
|---|---|
| `GET .../` | `200` editor; `$sections` vazio → editor inicia com `[]` (`SobreNosCmsController.php:33-45`). |
| Salvar com JSON válido | `302` + “Seções da página "Sobre Nós" salvas com sucesso!”. |
| Salvar sem seções | `302` + mesmo flash; payload `[]`. |
| Criar membro válido | `302` + alerta `Membro “<nome>” criado com sucesso.` (template com `&ldquo;`/`&rdquo;` a partir de `created_item`). |
| Criar membro sem nome | `302` back + “O nome é obrigatório.”. |
| Criar membro com LinkedIn inválido | `302` back + “O LinkedIn deve ser uma URL válida.”. |
| Criar membro com e-mail inválido | `302` back + “O email deve ser um endereço válido.”. |
| Criar membro com biênio inválido | `302` back + “O biênio selecionado é inválido.”. |
| Editar membro | `302` + “Membro atualizado com sucesso!”. |
| Excluir membro | `302` + “Membro "<nome>" removido com sucesso!”. |
| Criar biênio | `302` + alerta `Biênio “<label>” criado com sucesso.`; se `is_current=1`, desmarca o anterior. |
| Editar biênio | `302` + “Biênio atualizado com sucesso!”. |
| Excluir biênio | `302` + “Biênio "<label>" removido com sucesso!”. |
| Criar biênio sem label/ano | `302` back + “O nome do biênio é obrigatório.” / “O ano de início é obrigatório.”. |
| Ação de escrita com perfil `read` | `302` back + “Operação negada. Seu perfil possui apenas permissão de leitura para o módulo: Sobre Nós” (`V2PermissionMiddleware.php:120-122`). |

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

- Não mostra contagem total de membros nem filtro por biênio na UI (o controller aceita
  `?biennium_id=` mas não há campo).
- Não oferece campo de `slug`, `sort_order`, `is_active`, `created_by` ou `metadata`.
- Não pré-visualiza o payload salvo fora do iframe do editor LSE.
- Não tem tela de configuração de SEO: title/description/keywords de `/sobre-nos` são
  literais no controller (`SobreNosPortalController.php:69-76`).
- Não permite restaurar membros/biênios arquivados (não há tela de lixeira).
- Não traduz mensagens de validação do Laravel: quando não há mensagem customizada, a
  chave crua (`validation.required`, etc.) é exibida.

## 2. Abordagem

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

```mermaid
flowchart LR
  A[editor.blade] -->|form POST landing_sections_json| B[saveSections]
  B --> C[ensureIds]
  C --> D[(LayoutBaseConfig)]
  E[members/index.blade] -->|GET/POST/DELETE| F[BoardMemberController]
  F --> G[(v2_board_members)]
  E -->|modal AJAX-free| H[bienniumForm]
  H --> I[(v2_bienniums)]
  F --> J[activity_log + Redis page:*]
  E --> K[_form.blade: CRE + media-input]
```

### 2.2 Mecânica passo a passo

1. `sections()` carrega a config e o histórico; devolve `editor` com
   `sections`, `history`, `config` (`SobreNosCmsController.php:31-46`).
2. `editor.blade` renderiza um `<form id="sobrenos-sections-form">` com CSRF e inclui o
   componente LSE com `name='landing_sections_json'`; o botão “Salvar” fica no card header
   e referencia o form por atributo (`Views/cms/editor.blade.php:9-27`).
3. O componente serializa o estado no hidden, carrega forms por AJAX
   (`/cms/v2/landing-sections/form?type=…`), valida por
   `/cms/v2/landing-sections/validate` e mantém preview em iframe (`landing-sections-editor.blade.php:19-24,114-142`).
4. `saveSections` decodifica, normaliza, faz upsert com restauração de soft delete, limpa
   cache, audita e redireciona (`SobreNosCmsController.php:54-87`).
5. `index` monta a query de membros (`with`, filtro opcional, `orderBy('sort_order')`,
   `paginate(15)`) e a lista de biênios (`orderBy('start_year','desc')`) (`BoardMemberController.php:36-48`).
6. `create`/`edit` carregam os biênios e delegam o formulário ao `_form`
   (`:55-60,96-103`).
7. `store`/`update` chamam `fillMember` (que salva), auditam, limpam cache e redirecionam
   (`:68-88,112-128`).
8. `destroy` captura o nome, faz soft delete, audita, limpa cache e redireciona (`:136-154`).
9. Biênios: os três métodos operam direto no model, com a mesma sequência
   cache→auditoria→flash (`:162-249`).

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

- **Salvar tudo de uma vez** `[C]`: o editor envia o array completo; não há endpoints por
  seção. Simplifica o backend, mas qualquer falha de JSON apaga o conjunto.
- **Botão de salvar fora do `<form>`** `[C]`: usa o atributo `form="…"` para manter o
  cabeçalho fixo; um reimplementador deve preservar o `id` (`Views/cms/editor.blade.php:10-19`).
- **Modal de biênios sem rota de formulário** `[C]`: editar e criar usam o mesmo form,
  trocando `action` e `_method` por JS; não há `GET` de edição de biênio
  (`Views/cms/members/index.blade.php:214-243`).
- **Mensagens via `created_item` na listagem** `[C]`: criações não usam `flash()`; usam um
  array em sessão renderizado como alerta, o que mantém o padrão de outros módulos
  (`BoardMemberController.php:81-85`).
- **`@v2can` apenas nos controles de escrita** `[C]`: a tela é acessível com `read`; a
  proteção efetiva é o middleware (`Views/cms/members/index.blade.php:10-17,74-88`).

### 2.4 Trechos de código pivô

```blade
{{-- V2/Modules/Institucional/SobreNos/Views/cms/editor.blade.php:9-26 --}}
@v2can('SobreNos', 'write')
<button type="submit" form="sobrenos-sections-form"
        class="btn btn-primary fw-bold px-4 d-inline-flex align-items-center gap-1">
    <i class="bi bi-save"></i> Salvar
</button>
@endv2can
...
<form id="sobrenos-sections-form" action="{{ route('v2.sobrenos.cms.sections.save') }}" method="POST">
    {{ csrf_field() }}
    @include('v2-service-landingsectionseditor::components.forms.landing-sections-editor', [
        'name'  => 'landing_sections_json',
        'value' => $sections,
        'label' => 'Seções da Página Sobre Nós',
    ])
</form>
```

> O `id` do form e o `name` do hidden são o contrato entre a tela e o controller. Trocar
> qualquer um quebra o save silenciosamente (o controller recebe `null` e devolve erro de
> validação).

```blade
{{-- Views/cms/members/_form.blade.php:50-69 --}}
<div class="col-md-12">
    <label class="form-label">Biografia</label>
    @include('v2-service-contentricheditor::components.forms.content-rich-editor', [
        'name' => 'bio',
        'value' => old('bio', $isEdit ? (is_string($member->bio) ? $member->bio : json_encode($member->bio)) : ''),
        'placeholder' => 'Escreva a biografia do membro...',
    ])
</div>
<div class="col-md-6">
    <label class="form-label">Foto (mídia)</label>
    @include('v2-service-media::components.media-input', [
        'name' => 'photo_id',
        'label' => 'Foto',
        'accept' => 'image/*',
        'currentId' => old('photo_id', $isEdit ? $member->photo_id : ''),
        'currentUrl' => $isEdit && $member->getPhotoFullUrl() ? $member->getPhotoFullUrl() : '',
        'currentAlt' => $isEdit ? $member->name : '',
        'ownerModule' => 'sobrenos',
        'help' => 'Formatos: JPG, PNG, WebP.',
    ])
</div>
```

> O `bio` é enviado como string JSON pelo CRE; o model com cast `array` acaba gravando
> essa string como valor JSON (dupla codificação). O `media-input` grava o **id** da
> mídia em `photo_id` (hidden) e mostra a prévia por `currentUrl`.

```blade
{{-- Views/cms/members/index.blade.php:214-228 --}}
<script>
    function editBiennium(id, label, startYear, isCurrent) {
        document.getElementById('biennium_id').value = id;
        document.getElementById('biennium_label').value = label;
        document.getElementById('biennium_start_year').value = startYear;
        document.getElementById('biennium_is_current').checked = isCurrent;

        var form = document.getElementById('bienniumForm');
        form.action = '{{ url("cms/institucional/sobre-nos/bienniums/update") }}/' + id;
        document.getElementById('biennium_method').value = 'PUT';
        document.getElementById('bienniumFormLabel').textContent = 'Editar Biênio';
        document.getElementById('bienniumSubmitBtn').innerHTML = '<i class="bi bi-check-lg"></i> Atualizar';
        document.getElementById('bienniumCancelBtn').classList.remove('d-none');
    }
</script>
```

> `editBiennium` recebe o label já interpolado em uma string JS com aspas simples
> (`onclick="editBiennium('…','{{ $b->label }}',…)"`); um label com apóstrofo quebra o
> handler. O `action` é montado manualmente com `url()` — não há rota nomeada para update.

### 2.5 Algoritmos, consultas e custos

- 1 query de config + 1 de histórico por abertura do editor.
- 1 query paginada + count + eager loads por página da listagem; 1 query de biênios.
- Cada mutação: 1–2 queries de escrita + 1 `KEYS` no Redis + 1 insert em `activity_log`.
- Cada save de membro: até 3 queries de busca (CMS upsert, público upsert/delete) em
  `try/catch` (`Models/BoardMember.php:218-257`).

## 3. Particularidades

### 3.1 O que só existe aqui

- **Duas formas de feedback de sucesso** convivendo: `created_item` (alerta na listagem,
  só para criação) e `flash()` (mensagem textual, para update/delete/seções).
- **Edição de biênio sem tela**: o modal é o único lugar que altera `is_current`.
- **Editor híbrido**: o formulário de seções é do módulo, mas todo o comportamento é do
  serviço LSE (forms por AJAX, validação, preview).

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

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| RN-SOBRENOS-C01 | O editor só valida que `landing_sections_json` é string | JSON inválido apaga o payload sem erro | `SobreNosCmsController.php:56-63` |
| RN-SOBRENOS-C02 | O form de membro não possui `sort_order`/`is_active` | Update reseta para `0`/`true` | `BoardMemberController.php:287-288`; `_form.blade.php:17-70` |
| RN-SOBRENOS-C03 | O filtro `biennium_id` existe no controller mas não na view | Só é utilizável por URL manual | `BoardMemberController.php:40-42`; `index.blade.php:9-18` |
| RN-SOBRENOS-C04 | `created_item` é flash de sessão renderizado no topo do card | O alerta aparece uma única vez, no request seguinte ao redirect | `index.blade.php:23-30` |
| RN-SOBRENOS-C05 | Nome e cargo são `required` no HTML e no FormRequest (255) | Submit vazio é barrado duas vezes | `_form.blade.php:19-25`; `Requests/BoardMemberRequest.php:23-25` |
| RN-SOBRENOS-C06 | Biênio é required + exists, aceitando soft-deleted | É possível salvar membro em biênio arquivado | `Requests/BoardMemberRequest.php:23` |
| RN-SOBRENOS-C07 | `photo_id` aceita qualquer string ≤36 (sem `exists`) | Id inexistente grava `photo=null` sem erro | `:28`; `BoardMemberController.php:313-323` |
| RN-SOBRENOS-C08 | O modal reutiliza o form e alterna `POST`/`PUT` | Não existe `GET .../bienniums/editar` | `Routes/cms.php:28-33`; `index.blade.php:216-242` |
| RN-SOBRENOS-C09 | O toggle de biênio atual desmarca **todos** antes de gravar, sem transação | Corrida pode deixar dois correntes | `BoardMemberController.php:164-166,204-206` |
| RN-SOBRENOS-C10 | O editor carrega o histórico em `try/catch` | Falha do logger não impede a tela | `SobreNosCmsController.php:37-43` |
| RN-SOBRENOS-C11 | A tabela de biênios usa `aria-label="Membros"` | Leitor de tela anuncia o rótulo errado | `index.blade.php:125` |

### 3.3 Casos-limite e comportamentos surpreendentes

- **Sem biênio cadastrado**: o select de membro fica só com “Selecione...” e o submit
  falha com “O biênio é obrigatório.”; é preciso criar um biênio no modal primeiro.
- **Biênio excluído**: some do select (soft delete), mas membros antigos continuam na
  listagem mostrando “—”.
- **Membro excluído**: some da listagem; não há como desfazer pela UI.
- **Label com apóstrofo** (`D'Ávila`): quebra `editBiennium` no `onclick`.
- **Descrição longa**: `textarea` sem `maxlength`; o FormRequest não limita.
- **Upload de foto**: se a Galeria rejeitar (tipo/tamanho), o `media-input` permanece com
  o id anterior; o form pode ser enviado sem foto nova.
- **Duplo submit**: como não há botão desabilitado, dois POSTs podem criar dois membros
  (sem unique de nome/slug).
- **Payload muito grande**: vai inteiro no hidden; o limite é o `post_max_size` do PHP —
  excedido, o Laravel recebe POST vazio e `validate` falha.

### 3.4 Armadilhas de reimplementação

- **Não** troque o `name='landing_sections_json'` nem o `id` do form sem ajustar o botão.
- **Não** adicione validação de parâmetros no `saveSections` sem também aceitar payloads
  legados (há seeder e migração com formatos antigos).
- **Não** esqueça de restaurar `LayoutBaseConfig` soft-deleted; o `firstOrNew` sozinho
  criaria uma segunda linha com a mesma `key`.
- **Não** presuma que o `_form` envia `is_active`: se a reimplementação adicionar o campo,
  o comportamento muda (hoje todo update reativa o membro).
- **Não** remova o `onsubmit=confirm` sem reposição: o delete é imediato via form.

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

- O editor usa apenas `@v2can` para esconder botões; não há mensagem na tela para o perfil
  `read` explicando por que não pode salvar.
- O histórico do editor só aparece quando existe `activity_log`; criação de membro passa
  `model=null` e nunca mostra histórico (`create.blade.php:23`).
- A tabela de biênios repete `aria-label="Membros"`.
- O botão de editar membro é um link só com ícone, sem `aria-label` nem `title`
  (`index.blade.php:75-78`).
- O modal usa `role="dialog"`/`aria-modal` ausente (apenas `aria-hidden` via Bootstrap).

## 4. Integrabilidade

### 4.1 Superfície pública

| Símbolo | Tipo | Contrato |
|---|---|---|
| `v2-sobrenos::cms.editor` | view | Espera `$sections` (array), `$history` (Collection), `$config` (LayoutBaseConfig ou null) |
| `v2-sobrenos::cms.members.index` | view | Espera `$members` (LengthAwarePaginator), `$bienniums` (Collection) |
| `v2-sobrenos::cms.members.create`/`edit` | views | Esperam `$bienniums` (+ `$member` no edit) |
| `v2-sobrenos::cms.members._form` | partial | `$member` (null em create), `$bienniums` |
| `editBiennium(id,label,startYear,isCurrent)` | função JS global | Requer `#biennium_id`, `#biennium_label`, `#biennium_start_year`, `#biennium_is_current`, `#bienniumForm`, `#biennium_method`, `#bienniumFormLabel`, `#bienniumSubmitBtn`, `#bienniumCancelBtn` |
| `resetBienniumForm()` | função JS global | Idem |
| `#bienniumModal` | modal Bootstrap | Disparado por `data-bs-toggle="modal"` |

### 4.2 Contrato de UI/JS

- **Slots do layout CMS**: `page-breadcrumbs`, `page-actions`, `content`, `modals`
  (`@push('modals')`), `scripts` (`@push('scripts')`).
- **Eventos**: apenas submits nativos e `onsubmit` de confirmação; nenhum evento
  customizado ou `addEventListener` no módulo.
- **DOM ids**: `sobrenos-sections-form`, `bienniumModal`, `bienniumForm`, `biennium_id`,
  `biennium_method`, `biennium_label`, `biennium_start_year`, `biennium_is_current`,
  `bienniumFormLabel`, `bienniumSubmitBtn`, `bienniumCancelBtn`.
- **Sem JS**: o editor LSE não funciona sem JS; o modal de biênios abre via Bootstrap (JS)
  e, sem ele, o form de criação fica inacessível (está dentro do modal).
- **Acessibilidade**: labels sem `for` (associação implícita só quando envolvem o input,
  o que não é o caso); tabelas com `aria-label`; botões de exclusão com `aria-label` ou
  `title`; ícones decorativos sem `aria-hidden` em vários pontos.

### 4.3 Ordem e ciclo de vida

1. Middleware `web` → `module.active:SobreNos` → `CmsAuthenticate` (RBAC) antes de
   qualquer action (`Routes/cms.php:3-6`; `V2/Core/Middleware/CmsAuthenticate.php:44`).
2. `@v2can` avalia `PermissionService` em tempo de render (diretiva registrada em
   `V2/Core/Providers/V2ServiceProvider.php:30-35`).
3. JS inline do editor é empilhado em `@push('scripts')` e inicializa no `DOMContentLoaded`
   do componente LSE; o modal é empilhado em `@push('modals')`.

### 4.4 Receitas de uso

```blade
{{-- Reutilizar o formulário de membro em outra tela do CMS --}}
@include('v2-sobrenos::cms.members._form', ['member' => $member, 'bienniums' => $bienniums])
{{-- create: omitir $member; o partial detecta $isEdit=false e usa a rota de store --}}
```

```blade
{{-- Esconder qualquer ação de escrita no módulo --}}
@v2can('SobreNos', 'write')
<a href="{{ route('v2.sobrenos.cms.members.create') }}" class="btn btn-primary btn-sm">Novo Membro</a>
@endv2can
```

```http
POST /cms/institucional/sobre-nos/members/novo
Content-Type: application/x-www-form-urlencoded

name=Maria+Silva&position=Presidente&biennium_id=<uuid>&photo_id=12&linkedin_url=https%3A%2F%2F...&email=maria%40exemplo.com&bio=%7B...%7D
=> 302 Location: /cms/institucional/sobre-nos/members
```

### 4.5 Anti-uso

- **Adicionar campos ao `_form` sem atualizar o controller** — `fillMember` faz atribuição
  explícita; campos novos são ignorados (não usa `$request->all()`).
- **Criar uma rota `GET` de edição de biênio esperando que o modal a use** — o modal monta
  a URL de update por `url()` e envia `PUT`.
- **Consumir `created_item` em outra tela** — o alerta só é renderizado na listagem de
  membros; outras telas ignoram a chave.

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

- `paginate(15)` fixo, sem opção de tamanho.
- Cada abertura do editor carrega o histórico completo (`getHistory(...)->get()`).
- Limpeza de cache por `KEYS` em cada mutação (O(N) no Redis).
- Upload de mídia: limite do endpoint LSE (10 MB) e da Galeria; o módulo não valida tipo.

## 5. Rotas e contratos HTTP

| Tipo | Método | URI | Nome | Controller@ação | Permissão | Efeito |
|---|---|---|---|---|---|---|
| grupo | — | `cms/institucional/sobre-nos` | — | — | `SobreNos` | `web` + `module.active:SobreNos` + `CmsAuthenticate` (`Routes/cms.php:3-6`) |
| endpoint | GET | `/cms/institucional/sobre-nos/` | `v2.sobrenos.cms.sections` | `SobreNosCmsController@sections` | read | Editor + histórico (`:8-9`; `SobreNosCmsController.php:31-46`) |
| endpoint | POST | `/cms/institucional/sobre-nos/salvar` | `v2.sobrenos.cms.sections.save` | `saveSections` | write | 302 + flash (`:10-11`; `:54-87`) |
| endpoint | GET | `/cms/institucional/sobre-nos/members` | `v2.sobrenos.cms.members` | `BoardMemberController@index` | read | Lista + modal (`:14-15`; `:36-48`) |
| endpoint | GET | `/cms/institucional/sobre-nos/members/novo` | `v2.sobrenos.cms.members.create` | `create` | read | Form (`:16-17`; `:55-60`) |
| endpoint | POST | `/cms/institucional/sobre-nos/members/novo` | `v2.sobrenos.cms.members.store` | `store` | write | 302 (`:18-19`; `:68-88`) |
| endpoint | GET | `/cms/institucional/sobre-nos/members/editar/{id}` | `v2.sobrenos.cms.members.edit` | `edit` | read | Form preenchido (`:20-21`; `:96-103`) |
| endpoint | PUT/POST | `/cms/institucional/sobre-nos/members/editar/{id}` | `v2.sobrenos.cms.members.update` | `update` | write | 302 (`:22-23`; `:112-128`) |
| endpoint | DELETE | `/cms/institucional/sobre-nos/members/{id}` | `v2.sobrenos.cms.members.destroy` | `destroy` | write | Soft delete (`:24-25`; `:136-154`) |
| endpoint | POST | `/cms/institucional/sobre-nos/bienniums/store` | `v2.sobrenos.cms.bienniums.store` | `storeBiennium` | write | Cria (`:28-29`; `:162-190`) |
| endpoint | PUT/POST | `/cms/institucional/sobre-nos/bienniums/update/{id}` | `v2.sobrenos.cms.bienniums.update` | `updateBiennium` | write | Atualiza (`:30-31`; `:199-224`) |
| endpoint | DELETE | `/cms/institucional/sobre-nos/bienniums/{id}` | `v2.sobrenos.cms.bienniums.destroy` | `destroyBiennium` | write | Soft delete (`:32-33`; `:232-249`) |

## 6. Dados e modelo de domínio

Colunas, casts, hooks e derivações em [`dados.md`](dados.md) §6. Telas escrevem:
`payload` (editor), `v2_board_members` (form) e `v2_bienniums` (modal).

## 7. Fluxos e estados

```mermaid
sequenceDiagram
  participant O as Operador
  participant L as Listagem
  participant C as BoardMemberController
  participant DB as v2_board_members
  O->>L: Novo Membro
  O->>C: POST members/novo
  C->>C: BoardMemberRequest
  C->>DB: insert (id, slug, photo, created_by)
  C->>C: cache + activity_log
  C-->>O: 302 + created_item
```

```mermaid
sequenceDiagram
  participant O as Operador
  participant M as Modal biênios
  participant C as BoardMemberController
  O->>M: Editar (editBiennium)
  O->>C: PUT bienniums/update/{id} (_method=PUT)
  C->>C: desmarca is_current anteriores (se marcado)
  C-->>O: 302 + flash "Biênio atualizado com sucesso!"
```

- `FL-SOBRENOS-C01` — criar membro.
- `FL-SOBRENOS-C02` — editar membro (com reset de `sort_order`/`is_active`).
- `FL-SOBRENOS-C03` — excluir membro.
- `FL-SOBRENOS-C04` — criar/editar biênio via modal.

## 8. UI CMS

### UI-SOBRENOS-01 — Editor de seções

- **Rota**: `GET /cms/institucional/sobre-nos/` · **Permissão**: `SobreNos:read`
  (escrita `write`).
- **Layout**: `v2-cms::layouts.app` com título “Editor de Seções — Sobre Nós”.
- **Objetivo**: editar o array de seções e salvar de uma vez.
- **Estados**: vazio → componente LSE inicia com `[]`; erro de validação → volta com
  mensagem crua; sucesso → flash; histórico vazio → rodapé omitido.
- **Árvore**: card → header (título “Seções” + botão “Salvar” condicional) → body →
  `form#sobrenos-sections-form` → componente LSE (sidebar + preview iframe).
- **Campos**: apenas o hidden `landing_sections_json` (o restante é gerado pelo LSE).
- **Mensagens**: “Seções da página "Sobre Nós" salvas com sucesso!”.
- **A11y**: botão com ícone + texto; iframe com `title="Visualização em tempo real"`;
  foco do editor gerenciado pelo componente; sem `aria-live` para o flash.

### UI-SOBRENOS-02 — Listagem de membros

- **Rota**: `GET .../members` · **Permissão**: `read`.
- **Colunas**: Foto (40×40, `alt=name` ou ícone placeholder), Nome, Cargo, Biênio (badge
  `bg-info` ou “—”), Ativo (partial `status-badge`), Ordem, Ações (editar/excluir).
- **Ordenação**: `sort_order` ASC (sem critério secundário); **sem filtros/busca** na UI.
- **Escopo**: todos os membros (de qualquer biênio, ativos ou não) — o filtro `?biennium_id=`
  existe no controller mas não é exposto.
- **Paginação**: 15 por página, `appends(request()->query())`, partial
  `v2-cms::partials.pagination`.
- **Bulk actions**: nenhuma.
- **Vazio**: ícone `bi-people`, “Nenhum membro cadastrado”, “Clique em "Novo Membro" para
  adicionar o primeiro.”.
- **Ações do topo**: “Configurar Biênios” (abre modal) e “Novo Membro”.
- **Confirmação**: `Remover <nome>?` (nativo).

### UI-SOBRENOS-03 — Formulário de membro (create/edit)

- **Rotas**: `GET .../members/novo` e `GET .../members/editar/{id}` · **Permissão**: `read`
  (submit exige `write`).
- **Campos**:

| Campo | Tipo | Obrigatório | Default | Validação server | Ajuda/observação |
|---|---|---|---|---|---|
| `name` | text (255) | sim | — | required · string · max:255 | “O nome é obrigatório.” |
| `position` | text (255) | sim | — | required · string · max:255 | “O cargo é obrigatório.” |
| `biennium_id` | select | sim | — | required · string · exists:v2_bienniums,id | opção “Label (início–fim)” + `[Atual]` |
| `linkedin_url` | url (255) | não | — | nullable · url · max:255 | “O LinkedIn deve ser uma URL válida.” |
| `email` | email (255) | não | — | nullable · email · max:255 | “O email deve ser um endereço válido.” |
| `description` | textarea (3 linhas) | não | — | nullable · string | card/resumo/SEO |
| `bio` | CRE | não | — | nullable · string | JSON do editor |
| `photo_id` | media-input | não | — | nullable · string · max:36 | “Formatos: JPG, PNG, WebP.”, `ownerModule=sobrenos` |

- **Ausentes**: `slug`, `sort_order`, `is_active`, `created_by`.
- **Botões**: “Atualizar” (edit) / “Salvar” (create) e “Cancelar” (volta ao índice).
- **Título**: create “Novo Membro”; edit “Editar Membro — <nome>”.
- **Breadcrumbs**: “Membros da Diretoria” → “Novo Membro”/“<nome>”.

### UI-SOBRENOS-04 — Modal de biênios

- **Gatilho**: botão “Configurar Biênios” (`data-bs-target="#bienniumModal"`).
- **Tabela**: Label, Ano Início, Ano Fim, Atual (badge), Ações (editar/excluir).
- **Form**: hidden `biennium_id` e `_method`; `label` (text, required, placeholder
  “Ex: Biênio 2026-2027”), `start_year` (number, min 1900, max 2100, required),
  `is_current` (checkbox `value=1`), botões “Salvar”/“Atualizar” e “Cancelar” (oculto até
  editar).
- **Vazio**: “Nenhum biênio cadastrado ainda.”.
- **Mensagens**: `Biênio “<label>” criado com sucesso.` (via `created_item`) e “Biênio atualizado com sucesso!”.

## 9. UI Portal

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

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

- Chave de módulo `SobreNos`; `read`/`write` em `v2_roles.permissions_v2`.
- `@v2can('SobreNos','write')` controla botões; `CmsAuthenticate`→`V2PermissionMiddleware`
  controla métodos HTTP.
- Flag `module.active:SobreNos` bloqueia o grupo CMS inteiro quando desativada.
- Não há config própria do módulo.

## 11. Observabilidade, erros e limites

- `activity_log`: seções (`updated`, com `old`/`new`), membros (`created`/`updated`/`deleted`
  com `name`), biênios (`created`/`updated`/`deleted` com `label`).
- Logs: falha de cache (warning), falha de foto (error), falha de busca (error).
- HTTP: `302` em sucesso/erro de validação; `404` id inexistente; `403` AJAX sem permissão.
- Limites: 15 itens/página; textos 255; `start_year` 1900–2100; sem limite de tamanho para
  `description`/`bio`.

## 12. Seeds e dados iniciais

O seeder não cria telas nem permissões; cria o payload e as mídias usadas pelo editor
(ver [`dados.md`](dados.md) §12). A permissão `SobreNos` é atribuída por perfil em
Gestão de Permissões, não semeada.

## 13. Lacunas

- `LG-SOBRENOS-C01` — Não foi possível confirmar se o botão “Salvar” do editor fica
  desabilitado durante o POST no JS do LSE; não há indicação no markup.
- `LG-SOBRENOS-C02` — Não há dados de uso para saber se operadores criam biênios sem
  marcar `is_current` (o que deixa a seção team vazia).
- `LG-SOBRENOS-C03` — O comportamento de erros de validação exibidos como chaves cruas
  depende de `resources/lang`; o repositório não possui arquivos de tradução.
- `LG-SOBRENOS-C04` — Não há tela nem comando para corrigir `slug`/`photo` de registros
  antigos; a extensão do problema é desconhecida.

## 14. Checklist de paridade

- [ ] `PC-SOBRENOS-C01` — Editor carrega com `$sections` do payload e histórico (`SobreNosCmsController.php:31-46`).
- [ ] `PC-SOBRENOS-C02` — Botão Salvar some para perfil `read` (`editor.blade.php:9-14`).
- [ ] `PC-SOBRENOS-C03` — Salvar sem `landing_sections_json` devolve erro e mantém o payload.
- [ ] `PC-SOBRENOS-C04` — Listagem mostra foto/nome/cargo/biênio/ativo/ordem e pagina 15.
- [ ] `PC-SOBRENOS-C05` — Excluir membro usa `confirm('Remover <nome>?')` e faz soft delete.
- [ ] `PC-SOBRENOS-C06` — Formulário não possui `sort_order`/`is_active` e update grava `0`/`true`.
- [ ] `PC-SOBRENOS-C07` — Bio usa CRE com `name=bio`; foto usa `media-input` com `photo_id` e `ownerModule=sobrenos`.
- [ ] `PC-SOBRENOS-C08` — Modal cria biênio com label/ano e checkbox “Atual”.
- [ ] `PC-SOBRENOS-C09` — `editBiennium` troca action para `bienniums/update/{id}` e `_method=PUT`.
- [ ] `PC-SOBRENOS-C10` — `created_item` mostra “<Tipo> "<título>" criado com sucesso.” na listagem.
- [ ] `PC-SOBRENOS-C11` — Mensagens de validação literais dos FormRequests aparecem.
- [ ] `PC-SOBRENOS-C12` — Nenhuma ação de escrita é oferecida fora de `@v2can('SobreNos','write')`.

## 15. Evidências consultadas

| Arquivo | Faixas |
|---|---|
| `Views/cms/editor.blade.php` | 1-35 |
| `Views/cms/members/index.blade.php` | 1-244 |
| `Views/cms/members/_form.blade.php` | 1-79 |
| `Views/cms/members/create.blade.php` | 1-26 |
| `Views/cms/members/edit.blade.php` | 1-26 |
| `Controllers/SobreNosCmsController.php` | 31-106 |
| `Controllers/BoardMemberController.php` | 36-324 |
| `Requests/BoardMemberRequest.php` | 20-49 |
| `Requests/BienniumRequest.php` | 20-39 |
| `Routes/cms.php` | 3-34 |
| `V2/Core/Middleware/{CmsAuthenticate,V2PermissionMiddleware}.php` | conforme citado |
