---
id: MOD-PUBLICACOES-AGENDA-CMS
tipo: modulo
onda: 4
status: concluido
unidades: [MOD-PUBLICACOES-AGENDA]
fontes:
  - V2/Modules/Publicacoes/Agenda/
depende_de: [UI-CMS, SVC-MEDIA, SVC-GALLERY, CORE-AUTH]
consumidores: [UI-CMS]
criticidade: media
dono: Agenda
---

# Agenda — telas do CMS

> Listagem, criação e edição de eventos: campos, validação, mensagens exatas, JS
> (Flatpickr, programação dinâmica, componente de mídia), estados e acessibilidade.
> Contrato HTTP em [`rotas.md`](rotas.md); regras de negócio em [`spec.md`](spec.md).

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Permite a um operador autorizado criar, editar, publicar e arquivar eventos em três telas. |
| Entrada | `GET /cms/agenda`, `GET /cms/agenda/novo`, `GET /cms/agenda/editar/{id}`; formulário com 12 controles. |
| Saída | HTML com tabela/forms; `302` + flash/modal; erros de validação em `$errors` (não renderizados). |
| Efeitos colaterais | Upload/seleção de mídia via componente global; Flatpickr e jQuery no `@push('scripts')`; modal Bootstrap de sucesso. |
| Particularidade central | O formulário não usa editor rico, não exibe erros de validação e não repopula as linhas de programação após erro. |
| Como se integra | Namespace `v2-agenda::cms.*`; layout `v2-cms::layouts.app`; componentes `v2-cms::partials.*` e `v2-service-media::components.media-input`. |
| Se removido, quebra | Nenhum outro módulo consome estas views; o menu do CMS perde o item "Agenda" e seus 2 submódulos. |
| Superfície pública | Views `cms.index`, `cms.create`, `cms.edit`, `cms._form`; IDs/classes usados pelo JS inline e por `media-input.js`. |

## 1. Efeito

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

- Ver a lista paginada com imagem, título, data(s), local e status.
- Filtrar por status (Todos/Publicados/Rascunhos) e buscar por título/descrição.
- Criar/editar com imagem da galeria, datas por picker, programação dinâmica e publicação.
- Arquivar com confirmação nativa do navegador.

### 1.2 O que muda no sistema

| Efeito | Onde é produzido | Fonte |
|---|---|---|
| Modal `#creationSuccessModal` exibido uma vez após criar | `created_item` + partial do layout | `EventosCmsController.php:150-154`; `V2/layouts/cms/partials/creation-success-modal.blade.php:1-29` |
| Flash de sucesso/erro renderizado pelo layout | `@include('flash::message')` | `V2/layouts/cms/layouts/app.blade.php:90` |
| Linhas de programação adicionadas/removidas no DOM | JS inline | `Views/cms/_form.blade.php:190-230` |
| Upload/seleção de imagem com preview e crop | JS global `assets/js/media-input.js` | `V2/Services/Media/Views/components/media-input.blade.php:104-113` |
| Datas em Flatpickr pt-BR | `cms-layout.js` | `public/assets/js/cms-layout.js:65-67` |

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

| Ação | Resultado |
|---|---|
| Abrir listagem | Tabela com `N registro(s) encontrados`; cada linha tem link no título e botões Editar/Arquivar. |
| Buscar `lgpd` | Filtra `title ILIKE %lgpd%` **ou** `description ILIKE %lgpd%`; paginação preserva `search`. |
| Filtrar Publicados | `published_at IS NOT NULL` (inclui data futura). |
| Filtrar Rascunhos | `published_at IS NULL`. |
| Enviar sem título ou sem início | Volta ao formulário com `$errors`; **nenhuma mensagem é exibida**; valores de texto são repopulados por `old()`, programação não. |
| Enviar com término < início | Mesmo comportamento; mensagem existe apenas no objeto de erros. |
| Salvar criação | Redirect ao index + modal; não há flash textual além do modal. |
| Salvar edição | Redirect à própria edição + `Evento atualizado com sucesso!`. |
| Arquivar | `confirm()` nativo; redirect ao index + `Evento "X" arquivado com sucesso!`. |

### 1.4 O que estas telas NÃO fazem

- Não exibem erros de validação (nem `@if($errors)`, nem `@include('errors')` no layout).
- Não têm editor rico, upload de anexos, categorias, tags, destaque ou pré-visualização do portal.
- Não têm bulk actions, seleção múltipla, ordenação clicável ou `per_page`.
- Não têm link "ver no portal".
- Não restauram arquivados.
- Não atualizam `alt_text` da imagem (`image_id_alt` ignorado pelo controller).

