# Vereadores e Mandatos — Modelagem de Dados

> Módulo `vereadores` (id 43). Todas as quatro tabelas são **novas na v6**.
> Tipos e nullability conferidos no PostgreSQL em execução (`\d <tabela>`).

---

## 1. Visão geral

| Tabela | Lines de criação | Propósito |
|---|---|---|
| `mandate_categories` | `v6/database/migrations/2024_09_11_104115_create_mandate_categories_table.php` | Categorias de mandato (Legislaturas, Mesas Diretoras, Comissões Permanentes). |
| `mandates` | `v6/database/migrations/2024_09_11_104447_create_mandates_table.php` | Mandatos (uma legislatura, uma mesa diretora, uma comissão). |
| `councilmen` | `v6/database/migrations/2024_09_11_104741_create_councilmen_table.php` | Vereadores. |
| `councilmen_mandates` | `v6/database/migrations/2024_09_11_104812_create_councilmen_mandates_table.php` | Pivot N:NLIBERMANDOS + cargo + ordem. |

As quatro migrations usam `$table->engine = 'InnoDB'` (herança do MySQL; inócuo no
PostgreSQL) e `softDeletes()`.

## 2. Colunas

### 2.1 `councilmen`

| Coluna | Tipo | Null | Default | Observação |
|---|---|---|---|---|
| `id` | `integer` (serial) | não | `nextval('councilmen_id_seq')` | PK |
| `name` | `varchar(191)` | não | — | Nome completo |
| `slug` | `varchar(191)` | não | — | `str_slug($name)`; único apenas por convenção (sem índice) |
| `party` | `varchar(191)` | não | — | Partido |
| `text` | `text` | **sim** | — | Biografia (HTML do Summernote) |
| `archives_id` | `integer` | **sim** | — | FK → `archives.id` (foto) |
| `status` | `boolean` | não | — | `true` = visível no portal |
| `created_at` / `updated_at` | `timestamp(0)` | sim | — | |
| `deleted_at` | `timestamp(0)` | sim | — | Soft delete |

FK: `councilmen_archives_id_foreign FOREIGN KEY (archives_id) REFERENCES archives(id)`
(sem `ON DELETE`).

### 2.2 `mandate_categories`

| Coluna | Tipo | Null | Default | Observação |
|---|---|---|---|---|
| `id` | `integer` (serial) | não | `nextval('mandate_categories_id_seq')` | PK |
| `name` | `varchar(191)` | não | — | Ex.: "Legislaturas" |
| `slug` | `varchar(191)` | não | — | `str_slug($name)`; usado na URL `/mandatos/{slug}` |
| `created_at` / `updated_at` / `deleted_at` | `timestamp(0)` | sim | — | Soft delete |

Sem FKs próprias; é referenciada por `mandates`.

### 2.3 `mandates`

| Coluna | Tipo | Null | Default | Observação |
|---|---|---|---|---|
| `id` | `integer` (serial) | não | `nextval('mandates_id_seq')` | PK |
| `name` | `varchar(191)` | não | — | Ex.: "Legislatura 2021-2024" |
| `slug` | `varchar(191)` | não | — | `str_slug($name)` |
| `start_year` | `integer` | não | — | Ano inicial |
| `end_year` | `integer` | não | — | Ano final |
| `mandate_categories_id` | `integer` (unsigned) | não | — | FK → `mandate_categories.id` |
| `status` | `boolean` | não | — | `true` = ativo |
| `created_at` / `updated_at` / `deleted_at` | `timestamp(0)` | sim | — | Soft delete |

FK: `mandates_mandate_categories_id_foreign FOREIGN KEY (mandate_categories_id)
REFERENCES mandate_categories(id)`.

### 2.4 `councilmen_mandates` (pivot)

| Coluna | Tipo | Null | Default | Observação |
|---|---|---|---|---|
| `id` | `integer` (serial) | não | `nextval('councilmen_mandates_id_seq')` | PK; é este `id` que o CMS usa para excluir a associação |
| `councilmen_id` | `integer` (unsigned) | não | — | **Sem FK declarada** |
| `mandates_id` | `integer` (unsigned) | não | — | **Sem FK declarada** |
| `order` | `integer` | **sim** | `NULL` | Posição na ordenação do mandato (1..N) |
| `role` | `varchar(191)` | **não** | — | Cargo ocupado pelo vereador no mandato (ex.: "Presidente") |
| `created_at` / `updated_at` | `timestamp(0)` | sim | — | `nullableTimestamps()` |
| `deleted_at` | `timestamp(0)` | sim | — | Coluna existe, mas o model **não usa `SoftDeletes`** |

