# Vereadores e Mandatos — Regras de Negócio e Portabilidade

> Módulo `vereadores` (id 43). **Novo na v6** — a `v5` não tem nada equivalente.

---

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

| # | Regra | Onde é implementada | Efeito observável |
|---|---|---|---|
| R1 | Um vereador só é publicado se `councilmen.status = true` | `v6/app/Http/Controllers/Portal/CityCouncilorController.php:19` | `/vereadores` não lista vereadores desativados. |
| R2 | O perfil `/vereadores/{id}` **não** filtra por `status` | `v6/app/Http/Controllers/Portal/CityCouncilorController.php:34` | Um vereador desativado continua acessível por link direto. |
| R3 | O carrossel da home só exibe `status = 1` | `v6/resources/views/portal/layouts/modelo5/home/vereadores/vereadores-carousel.blade.php` | Vereadores desativados somem do carrossel. |
| R4 | Não se pode excluir um vereador que tenha mandatos | `v6/app/Http/Controllers/Prefecture/CityCouncilorController.php:259-262` | Flash: "Esse vereador tem mandatos ativos, não pode ser excluído." |
| R5 | A exclusão de vereador é **soft delete** (`councilmen.deleted_at`) | `CityCouncilor` usa `SoftDeletes` | O registro some das listagens mas permanece no banco. |
| R6 | A remoção de uma associação vereador↔mandato é **física** | `CouncilmenMandates` **não** usa `SoftDeletes` | A linha some de vez do pivot, apesar de a tabela ter `deleted_at`. |
| R7 | Todo mandato pertence a uma categoria | `mandates.mandate_categories_id` (NOT NULL, FK) | O portal agrupa os mandatos por categoria. |
| R8 | `role` é obrigatório e identifica o cargo | `Validator::make` inline em `setMandates` (`role` → `required|max:190`) | Sem cargo, a atribuição é rejeitada. |
| R9 | `order` define a ordem dos vereadores dentro do mandato | `MandateController@ajustOrder` + `orderBy('order','asc')` | Reordenar no CMS muda a ordem no portal. |
| R10 | `sync()` substitui o conjunto inteiro de mandatos do vereador | `v6/app/Http/Controllers/Prefecture/CityCouncilorController.php` (`$vereador->mandates()->sync($mandatos)`) | Não é aditivo: mandatos não listados são desvinculados. |
| R11 | Ao criar, `status` default é `'0'` (inativo) | `store()` — `if (!isset($dataForm['status'])) { $dataForm['status'] = '0'; }` | Um vereador criado sem o checkbox "Ativar" **não** aparece no portal. |
| R12 | O `slug` é gerado a partir do nome e recebe sufixo aleatório se duplicado | `store()` deCadastrar, `store()` de mandato e `store()` de categoria | URLs de mandato são instáveis quando há nomes repetidos. |
| R13 | O `slug` do vereador **não** é regenerado na edição | `update()` não toca em `slug` | Renomear o vereador mantém a URL antiga. |
| R14 | Na edição de mandato, a verificação de duplicidade de slug **não exclui o próprio registro** | `MandateController@update` — `Mandate::where('slug',$dataForm['slug'])->first()` | Salvar sem alterar o nome gera um **novo** slug com sufixo aleatório a cada gravação. |
| R15 | A foto é gravada em `content/vereadores/{id}/{nome}` e registrada em `archives` com `path = "storage/vereadores/{id}/{nome}"` | `store()`/`update()` | `path` e o disco real divergem; o carrossel corrige com *fallback* de string. |
| R16 | Ao remover a foto, `councilmen.archives_id` volta a `null` | `ajaxDeleteFiles` | O portal volta a exibir o placeholder. |
| R17 | O módulo nasce **desligado** | `modulos.status = false` (id 43) | O operador precisa ativar em `/cms/modulos`. |
| R18 | A ordenação só é persistida para os itens enviados | `ajustOrder` itera `sortable_data` | Itens fora da lista mantêm o `order` antigo (podendo duplicar posições). |

## 2. Dependências com outros módulos