## 2. Abordagem

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

Três views finas + um partial compartilhado. A listagem usa os componentes padrão do CMS (`filter-card`, `action-create`, `pagination`) para manter consistência visual; o formulário é um único card Bootstrap com grid e um bloco de JS inline para as linhas de programação. Toda a lógica de exibição deriva do model (`$isEditing`, `$evento->published_at`, `$evento->is_active`).

```mermaid
flowchart LR
  I[cms.index] -->|filter-card| F[GET ?search&filter&page]
  I -->|action-create| N[cms.create]
  I -->|Editar| E[cms.edit]
  N -->|POST| S[store]
  E -->|POST + _method=PUT| U[update]
  N --> P[_form]
  E --> P
  P --> M[v2-service-media::components.media-input]
  P --> J[JS inline: schedule rows]
  I --> D[DELETE + confirm]
```

### 2.2 Mecânica passo a passo

1. `index` consulta `Evento` com filtro/busca/ordem e entrega `$eventos` ao paginator (`EventosCmsController.php:39-62`).
2. A view lê `request('filter')`/`request('search')` de novo para marcar os controles (`Views/cms/index.blade.php:1-6`).
3. `filter-card` monta o form GET com `search` e o slot do select `filter` (`Views/cms/index.blade.php:30-44`).
4. Cada linha usa `getImageUrl()` (lazy load da relação), formata datas com `d/m/Y H:i`, mostra local ou `-` e badge de status (`:60-123`).
5. Paginação usa `appends(request()->except('page'))` para preservar filtros (`:129-133`).
6. `create`/`edit` renderizam o `_form` dentro de `<form>` com CSRF (e `_method=PUT` no edit) (`Views/cms/create.blade.php:19-23`; `Views/cms/edit.blade.php:19-24`).
7. `_form` calcula `$isEditing` por `$evento->exists`, `$scheduleItems` do model e `$topicsList` vindo do controller (`Views/cms/_form.blade.php:1-7`).
8. O componente de mídia recebe `currentId`, `currentUrl` e `currentAlt` e cuida do resto (`:24-35`).
9. O JS inline adiciona linhas com índice incremental e impede remover a última (`:190-230`).

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

- **Partial único** `[C]`: `_form` serve create/edit; evita divergência de campos.
- **`old()` em todos os campos de texto** `[C]`: preserva entrada após erro de validação.
- **Programação sem `old()`** `[C]`: usa apenas o model; após erro, todas as linhas digitadas são perdidas (`:100-127`).
- **Select de status dentro do painel colapsável** `[C]`: o `filter-card` só expande quando há `search`/datas ativos; um filtro de status sozinho fica oculto após o reload (`V2/layouts/cms/partials/filter-card.blade.php:44-50,96-122`).
- **Componentes padrão do CMS** `[C]`: breadcrumb, ação de criação, paginação e filter-card são reutilizados, garantindo consistência e a11y.
- **Sem preview** `[C]`: o operador só vê o resultado no portal.

### 2.4 Trechos de código pivô

```blade
{{-- V2/Modules/Publicacoes/Agenda/Views/cms/_form.blade.php:24-35 — integração com o componente de mídia --}}
<div class="col-md-4">
    @include('v2-service-media::components.media-input', [
        'name' => 'image_id',
        'label' => 'Imagem do Evento',
        'accept' => 'image/*',
        'required' => false,
        'ownerModule' => 'eventos',
        'currentId' => $evento->image_id ?? '',
        'currentUrl' => $isEditing ? $evento->getImageUrl() : '',
        'currentAlt' => $evento->title ?? '',
    ])
</div>
```

`name='image_id'` vira um `input[type=hidden]` preenchido pelo JS; `image_id_alt` também é enviado, mas não é persistido `[C]`.