Índices: apenas `councilmen_mandates_pkey`. **Não há** índice único composto
`(councilmen_id, mandates_id)` nem FKs — apesar de as migrations de `councilmen` e
`mandates` criarem FKs normalmente.

## 3. Relacionamentos

```
        mandate_categories                mandates                      councilmen
        ┌────────────────┐          ┌──────────────────┐          ┌────────────────┐
        │ id  (PK)       │ 1──────N │ id  (PK)         │     N──M │ id  (PK)       │
        │ name           │          │ name             │          │ name           │
        │ slug           │          │ slug             │          │ slug           │
        │ timestamps     │          │ start_year       │          │ party          │
        │ deleted_at     │          │ end_year         │          │ text           │
        └────────────────┘          │ mandate_categori │          │ archives_id ───┼──N archives.id
                                   │  es_id (FK)      │          │ status         │
                                   │ status           │          │ timestamps     │
                                   │ timestamps       │          │ deleted_at     │
                                   │ deleted_at       │          └────────────────┘
                                   └──────────────────┘                 ▲ │
                                             ▲                         │ │
                                             │   ┌──────────────────────────────┐
                                             └───┤ councilmen_mandates (pivot) │
                                                 │ id (PK)                     │
                                                 │ councilmen_id (sem FK)       │
                                                 │ mandates_id  (sem FK)       │
                                                 │ order (nullable)            │
                                                 │ role  (NOT NULL)             │
                                                 │ timestamps / deleted_at      │
                                                 └──────────────────────────────┘
```

### 3.1 Papel de `order`

`councilmen_mandates.order` é a **posição do vereador dentro do mandato**. Quem o
consome:

- `v6/app/Http/Controllers/Prefecture/MandateController.php:210-213` —
  `CouncilmenMandates::with('councilmen')->where('mandates_id',$id)->orderBy('order','asc')`
  (tela "Vereadores do Mandato" do CMS).
- `v6/resources/views/portal/layouts/modelo1/informacoes-instituicao/mandates/show.blade.php`
  — mesma ordenação (`orderBy('order','asc')`) dentro do acordeão público.

Quem o escreve: `MandateController@ajustOrder` (`v6/app/Http/Controllers/Prefecture/MandateController.php:226-238`),
que recebe `sortable_data[]` (ids do pivot) e **renumera de 1 em 1**. O campo é
nullable e nasce `NULL` quando o mandato é atribuído via
`CityCouncilorController@setMandates` — o que faz as novas entradas aparecerem no fim
da ordenação (no PostgreSQL, `NULL` ordena por último em `ASC`).

### 3.2 Papel de `role`

`role` é o **cargo** que o vereador ocupa no mandato ("Presidente", "Vice-Presidente",
"Correlator"). É texto livre (obrigatório, máximo 191). Aparece em:

- portal, no perfil do vereador: `{{$mandate->pivot->role}}`
  (`v6/resources/views/portal/layouts/modelo1/informacoes-instituicao/vereadores/show.blade.php`);
- portal, no acordeão de mandatos: `{{$vereador->role}}`
  (`v6/resources/views/portal/layouts/modelo1/informacoes-instituicao/mandates/show.blade.php`);
- CMS, na tela de ordenação: `{{ $councilmen->role }}`
  (`v6/resources/views/prefecture/mandates/edit-council-members.blade.php`);
- CMS, na tela de atribuição: campo `role` do formulário
  (`v6/resources/views/prefecture/councilmen/show.blade.php`).

É validação manual (não Form Request) em `CityCouncilorController@setMandates`:
`Validator::make($request->all(), ['mandates_id' => 'required', 'role' => 'required|max:190'], $messages)->validate()`.

### 3.3 Relações declaradas nos models

