# Agenda de Eventos — Regras de Negócio, Acoplamentos e Armadilhas

---

## 1. Regras de negócio observáveis

| # | Regra | Onde está implementada |
|---|---|---|
| R1 | Só eventos com `status = true` aparecem no portal | `v6/app/Http/Controllers/Portal/CalendarEventController.php:28,51` |
| R2 | A listagem pública pagina 12 registros por página | `paginate(12)` no mesmo controller |
| R3 | A listagem pública **não tem ordenação explícita** (ordem do banco) | idem |
| R4 | O filtro por categoria usa o **slug** da categoria | `@categoryIndex($slug)` -> `whereHas('categoryCalendarEvents', fn => slug)` |
| R5 | O detalhe busca o evento **apenas pelo slug**, ignorando `{category}` | `CalendarEvent::where('slug', $slug)->first()` |
| R6 | Um evento pertence a **uma** categoria (FK simples) | `calendar_events.category_calendar_events_id` |
| R7 | Um evento tem **no máximo uma** secretaria e **um** órgão | `secretarias_id`, `orgaos_id` (FK `contents.id`) |
| R8 | O slug do evento deriva do título (máx. 50 chars) com desambiguação `-N` | `CalendarEventRepositoryDefaultImpl::structureUpInsert()` |
| R9 | O slug da categoria deriva do nome, com desambiguação `-N` | `CategoryCalendarEventController::extractSave()` |
| R10 | "Publicar" desmarcado = rascunho (o evento fica fora do portal) | `isTrue($request->status)` no repository |
| R11 | "Ativar Galeria" cria a aba Galeria e redireciona para `editar-agenda#galeria` | `CalendarEventController@store/@update` |
| R12 | A imagem de destaque é **substituída** a cada novo upload (apaga o arquivo anterior) | `extractedToArchive()` -> `Archive::where(...,'highlight',1)->first()` + `saveUpload(..., $archiveOld->path)` |
| R13 | A galeria usa `highlight = null`; o destaque usa `highlight = 1` | `CalendarEvent::archiveGallery()` e o controller |
| R14 | Categorias e eventos usam **soft delete** | `SoftDeletes` nos dois models |
| R15 | Exclusão em lote apaga todos os ids enviados em uma transação | `Repository::destroyAll()` |
| R16 | Toda ação do CMS gera log de auditoria (`activity()`) com o id do usuário | `CalendarEventController::log()`, `deleteByLote()` |
| R17 | O clique no menu do CMS incrementa `menu_orders.clicks` | `menuClicks('agenda-evento')` (slug do `menu_orders`) |
| R18 | O dashboard conta **todos** os eventos não soft-deleted (sem filtro de status/data) | `v6/app/Services/Dashboard/Widgets/EventosWidget.php:43` (`CalendarEvent::count()`), coberto por `v6/tests/Feature/DashboardEventosWidgetTest.php` |

---

## 2. Acoplamentos

### 2.1 Dependências de outros módulos (this módulo depende)

| Módulo | Acoplamento |
|---|---|
| **Mídia / Arquivos (`archives`)** | Destaque e galeria gravam na tabela `archives` com `calendar_events_id`. O controller do CMS depende de `IArchiveRepository` e do model `Archive` |
| **Conteúdo / Categorias de conteúdo** | Secretarias e órgãos vêm de `contents` filtrados por `category_contents.slug = 'secretarias' | 'orgaos'` (`GetInformationsPrefectureTrait::getContentByCategoryWith`) |
| **Galeria genérica** | A aba Galeria usa `Prefecture\ArchiveController` e o endpoint `/cms/galeria` (compartilhado com Conteúdo e Página Estática) |
| **Dashboard** | `EventosWidget` conta `CalendarEvent` |
| **Categorias de conteúdo / Menu do portal** | O item de menu "Agenda" aponta para a categoria de conteúdo `agenda-eventos` (`category_contents`), que é **outra** tabela, distinta de `category_calendar_events` |
| **Configurações** | `Configuration::where('key','layout')` define o template do portal; `cor_1` é usado no CSS da listagem |

### 2.2 Dependências de módulos que dependem deste

| Consumidor | Acoplamento |
|---|---|
| Dashboard (`EventosWidget`) | Contagem de eventos (regra: não soft-deleted) |
| Portal — menu e breadcrumb | Rotas `agenda-eventos*` e `calendar-events*` |
| View SQL `agenda_de_eventos` | Reproduz a regra de listagem do CMS; qualquer mudança na tabela `calendar_events` pode quebrar o dashboard e o CMS |

---

## 3. Armadilhas técnicas