```blade
{{-- V2/Modules/Publicacoes/Agenda/Views/cms/_form.blade.php:97-127 — linhas de programação vindas do model --}}
<div class="col-md-6">
    <label class="form-label fw-semibold">Programação</label>
    <div id="schedule-container">
        @if(count($scheduleItems) > 0)
            @foreach($scheduleItems as $index => $item)
                <div class="schedule-row row g-2 mb-2 align-items-center">
                    <div class="col-md-3">
                        <input type="text" name="schedule[{{ $index }}][time]"
                               class="form-control form-control-sm"
                               placeholder="Horário"
                               value="{{ $item['time'] ?? '' }}">
                    </div>
                    <div class="col-md-4">
                        <input type="text" name="schedule[{{ $index }}][activity]"
                               class="form-control form-control-sm"
                               placeholder="Atividade"
                               value="{{ $item['activity'] ?? '' }}">
                    </div>
                    <div class="col-md-4">
                        <input type="text" name="schedule[{{ $index }}][speaker]"
                               class="form-control form-control-sm"
                               placeholder="Palestrante"
                               value="{{ $item['speaker'] ?? '' }}">
                    </div>
                    <div class="col-md-1">
                        <button type="button" class="btn btn-outline-danger btn-sm remove-schedule-row" title="Remover">
                            <i class="bi bi-trash"></i>
                        </button>
                    </div>
                </div>
            @endforeach
        @else
```

Não há `old('schedule')`; só o model repopula `[C]`.

```blade
{{-- V2/Modules/Publicacoes/Agenda/Views/cms/_form.blade.php:159-176 — dois eixos independentes --}}
<div class="col-md-4">
    <div class="form-check form-switch mt-2">
        <input type="hidden" name="is_active" value="0">
        <input type="checkbox" id="is_active" name="is_active"
               class="form-check-input" value="1"
               {{ old('is_active', $isEditing ? $evento->is_active : true) ? 'checked' : '' }}>
        <label class="form-check-label fw-semibold" for="is_active">Ativo</label>
    </div>
</div>

<div class="col-md-4">
    <label for="published_at" class="form-label fw-semibold">Data de Publicação</label>
    <input type="text" id="published_at" name="published_at"
           class="form-control"
           data-picker="date"
           value="{{ old('published_at', $isEditing && $evento->published_at ? $evento->published_at->format('Y-m-d') : '') }}">
    <div class="form-text">Deixe em branco para salvar como rascunho.</div>
</div>
```

O hidden `0` garante que desmarcar envie `0`; marcar envia `1` (o último valor vence no PHP). O default no create é `checked` `[C]`.

```js
// V2/Modules/Publicacoes/Agenda/Views/cms/_form.blade.php:192-219 — índice e clonagem de linha
$(document).ready(function() {
    let scheduleIndex = {{ count($scheduleItems) }};

    $('#add-schedule-row').on('click', function() {
        const container = $('#schedule-container');
        const row = `
            <div class="schedule-row row g-2 mb-2 align-items-center">
                <div class="col-md-3">
                    <input type="text" name="schedule[${scheduleIndex}][time]"
                           class="form-control form-control-sm" placeholder="Horário">
                </div>
                <div class="col-md-4">
                    <input type="text" name="schedule[${scheduleIndex}][activity]"
                           class="form-control form-control-sm" placeholder="Atividade">
                </div>
                <div class="col-md-4">
                    <input type="text" name="schedule[${scheduleIndex}][speaker]"
                           class="form-control form-control-sm" placeholder="Palestrante">
                </div>
                <div class="col-md-1">
                    <button type="button" class="btn btn-outline-danger btn-sm remove-schedule-row" title="Remover">
                        <i class="bi bi-trash"></i>
                    </button>
                </div>
            </div>
        `;
        container.append(row);
        scheduleIndex++;
    });
```

O contador inicia no tamanho do array para não colidir com índices existentes; remover reindexa? Não — os índices enviados mantêm buracos, e o controller aplica `array_values` `[C]`.

```blade
{{-- V2/Modules/Publicacoes/Agenda/Views/cms/index.blade.php:111-120 — confirmação nativa e method spoofing --}}
<form action="{{ route('v2.agenda.cms.destroy', ['id' => $evento->id]) }}"
      method="POST" class="d-inline"
      onsubmit="return confirm('Tem certeza que deseja arquivar este evento?')">
    {{ csrf_field() }}
    {{ method_field('DELETE') }}
    <button type="submit" class="btn btn-sm btn-outline-secondary"
            title="Arquivar">
        <i class="bi bi-archive" aria-hidden="true"></i>
    </button>
</form>
```

Sem modal próprio: usa o `confirm()` do navegador, que não é estilizável nem acessível de forma consistente `[C]`.