| Model | Método | Declaração | Correto? |
|---|---|---|---|
| `CityCouncilor` | `mandates()` | `belongsToMany(Mandate::class,'councilmen_mandates','councilmen_id','mandates_id')->withPivot('id','role')` | Sim |
| `CityCouncilor` | `archives()` | `hasOne(Archive::class,'id','archives_id')` | Funcional (1:1), mas `belongsTo` seria o idiomático |
| `Mandate` | `categories()` | `hasOne(MandateCategory::class,'id','mandate_categories_id')` | **Armadilha**: o nome sugere `hasMany`, mas a FK está em `mandates` — o correto é `belongsTo`. Funciona porque `hasOne` com chave estrangeira em `id` produz o mesmo resultado para cardinalidade 1:1 |
| `Mandate` | `councilmen()` | `belongsToMany(CityCouncilor::class,'councilmen_mandates','mandates_id','councilmen_id')->withPivot('id','role')` | Sim |
| `CouncilmenMandates` | `councilmen()` | `hasOne(CityCouncilor::class,'id','councilmen_id')` | **Armadilha**: o correto seria `belongsTo` |
| `Content` | `councilman()` | `belongsTo(CityCouncilor::class,'councilmen_id')` | Sim |
| `Viap` | `councilman()` | `belongsTo(CityCouncilor::class,'councilman_id')` | Sim |

Note que `withPivot('id','role')` **não** inclui `order`; por isso o portal só
consegue ler `role` via pivot e precisa de uma query separada
(`CouncilmenMandates`) para ordenar.

## 4. FKs externas que apontam para `councilmen`

| Tabela | Coluna | FK | Efeito |
|---|---|---|---|
| `contents` | `councilmen_id` | `FOREIGN KEY (councilmen_id) REFERENCES councilmen(id) ON DELETE SET NULL` | Indica o **requerente** de um conteúdo da categoria `requerimentos`. Coluna criada por `v6/database/migrations/2026_06_12_100000_add_councilmen_id_to_contents_table.php`. |
| `viap` | `councilman_id` | `FOREIGN KEY (councilman_id) REFERENCES councilmen(id) ON DELETE CASCADE` | Vínculo "Gabinete" de um atendimento (VIAP). Coluna criada por `v6/database/migrations/2026_05_25_112000_create_viap_table.php`. |

Consequência: **excluir fisicamente um vereador apaga em cascata os vínculos de
gabinete do VIAP**; o soft delete (`councilmen.deleted_at`) não dispara cascade.

## 5. Views SQL que leem `councilmen`

| View | O que abre | Trecho |
|---|---|---|
| `requerimentos` | Lista de requerimentos com o nome do vereador requerente | `LEFT JOIN councilmen cm ON cm.id = c.councilmen_id AND cm.deleted_at IS NULL` |

A view é consumida pelo CMS de conteúdo genérico
(`v6/app/Http/Controllers/Prefecture/ContentController.php:203-212`), que também
adiciona o vereador atual ao topo da lista de-requisitantes, mesmo se estiver
inativo.

## 6. Seeds

| Seed | Caminho | O que faz |
|---|---|---|
| `MandateCategoryTableSeeder` | `v6/database/seeds/MandateCategoryTableSeeder.php` | Cria as categorias `mesas-diretoras` ("Mesas Diretoras"), `legislaturas` ("Legislaturas") e `comissoes-permanentes` ("Comissões Permanentes") via `updateOrCreate`/`updateOrInsert`. |
| `AddVereadoresCarouselToLayout` | `v6/database/seeds/AddVereadoresCarouselToLayout.php` | Cria/atualiza as chaves `layouts` `vereadores` (section, order 99, path `portal.layouts.modelo5.home`) e `vereadores-carousel` (element, order 1, path `portal.layouts.modelo5.home.vereadores`, título "Carrossel de Vereadores") e injeta o HTML de configuração em `layouts.value` das chaves `tags` e `preview`. |
| `MenuOrderSeeder` | `v6/database/seeds/MenuOrderSeeder.php:251-255` | `menu_orders` slug `vereadores`, `modulo_id` 43. |
| `ResourcesTableSeeder` | `v6/database/seeds/ResourcesTableSeeder.php:513-545` | `resources` slug `vereadores`, `mandatos`, `categorias-mandatos`, `atribuir-mandato`, todos com `modulo_id` 43. |
| `LinksTableSeeder` | `v6/database/seeds/LinksTableSeeder.php:388-406` | Atalhos `/vereadores` e `/mandatos`. |

## 7. Estado no banco (verificado)

| Item | Valor |
|---|---|
| `modulos` (slug `vereadores`) | id 43, name "Vereadores", `status = false` (desligado por padrão) |
| `menu_orders` | id 32, slug `vereadores`, `clicks = 134`, `modulo_id = 43` |
| `permissions` | 9 linhas genéricas (`index`, `create`, `edit`, `delete`, `inactive`, `inactivate`, `restore`, `show`, `publish`) — **não há** linhas `vereadores.create` etc.; o `has_access()` resolve pelo slug completo via Sentinel |