| # | Armadilha | Impacto |
|---|---|---|
| A1 | `CalendarEvent::categoryCalendarEvents()` é **hasOne**, não belongsTo | Funciona, mas a semântica está invertida. O `whereHas` de `@categoryIndex` vira um `EXISTS` ligando `calendar_events.category_calendar_events_id` a `category_calendar_events.id` e filtrando `slug` |
| A2 | `CategoryCalendarEvent::calendarEvents()` é **belongsTo** com a FK invertida (`category_calendar_events_id` -> `calendar_events.id`) | A coleção "categoria -> eventos" não existe; quem a usa é o `whereHas` no lado do evento |
| A3 | `CategoryCalendarEvent::contents()` referencia `contents.category_contents_id` a partir de `id` | Relação inválida (coluna inexistente na tabela da categoria) |
| A4 | `CalendarEvent::agendaEventosSecretarias()` / `agendaEventosOrgaos()` são auto-relacionamentos com `calendar_events` | Não apontam para `contents`; não são usados pelo módulo |
| A5 | `archives_id` está no `$fillable` de `CalendarEvent`, mas a coluna não existe | Inofensivo hoje (o campo nunca é enviado); quebra se alguém começar a enviá-lo |
| A6 | O model **não declara `$casts`** | `status`/`galery` chegam como `true`/`false` do driver, mas `initial_date`/`final_date` são strings — o Blade converte com `strtotime()` |
| A7 | `toUSDataHourFormat()` **lança exceção** se a data não estiver em `dd/mm/aaaa hh:mm` | Se `final_date` vier vazio (agora `nullable`), o repository cai no `catch (\Throwable)` e devolve `false`: o controller redireciona de volta **sem mensagem de erro** — falha silenciosa |
| A8 | `CalendarEventRepositoryDefaultImpl::upInsert()` engole a exceção (`catch (\Throwable $th) {}`) | Falhas de banco viram "não salvou", sem log |
| A9 | A interface `ICalendarEventRepository` está **vazia** | O contrato não define o que o controller pode fazer; a implementação depende da classe base `Repository` |
| A10 | O controller do portal acessa `$content->id` **antes** de testar `$content` | Um slug inexistente gera erro 500 em vez da tela "NOTÍCIA NÃO ENCONTRADA" |
| A11 | `$content->linksUnique` **não existe** em `CalendarEvent` | O bloco `@include("portal.pages.elements.video-youtube")` nunca executa (a view includeada também não existe em `v6/resources/views/portal/` — só há `portal/components/secretarias/video-youtube.blade.php` e `portal/layouts/default/pages/elements/video-youtube.blade.php`) |
| A12 | A view do portal usa `@getMinFile(...)`, que **não é uma diretiva Blade registrada** | O token é impresso literalmente no `background:url(...)` |
| A13 | A rota `agenda-evento.show` (do `Route::resource`) não tem método `show()` no controller | Erro 500 em `/cms/agenda-evento/{id}` |
| A14 | `categoria-agenda-evento.create` e `.show` não têm métodos correspondentes | Erro 500 nas rotas correspondentes |
| A15 | A DataTable usa `data.galery`, mas a view SQL expõe a coluna **`galeria`** | `createGaleryButton` sempre recebe `undefined` -> o botão de galeria fica desabilitado na listagem |
| A16 | A listagem do CMS consulta **todos** os eventos (`DB::table('agenda_de_eventos')->get()`) sem paginação no servidor | O DataTables faz a paginação no cliente: consumo de memória e CPU cresce com a base |
| A17 | A exclusão em lote não valida o array `toDelete` | `whereIn` com array não numérico gera erro de SQL capturado e devolvido como 500 |
| A18 | `deleteByLote` não usa transação própria, mas o repository usa `DB::transaction` internamente | Operação atômica, porém o log de auditoria é gravado **antes** da exclusão |
| A19 | O soft delete do evento **não** remove os `archives` | Arquivos órfãos permanecem em `archives` e no storage |
| A20 | A FK `archives.calendar_events_id` não tem `ON DELETE CASCADE` | Deleção física do evento viola a integridade referencial |
| A21 | A rota `POST /cms/ajax-upload-files-calendar-event` grava `archives` **sem** `calendar_events_id` no fluxo "sem evento" (midias) e o método **não retorna resposta HTTP** | Endpoint legado sem chamador no front atual; retorno vazio (200) |
| A22 | `ajaxUploadFiles` engole exceções (`catch (Exception $e) { // }`) | Falha silenciosa no upload |
| A23 | `ajaxUploadFiles` aceita uma lista ampla de extensões (24) só no fluxo "sem evento" | Risco de upload de arquivo arbitrário pelo endpoint legado |
| A24 | A listagem do portal não filtra por data (eventos passados continuam visíveis) | Regra de negócio implícita; a data inicial só é exibida |
| A25 | `where('status','1')` compara boolean com string | Funciona no PostgreSQL (cast implícito), mas é frágil |
| A26 | `CategoryCalendarEventController::store()` usa `CategoryCalendarEvent::create($request->request->all())` | Mass assignment direto do POST; campos fora do `$fillable` são ignorados silenciosamente |
| A27 | `CategoryCalendarEventController::update()` usa `updateOrCreate` com o array completo do request | Mesma observação de A26 |
| A28 | O `update()` do controller de categorias **não** usa `FormRequest` | Valida apenas o que o `CategoryCalendarEventRequest` define no `store` |