```js
// public/assets/js/cms-layout.js:65-67 — formatos dos pickers globais
flatpickr('[data-picker="date"]', { locale: 'pt', dateFormat: 'Y-m-d', allowInput: true, altInput: true, altFormat: 'd/m/Y' });
flatpickr('[data-picker="datetime"]', { locale: 'pt', enableTime: true, dateFormat: 'Y-m-d H:i:s', altInput: true, altFormat: 'd/m/Y H:i', time_24hr: true, allowInput: true, minuteIncrement: 5 });
flatpickr('[data-picker="time"]', { locale: 'pt', enableTime: true, noCalendar: true, dateFormat: 'H:i', time_24hr: true, allowInput: true, minuteIncrement: 5 });
```

O `datetime` espera segundos (`Y-m-d H:i:s`), mas o `_form` preenche `Y-m-d H:i` (sem segundos) — risco de o picker não reconhecer o valor inicial `[I]` (`_form.blade.php:44`).

### 2.5 Algoritmos, consultas e custos

- **Listagem**: 1 count + 1 select (15) + até 15 selects de mídia (relação lazy, não há `with('image')`) `[C]`.
- **Busca**: `ilike` em duas colunas, sem índice `[C]`.
- **Edição**: 1 select do evento; tópicos decodificados em memória `[C]`.
- **JS**: sem AJAX próprio; o componente de mídia faz upload sob demanda.
- **Assets**: `media-input.js` é incluído uma única vez por request via `$GLOBALS['media_input_scripts_rendered']` `[C]`.

## 3. Particularidades

### 3.1 O que só existe aqui

- Programação dinâmica em três campos por linha, sem componente reutilizável.
- Duplo controle de publicação na mesma tela (`Ativo` + `Data de Publicação`) sem explicação textual da diferença.
- Badge de status que ignora `is_active`.

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

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| UI-AGENDA-01 | `$isEditing = !is_null($evento) && $evento->exists` | botão muda de "Criar Evento" para "Atualizar Evento" | `_form.blade.php:1-4,183-186` |
| UI-AGENDA-02 | Sempre há ≥1 linha de programação no create | todo evento criado persiste `schedule[0]` vazio | `_form.blade.php:128-151`; `EventosCmsController.php:120-123` |
| UI-AGENDA-03 | Remover linha não é permitido quando resta uma | mínimo de 1 linha | `_form.blade.php:222-227` |
| UI-AGENDA-04 | Select de status fica no painel colapsável | filtro ativo pode ficar invisível após reload | `index.blade.php:36-43`; `filter-card.blade.php:44-50` |
| UI-AGENDA-05 | Erros de validação não são renderizados | usuário não sabe o que falhou além do `required` HTML | `_form.blade.php` (sem `$errors`); `V2/layouts/cms/` (sem partial) |
| UI-AGENDA-06 | Programação não usa `old()` | perde linhas digitadas em erro de validação | `_form.blade.php:100-127` |
| UI-AGENDA-07 | `published_at` é date-only | publicar hoje = 00:00 | `_form.blade.php:169-175` |
| UI-AGENDA-08 | `image_id_alt` é enviado e ignorado | `alt_text` da galeria não muda | `V2/Services/Media/Views/components/media-input.blade.php:46-47`; `EventosCmsController.php:113` |

### 3.3 Casos-limite e comportamentos surpreendentes

- **Erro de validação**: o form volta com `old()` nos textos, mas a programação é recarregada do model (vazia no create) e o usuário não vê mensagem alguma `[C]`.
- **Título duplicado**: cria com slug `-1`, sem aviso `[C]`.
- **Imagem selecionada e removida**: o hidden fica vazio; `image_id` é gravado `null` só no update (no store já nasce nulo) `[I]`.
- **Data de término anterior**: só é barrada no servidor (o input não tem `min`) `[C]`.
- **`datetime` sem segundos**: valor inicial pode não ser reconhecido pelo Flatpickr `[I]`.
- **Filtro `draft` + busca vazia**: o painel volta colapsado e o select não aparece, embora o filtro esteja aplicado `[C]`.
- **Paginação**: links preservam `filter` e `search`, mas não uma âncora/scroll `[C]`.
- **Evento arquivado**: some da lista (soft delete fora do escopo global) e não há indicação de onde encontrá-lo `[C]`.

### 3.4 Armadilhas de reimplementação