| Módulo | Forma do acoplamento |
|---|---|
| **Conteúdo genérico / requerimentos** | `contents.councilmen_id` → `councilmen.id` (ON DELETE SET NULL). O controller de conteúdo usa `CityCouncilor::where('status', true)->orderBy('name')->get()` para montar o select de requerente (`v6/app/Http/Controllers/Prefecture/ContentController.php:204`) e o `ContentRequest` valida a existence do vereador (`v6/app/Http/Requests/ContentRequest.php:246-255`). A view SQL `requerimentos` faz `LEFT JOIN councilmen`. |
| **VIAP** | `viap.councilman_id` → `councilmen.id` (ON DELETE CASCADE). O(repository cria slug com o nome do vereador (`v6/app/Repositories/Eloquent/Prefecture/Viap/ViapRepositoryDefaultImpl.php:52-57`). |
| **Layouts / home (tema `modelo5`)** | O carrossel é um elemento de layout; o seed `AddVereadoresCarouselToLayout` injeta o HTML de configuração em `layouts.value`. |
| **Módulos (CMS)** | `menu_orders.modulo_id = 43`, `resources.modulo_id = 43`. O módulo é ligado/desligado em `/cms/modulos`. |
| **Auditoria / activity log** | Todos os controllers gravam em `activity()`. |
| **Storage / arquivos** | `App\Prefecture\Archive` + `Storage::delete()`. |

O módulo **não** depende de `sessoes`, `pauta-do-dia`, `transmissoes` ou `tv-camara`
(também novos na v6).

## 3. Armadilhas e fragilidades

### 3.1 Relações declaradas incorretamente

| Onde | Declaração | Problema |
|---|---|---|
| `v6/app/Http/Model/Prefecture/Mandate.php:26-29` | `hasOne(MandateCategory::class,'id','mandate_categories_id')` | A FK está em `mandates`; o idiomático é `belongsTo(MandateCategory::class,'mandate_categories_id')`. Funciona por acaso (chave estrangeira em `id`), mas quebra qualquer expectativa de coleção e confunde leitura. |
| `v6/app/Http/Model/Prefecture/CouncilmenMandates.php:18-20` | `hasOne(CityCouncilor::class,'id','councilmen_id')` | O correto é `belongsTo(CityCouncilor::class,'councilmen_id')`. |
| `v6/app/Http/Model/Prefecture/CityCouncilor.php:32-35` | `hasOne(Archive::class,'id','archives_id')` | Semelhante: `belongsTo` seria o correto. Funciona porque a FK é única por vereador. |

### 3.2 `CouncilmenMandates` sem `SoftDeletes`

A migration cria `deleted_at`, mas o model não usa o trait. Consequências:

- `dropMandates()` executa `DELETE` físico — a associação não é recuperável e o
  `deleted_at` permanece `NULL` para sempre.
- A coluna `deleted_at` fica sem uso, induzindo quem lê o schema a supor soft delete.
- Se o trait for adicionado depois, os registros já removidos fisicamente não voltam.

### 3.3 `count($vereador->mandates)` em memória

`v6/app/Http/Controllers/Prefecture/CityCouncilorController.php:259-262`:

```php
$vereador = $this->councilor->findOrFail($id);
$mandatos = count($vereador->mandates);
```

- Carrega **todas** as linhas do pivot para contar, em vez de `exists()` /
  `withCount()`. Custo desnecessário e comportamento diferente do desejado: a regra
  "não pode excluir se **tem** mandatos" usa o total, não a existência.
- `SoftDeletes` no modelo `Mandate` **não** se aplica ao pivot: como
  `CouncilmenMandates` não tem `SoftDeletes` e a relação `belongsToMany` não filtra
  `deleted_at`, vínculos rem fisicamente não contam; mas se alguém adicionar
  `SoftDeletes` ao pivot sem adicionar `withTrashed` à relação, a contagem muda.

### 3.4 Pivot sem FK, sem índice e sem unique

`v6/database/migrations/2024_09_11_104812_create_councilmen_mandates_table.php` cria
apenas a PK. Consequências:

- É possível gravar `councilmen_id` apontando para um vereador inexistente
  (órfão) — a view `mandates/show.blade.php` acessaria
  `$vereador->councilmen->name` e quebraria com `null`.
- É possível gravar **duplicatas** da mesma pair (vereador, mandato).
- Não há índice para as consultas mais frequentes
  (`where('mandates_id',$id)` e `where('councilmen_id',$id)`) → *seq scan*.

### 3.5 `sync()` incompleto

`$vereador->mandates()->sync($mandatos)` insere no pivot apenas as chaves
estrangeiras. Como `councilmen_mandates.role` é **NOT NULL** e `order` é nullable, a
inserção via `sync()` **violaria** a constraint `role` — o `catch (\PDOException)`
transformaria isso num flash de erro e a transação inteira seria desfeita.

Na prática isso não acontece porque **o formulário do vereador não tem campo
`mandates_id`** (`v6/resources/views/prefecture/councilmen/form.blade.php`), embora
`create()` e `edit()` passem `$mandates` para a view. O caminho de código é
**morto** — mas continua sendo um bug latente: reintroduzir o select de mandatos
quebraria o cadastro. A atribuição funcional é feita por `setMandates()`, que grava
`role` (e deixa `order` como `NULL`).

### 3.6 Slugs

- `CityCouncilorController@update` **não** regenera o slug (apenas o `store()` faz).
- `MandateController@update` verifica duplicidade **sem excluir o registro em edição**:
  salvar o mandato sem renomear sempre gera um slug novo (`slug_xxxxx`), acumulando
  slugs órfãos e quebrando links.
- `MandateCategoryController@update` **não** regenera o slug nem verifica duplicidade.
- Nenhum slug tem índice único no banco — a unicidade é apenas "por sorte" no
  momento da criação.

### 3.7 Hardcodes e acoplamentos

| Item | Onde | Observação |
|---|---|---|
| Caminho de storage da foto | `CityCouncilorController` (`storeAs("content/vereadores/{id}/", $name)`) e as 12 views do portal | String repetida em código e em views; sem `config('filesystems')`. |
| `path` do `Archive` divergente do disco real | `CityCouncilorController` (`"storage/vereadores/$id/$name"`) | Obriga o *fallback* do carrossel. |
| `trata_nome()` | 12 arquivos Blade do portal | Função redefinida por view; deveria virar um helper único. |
| `str_slug()` + `str_random(5)` | 3 controllers do CMS | Duplicação do padrão "slug único". |
| `menuClicks('vereadores')` | `MandateController::__construct` | A tela de mandatos contabiliza clique no módulo de vereadores. |
| `has_access(['agenda.delete'])` | `prefecture/councilmen/index.blade.php` | Slug de outro módulo controlando a exclusão de vereadores. |
| `order` não está em `withPivot` | `CityCouncilor` e `Mandate` |obriga uma query extra para ordenar. |
| `order` NULL na atribuição | `setMandates` | Novas entradas vão para o fim da ordenação. |
| SQL hardcoded | — | Não há `whereRaw`/`DB::raw` no módulo. |

### 3.8 N+1

- `v6/resources/views/portal/layouts/modelo1/informacoes-instituicao/mandates/show.blade.php`
  faz **uma query por mandato** (`$CouncilmenMandates->with('councilmen')->where('mandates_id',$content->id)`)
  dentro do `@foreach`. Com N mandatos são N+1 consultas (o `with('councilmen')`
  evita o N+1 interno dos cards).
- A listagem pública de vereadores usa `with('archives','mandates')` — correto.

### 3.9 Inconsistências de interface

- A listagem de vereadores usa o id de tabela `dataTableListContentsConf` — o **mesmo id** usado em `mandates/index.blade.php` e
  `mandate-categories/index.blade.php`. Como são páginas diferentes, não há
  conflito, mas impede incluir duas listagens na mesma página.
- `prefecture/councilmen/index.blade.php` mantém comentada no `<thead>` a coluna
  "ID" (`{{--<th width="15%">ID</th>--}}`) e a mesma coluna no `<tbody>` — resíduo de
  template.
- O botão "Excluir" na tela de detalhe do vereador usa `data-toggle="delete_btn"` com
  um `confirmation` cujo `onConfirm` **não faz `form.submit()`** (está comentado) —
  a exclusão do mandato atribuído pode não acontecer.

## 4. Ausência de recursos esperados

| Recurso | Situação na v6 |
|---|---|
| Exportação (Excel/PDF/CSV) | **Não existe** no módulo. |
| API JSON | **Não existe** endpoint próprio de vereadores. |
| Fila (jobs) | **Não existe**; o upload é síncrono. |
| Cache | **Não existe**. |
| Filtros/busca no portal | **Não existe** (lista estática). |
| Paginação no portal | **Não existe** — `->get()` traz todos os vereadores ativos. |

## 5. Código duplicado / copiado

- `trata_nome()` — 12 arquivos Blade.
- Padrão "slug único com sufixo" — 3 controllers.
- Bloco de upload de foto + `Archive::create()` — duplicado em `store()` e `update()`
  do `CityCouncilorController` (≈30 linhas iguais).
- Padrão `try { ... } catch (\PDOException) { flash(...); return redirect()->route(...); }`
  repetido em todos os métodos dos 3 controllers do CMS.

## 6. Onde a v6 divergiu da v5

A v6 **introduziu** o módulo do zero. Não há código pré-existente a comparar:

- Nenhuma rota em `v5/routes/` referencia vereadores, mandatos ou câmaras.
- Nenhum model `councilmen`/`mandate` em `v5/app/Http/Model/Prefecture/`.
- Nenhuma migration equivalente em `v5/database/migrations/`.
- Nenhuma view de vereadores em `v5/resources/views/`.

Portanto, todo o comportamento descrito neste dossiê é **funcionalidade nova da v6**
e não há "código copiado da v5" para reaproveitar. Ver `08-GAPS-E-DIVIDAS.md` para o mapa
v5 → v6.