---

## 4. Código duplicado / copiado entre v5 e v6

| Arquivo | Situação |
|---|---|
| `v5/app/Http/Controllers/Prefecture/CalendarEvent/CalendarEventController.php` x `v6/app/Http/Controllers/Prefecture/CalendarEvent/CalendarEventController.php` | **Idênticos** (sem diferenças no `diff`) |
| `v5/app/Http/Controllers/Prefecture/CalendarEvent/CategoryCalendarEventController.php` x `v6/app/Http/Controllers/Prefecture/CalendarEvent/CalendarEventController.php` | **Idênticos** |
| `v5/app/Http/Controllers/Portal/CalendarEventController.php` x `v6/app/Http/Controllers/Prefecture/CalendarEvent/CalendarEventController.php` | **Idênticos** |
| `v5/app/Http/Model/Prefecture/CalendarEvent.php` x `v6/app/Http/Controllers/Prefecture/CalendarEvent/CalendarEventController.php` | **Idênticos** |
| `v5/app/Http/Model/Prefecture/CategoryCalendarEvent.php` x `v6/app/Http/Controllers/Prefecture/CalendarEvent/CalendarEventController.php` | **Idênticos** |
| `v5/app/Repositories/Eloquent/Prefecture/CalendarEvent/CalendarEventRepositoryDefaultImpl.php` x `v6/app/Http/Controllers/Prefecture/CalendarEvent/CalendarEventController.php` | Difere apenas no tratamento de `category_calendar_events_id` (v6 normaliza `0` -> `null`) |
| `v5/app/Http/Requests/CalendarEventRequest.php` x `v6/app/Http/Controllers/Prefecture/CalendarEvent/CalendarEventController.php` | v6: `final_date` passou de `required` para `nullable`; `SelectRule` passou a receber o rótulo "Categoria" |
| `v5/app/Http/Requests/CategoryCalendarEventRequest.php` x `v6/app/Http/Controllers/Prefecture/CalendarEvent/CalendarEventController.php` | v6: `name` passou de `required` para `nullable` |
| Views do portal (`calendar-events/index.blade.php`, `show.blade.php`) | **Idênticas** |
| Views do CMS (`index`, `create`, `edit`, `category-calendar-events/*`) | **Idênticas**, exceto `form.blade.php` (v6: `p-0` no wrapper, select de categoria com `size => 2` e "Secretaria" deixa de ser obrigatório) |

O módulo sofreu **cópia literal** entre as duas versões — não há refatoração nem
evolução estrutural.

---

## 5. Onde a v6 divergiu da v5

1. **Nova rota portal `/agenda`** (nome `agenda`), alias de `/agenda-eventos`
   (`v6/routes/web.php:26`; ausente em `v5/routes/web.php`).
2. **Validação mais permissiva**: `final_date` nullable e `name` da categoria nullable.
3. **Normalização da categoria**: `category_calendar_events_id == 0` vira `null` no
   repository (permite eventos sem categoria).
4. **Select de categoria com rótulo de erro** ("O campo Categoria é obrigatório").
5. **Formulário**: "Secretaria" e "Órgãos" passaram a ser opcionais no front
   (`mandatory => false`), e o select de categoria ocupa menos espaço (`size => 2`).
6. **Painel (dashboard)**: o `EventosWidget` e o teste de regressão
   `v6/tests/Feature/DashboardEventosWidgetTest.php` são novidade da v6 — o módulo
   passou a ser consumido como indicador do dashboard.
7. A **view SQL `agenda_de_eventos`** ganhou a coluna `ce.slug`
   (`v6/database/migrations/2024_11_29_093605_update_view_agenta_de_eventos.php`),
   necessária para o botão "Acessar Evento" da listagem.

> Racional das mudanças 2-4: tornar a categoria e a data de encerramento opcionais para
> permitir a edição de eventos antigos (importados) sem categoria e com data final vazia.
> [INFERÊNCIA] — não há ticket ou comentário no código explicando a mudança.