- Não usar `old('schedule.*')`: a estrutura atual não suporta; replicar exige mudar o backend para `array_values` com índices estáveis.
- Não mover o select de status para fora do colapso sem revisar o `filter-card` (o slot é a única área avançada).
- Não trocar `confirm()` por modal sem manter o `name`/`method` do form.
- Não usar `datetime-local` sem ajustar o formato esperado pelo controller (`Carbon::parse` aceita ambos, mas o picker global não).
- Não exibir erros apenas com `alert()`: a reimplementação deve mostrar `$errors` por campo (lacuna conhecida).
- Não assumir que o `alt` da imagem vem do formulário: ele vem da galeria.

### 3.5 Divergências e dívidas

- Breadcrumb da listagem usa o partial padrão (`index.blade.php:12-18`), enquanto create/edit usam spans manuais (`create.blade.php:11-16`; `edit.blade.php:11-16`) — markup e a11y diferentes.
- `create`/`edit` definem `$pageIcon` que o layout pode ignorar `[I]`.
- O formulário declara `required` em título/início, mas o servidor é a única validação efetiva de data de término.
- O componente de mídia aceita `image/*` embora a coluna `image_id` e a view não tratem PDF/áudio.

## 4. Integrabilidade

### 4.1 Superfície pública

| Símbolo | Tipo | Assinatura/variável | Precondição | Consumidores |
|---|---|---|---|---|
| `v2-agenda::cms.index` | view | `$eventos` (LengthAwarePaginator) | rota index | `EventosCmsController@index` |
| `v2-agenda::cms.create` | view | `$evento = null` | rota create | `@create` |
| `v2-agenda::cms.edit` | view | `$evento`, `$topicsList` | rota edit | `@edit` |
| `v2-agenda::cms._form` | partial | `$evento` (nullável), `$topicsList` (string) | — | create/edit |
| `v2-cms::layouts.app` | layout | `title` | — | as 3 telas |
| `v2-cms::partials.filter-card` | componente | `searchPlaceholder`, `searchValue`, `action`, `showDateRange`, slot | — | index |
| `v2-service-media::components.media-input` | componente | `name`, `label`, `accept`, `required`, `ownerModule`, `currentId`, `currentUrl`, `currentAlt` | serviço de mídia ativo | `_form` |

### 4.2 Contrato de UI/JS

- **IDs**: `#title`, `#event_date_start`, `#event_date_end`, `#location`, `#organizer`, `#target_audience`, `#description`, `#topics_list`, `#schedule-container`, `#add-schedule-row`, `#is_active`, `#published_at`; mídia: `#wrapper-image_id`, `#media-id-image_id`, `#media-input-image_id`.
- **Classes/atributos JS**: `.schedule-row`, `.remove-schedule-row`, `[data-picker="datetime"]`, `[data-picker="date"]`, `.js-media-input`, `.v2-media-dropzone`.
- **Eventos**: `click` em `#add-schedule-row` (append de linha), `click` delegado em `.remove-schedule-row` (remove se >1), `DOMContentLoaded` para o modal de criação.
- **Sem JS**: o form continua funcional para campos de texto; datas viram texto livre (o controller faz `Carbon::parse`); programação fica com as linhas renderizadas no servidor; mídia não pode ser escolhida.
- **Ordem**: jQuery/Flatpickr/Bootstrap vêm do layout; o `@push('scripts')` do `_form` roda depois.

### 4.3 Ordem e ciclo de vida

1. Layout CMS inclui Bootstrap, Flatpickr (via `cms-layout.js`) e `flash::message` antes do conteúdo.
2. `media-input` injeta `media-input.js` no `@push('scripts')` uma única vez.
3. O JS inline do `_form` depende de jQuery e de o container existir no DOM.
4. O modal de criação depende de `bootstrap.Modal` e do `created_item` na sessão (consumido no próximo request).

### 4.4 Receitas de uso

**Reutilizar o formulário em outra tela (ex.: duplicar evento):**

```blade
<form action="{{ route('v2.agenda.cms.store') }}" method="POST">
    {{ csrf_field() }}
    @include('v2-agenda::cms._form', ['evento' => null, 'topicsList' => ''])
</form>
```

**Renderizar a listagem com os mesmos filtros:**

```blade
@include('v2-agenda::cms.index')
```

(na prática, use a rota `v2.agenda.cms.index`; a view espera `$eventos`.)

**Adicionar uma coluna à tabela sem quebrar o layout:**

```blade
<th scope="col">Organizador</th>
...
<td><span class="small text-muted">{{ $evento->organizer ?: '-' }}</span></td>
```

**Abrir o modal de sucesso após um create fora do controller:**

```php
session()->flash('created_item', [
    'type' => 'Evento',
    'title' => $evento->title,
    'route' => route('v2.agenda.cms.index'),
]);
```

### 4.5 Anti-uso

- **Chamar `_form` sem `$evento`**: o partial tolera (`$evento = null`), mas `$isEditing` fica falso e `getImageUrl()` nunca é chamado — ok para create, errado para edit.
- **Contar com `$errors` na view**: o layout não renderiza; a reimplementação deve adicionar o bloco, senão erros de servidor continuam invisíveis.
- **Usar `schedule[]` sem índices**: o backend espera `schedule[i][time]`; índices nomeados/strings quebram `array_values` e a ordernação `[I]`.

### 4.6 Custos e limites

- Sem limite de linhas de programação no cliente; o payload cresce linearmente.
- Um `select` de mídia por tela; o JS carrega a galeria sob demanda.
- Sem upload direto no `_form` além do componente; timeouts do serviço de mídia se aplicam.
- A listagem faz até 15 queries extras de imagem (sem eager loading).
- Sem confirmação acessível além do `confirm()` nativo.

## 5. Rotas e contratos HTTP

| Tela | Rota | Método | Permissão | Saída |
|---|---|---|---|---|
| Listagem | `v2.agenda.cms.index` | GET | `Agenda:read` | `v2-agenda::cms.index` |
| Criação | `v2.agenda.cms.create` | GET | `Agenda:read` | `cms.create` + `_form` |
| Gravar | `v2.agenda.cms.store` | POST | `Agenda:write` | redirect index |
| Edição | `v2.agenda.cms.edit` | GET | `Agenda:read` | `cms.edit` + `_form` |
| Atualizar | `v2.agenda.cms.update` | PUT/POST | `Agenda:write` | redirect edit |
| Arquivar | `v2.agenda.cms.destroy` | DELETE | `Agenda:write` | redirect index |

Detalhes de middleware/cache/erros: [`rotas.md`](rotas.md).

## 6. Dados e modelo de domínio

A tela lê/escreve os campos de `v2_eventos` descritos em [`dados.md`](dados.md): `title`, `image_id`, `description`, `event_date_start`, `event_date_end`, `location`, `organizer`, `target_audience`, `topics_list` (texto multilinha ↔ JSON), `schedule` (array), `is_active`, `published_at`. `slug` é derivado no servidor e não aparece na UI.

## 7. Fluxos e estados

```mermaid
stateDiagram-v2
  [*] --> Listagem
  Listagem --> Criacao: Novo Evento
  Listagem --> Edicao: Editar
  Criacao --> Listagem: Criar Evento (modal)
  Criacao --> Criacao: erro de validacao (sem mensagem)
  Edicao --> Edicao: Atualizar Evento (flash)
  Listagem --> Listagem: Arquivar (confirm + flash)
```

## 8. UI CMS

### 8.1 Tela: listagem

| Item | Valor |
|---|---|
| Rota | `GET /cms/agenda` (`v2.agenda.cms.index`) |
| Permissão | `Agenda:read` (ou `write`) |
| Layout | `v2-cms::layouts.app`, breadcrumb Publicações › Agenda, ação "Novo Evento" |
| Objetivo | localizar e abrir eventos; arquivar |
| Estados | com dados; vazio sem filtro (`Nenhum evento encontrado` + `Comece criando o primeiro evento da agenda.` + botão "Criar Primeiro Evento"); vazio com filtro (`Tente ajustar os filtros ou limpar a busca.` + "Limpar Filtros"); erro de RBAC/flag = redirect, não estado da tela |
| Contador | `{{ $eventos->total() }} registro(s) encontrados` |
| Colunas | Imagem (50×50, `object-fit: cover`, lazy), Título (link para editar), Data (`d/m/Y H:i` + `até ...`), Local (`-` se vazio), Status (badge `Publicado`/`Rascunho`), Ações (Editar/Arquivar) |
| Filtros | `search` (título/descrição), `filter` (`published`/`draft`) |
| Ordenação | fixa `event_date_start DESC` |
| Paginação | 15, links com `appends` |
| Bulk | não existe |

### 8.2 Tela: criação/edição

| Item | Valor |
|---|---|
| Rotas | `GET /cms/agenda/novo`, `GET /cms/agenda/editar/{id}` |
| Permissão | `Agenda:read` para abrir; `write` para salvar |
| Layout | card único, rodapé com Voltar/Criar|Atualizar |
| Título da página | `Novo Evento` / `Editar: {título}` |
| Ícone | `bi-plus-circle` / `bi-pencil-square` |

**Campos**

| # | Nome | Controle | Obrigatório | Default | Validação cliente | Validação servidor | Ajuda/placeholder |
|---|---|---|---|---|---|---|---|
| 1 | `title` | text `maxlength=255` | sim | vazio/valor | `required` | `required|string|max:255` | `Digite o título do evento...` |
| 2 | `image_id` | componente de mídia (hidden + dropzone) | não | atual | nenhuma | `nullable|string|max:36` | `Formatos aprovados: image/*` |
| 3 | `event_date_start` | text + Flatpickr datetime | sim | vazio/`Y-m-d H:i` | `required` | `required|date` | — |
| 4 | `event_date_end` | text + Flatpickr datetime | não | vazio | — | `nullable|date|after_or_equal` | — |
| 5 | `location` | text `maxlength=255` | não | — | — | `nullable|string|max:255` | `Local do evento...` |
| 6 | `organizer` | text `maxlength=255` | não | — | — | `nullable|string|max:255` | `Nome do organizador...` |
| 7 | `target_audience` | text `maxlength=255` | não | — | — | `nullable|string|max:255` | `Ex: Profissionais de TI, Gestores...` |
| 8 | `description` | textarea 6 linhas | não | — | — | `nullable|string` | `Descrição do evento...` |
| 9 | `topics_list` | textarea 5 linhas | não | `topicsList` | — | `nullable|string` | `Cada linha será exibida como um item na lista de tópicos.` |
| 10 | `schedule[i][time|activity|speaker]` | 3 texts por linha | não | 1 linha vazia | — | `schedule` array; itens strings | `Adicione horários, atividades e palestrantes da programação.` |
| 11 | `is_active` | hidden `0` + checkbox `1` | não | `true` no create | — | `nullable|boolean` | label `Ativo` |
| 12 | `published_at` | text + Flatpickr date | não | vazio/`Y-m-d` | — | `nullable|date` | `Deixe em branco para salvar como rascunho.` |

**Mensagens exatas**

- Sucesso na edição: `Evento atualizado com sucesso!`
- Sucesso ao arquivar: `Evento "{título}" arquivado com sucesso!`
- Confirmação: `Tem certeza que deseja arquivar este evento?`
- Modal de criação: `Evento criado com sucesso!` + título + botão `Voltar para Listagem`
- Vazio (lista): `Nenhum evento encontrado`, `Tente ajustar os filtros ou limpar a busca.`, `Limpar Filtros`, `Comece criando o primeiro evento da agenda.`, `Criar Primeiro Evento`

### 8.3 JS da tela

| Arquivo | Função/trecho | Evento | Efeito |
|---|---|---|---|
| `_form.blade.php` inline | `$('#add-schedule-row').on('click')` | click | adiciona `.schedule-row` com índice incremental |
| `_form.blade.php` inline | `$(document).on('click', '.remove-schedule-row')` | click delegado | remove a linha se houver >1 |
| `public/assets/js/media-input.js` | componente global | drop/click/upload | preenche `#media-id-image_id` e preview |
| `public/assets/js/cms-layout.js:65-67` | Flatpickr | init | máscaras e calendário pt-BR |
| `V2/layouts/cms/partials/creation-success-modal.blade.php` | `new bootstrap.Modal(...).show()` | DOMContentLoaded | abre modal `#creationSuccessModal` |
| `filter-card` inline | toggle do colapso + reinit do Flatpickr | click/shown | expande filtros avançados |

### 8.4 Acessibilidade

- Todos os campos têm `<label for>`; obrigatórios têm `*` com `aria-hidden` + `<span class="visually-hidden">Obrigatório</span>` (via componente de mídia) e `required` nativo nos campos 1 e 3.
- Tabela com `aria-label="Eventos"` e `scope="col"` nos cabeçalhos (`index.blade.php:48-57`).
- Botões de ação usam `title` e ícones `aria-hidden`; o form de arquivar não tem `aria-label`, mas o `title` do botão supre.
- O `confirm()` nativo é acessível, porém não estilizável.
- O select de filtro tem `aria-label="Filtrar por status"`.
- O modal de criação tem `role="dialog"`/`aria-hidden` gerenciados pelo Bootstrap, mas `data-bs-backdrop="static"` + `keyboard:false` impedem fechar com ESC/clique fora — decisão deliberada (`creation-success-modal.blade.php:4-5`).
- Status é texto (`Publicado`/`Rascunho`), não só cor.
- **Lacuna de a11y**: erros de validação não são anunciados nem associados aos campos.

## 9. UI Portal

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

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

- `Agenda:read` para abrir as três telas; `Agenda:write` para POST/PUT/DELETE (`V2PermissionMiddleware.php:101-127`).
- Flag `Agenda` desligada: o CMS responde `404 Modulo desativado.` (HTML; AJAX/JSON recebe 404 JSON), sem redirect ao dashboard (`ModuleAccessMiddleware.php:52-55`).
- Menu: `ModuleProvider::getConfig()` posiciona "Agenda" em 6, ícone `bi bi-calendar-event`, com "Todos os Eventos" e "Novo Evento" (`ModuleProvider.php:13-41`).
- `password`/segredos: não se aplica.

## 11. Observabilidade, erros e limites

- Cada submit grava logs `[Agenda] Store/Update/Destroy ...` com id/título (nível debug) e erros de save (nível error).
- Erros de validação não são logados.
- O layout global mostra `flash::message`; não há toast customizado.
- Limite de upload definido pelo serviço de mídia.
- Sem confirmação para descartar alterações ao sair do formulário.

## 12. Seeds e dados iniciais

Não há seed de eventos na unidade; a lista nasce vazia em instalação nova. Para demonstração, `DemoContentSeeder` cria 5 eventos (`V2/Seeds/Content/DemoContentSeeder.php:611-668`).

## 13. Lacunas

- **LG-AGENDA-13** — não há exibição de `$errors`; não foi possível determinar se isso é decisão ou regressão. Onde procurar: versões anteriores do layout CMS.
- **LG-AGENDA-14** — o formato inicial `Y-m-d H:i` no campo datetime pode ser rejeitado pelo Flatpickr; não foi possível testar no navegador (levantamento read-only). Onde procurar: `assets/js/cms-layout.js` e console.
- **LG-AGENDA-15** — não há indicação de como o `_form` deve tratar uploads de PDF/áudio, embora `accept` seja configurável.
- **LG-AGENDA-16** — o destino do `image_id_alt` não existe no backend (LG-AGENDA-03).
- **LG-AGENDA-17** — não há bulk actions nem exportação; não foi possível saber se eram desejadas.

## 14. Checklist de paridade

- [ ] `PC-AGENDA-01` — criar com título+início mostra modal e badge "Rascunho".
- [ ] `PC-AGENDA-12` — arquivar pede confirmação e mostra a flash com o título entre aspas.
- [ ] `PC-AGENDA-15` — modal de criação aparece uma vez e o botão volta ao index.
- [ ] `PC-AGENDA-28` — listagem tem contador, 6 colunas, busca e filtro de status.
- [ ] `PC-AGENDA-29` — filtro de status aplicado sozinho fica invisível após reload (painel colapsado).
- [ ] `PC-AGENDA-30` — formulário sempre tem ao menos uma linha de programação e não permite remover a última.
- [ ] `PC-AGENDA-31` — trocar o título não altera a URL do evento.
- [ ] `PC-AGENDA-32` — erro de validação não exibe mensagem e perde as linhas de programação digitadas.
- [ ] `PC-AGENDA-33` — `is_active` desmarcado envia `0` e o evento some do portal.
- [ ] `PC-AGENDA-34` — imagem atual aparece no componente com preview e o hidden recebe o id.

## 15. Evidências consultadas

- `V2/Modules/Publicacoes/Agenda/Views/cms/index.blade.php:1-152`.
- `V2/Modules/Publicacoes/Agenda/Views/cms/_form.blade.php:1-230`.
- `V2/Modules/Publicacoes/Agenda/Views/cms/create.blade.php:1-23`; `edit.blade.php:1-25`.
- `V2/Modules/Publicacoes/Agenda/Controllers/EventosCmsController.php:39-305`.
- `V2/layouts/cms/layouts/app.blade.php:90,102`.
- `V2/layouts/cms/partials/creation-success-modal.blade.php:1-29`.
- `V2/layouts/cms/partials/filter-card.blade.php:1-175`.
- `V2/Services/Media/Views/components/media-input.blade.php:1-114`.
- `public/assets/js/cms-layout.js:65-67`.
- `V2/Core/Middleware/V2PermissionMiddleware.php:101-127`; `ModuleAccessMiddleware.php:44-62`.
