# TV Câmara — Rotas, Controllers e Permissões

> **Escopo:** módulo `tv-camara` na `v6`. Todas as rotas e ações citadas foram
> lidas diretamente de `v6/routes/web.php`, `v6/routes/basepro.php` e dos
> controllers correspondentes.

---

## 1. Rotas do portal (público)

Arquivo: `v6/routes/web.php`, dentro do grupo
`Route::middleware(['check.site.manutention'])->group(...)` aberto na linha 8.

| Método | URI | Nome da rota | Controller@ação | Linha |
|---|---|---|---|---|
| ANY | `/tv-camara` | `portal.tv-camara.index` | `Portal\TvCamaraController@index` | `v6/routes/web.php:216` |
| GET | `/tv-camara/{slug}` | `portal.tv-camara.show` | `Portal\TvCamaraController@show` | `v6/routes/web.php:217` |

Observações:

- `Route::any` na listagem aceita GET/POST; apenas GET é usado pelo portal.
- A rota de detalhe usa `{slug}` (não `{id}`): a URL pública é
  `/tv-camara/sessao-ordinaria-de-2024`.
- Não há API JSON para o módulo em `v6/routes/api.php`.
- O `slug` não tem restrição de formato na rota: qualquer string é aceita e
  propagada para `where('slug', $slug)`.

### 1.1 Breadcrumbs

`v6/routes/breadcrumbs.php:422-435`:

| Nome | Hierarquia | URL gerada |
|---|---|---|
| `tv-camara` | Home › TV Câmara | `route('portal.tv-camara.index')` |
| `tv-camara-show` | Home › TV Câmara › título | `route('portal.tv-camara.show', [$title])` |

**Bug**: o breadcrumb da página de detalhes passa o **título** no lugar do
`slug`, gerando uma URL inválida (`/tv-camara/Título do vídeo`).

## 2. Rotas do CMS (`/cms`)

Arquivo: `v6/routes/basepro.php`, dentro de
`Route::group(['prefix' => 'cms', 'before' => 'csrf'], ...)` (linha 35).

| Método | URI completa | Nome da rota | Controller@ação | Linha |
|---|---|---|---|---|
| GET | `/cms/tv-camara` | `tv-camara.index` | `Prefecture\TvCamaraController@index` | `v6/routes/basepro.php:376` |
| POST | `/cms/tv-camara/store` | `tv-camara.store` | `Prefecture\TvCamaraController@store` | `v6/routes/basepro.php:381` |
| GET | `/cms/cadastrar/tv-camara` | `tv-camara.create` | `Prefecture\TvCamaraController@create` | `v6/routes/basepro.php:378` |
| GET | `/cms/tv-camara/{id}/edit` | `editar-tv-camara` | `Prefecture\TvCamaraController@edit` | `v6/routes/basepro.php:379` |
| POST | `/cms/tv-camara/update/{id}` | `tv-camara.update` | `Prefecture\TvCamaraController@update` | `v6/routes/basepro.php:377` |
| DELETE | `/cms/tv-camara/delete/{id}` | `tv-camara.destroy` | `Prefecture\TvCamaraController@destroy` | `v6/routes/basepro.php:380` |
| POST | `/cms/tv-camara/set-status` | `tv-camara.setStatus` | `Prefecture\TvCamaraController@setStatus` | `v6/routes/basepro.php:382` |
| POST | `/cms/ajax/tv-camara/delete-por-lote` | `ajax.tv-camara.deleteByLote` | `Prefecture\TvCamaraController@deleteByLote` | `v6/routes/basepro.php:157` |
| GET | `/cms/datatable/tv-camara` | `datatable.tv-camara` | `Prefecture\DatatableController@get_tv_camara` | `v6/routes/basepro.php:460` |

Inconsistências de nomenclatura (mantidas por compatibilidade):

- A rota de edição chama-se `editar-tv-camara`, e não `tv-camara.edit`.
- A rota de exclusão em lote usa o prefixo `ajax.` e o prefixo de URI `ajax/`.
- O `setStatus` é `POST` (não `PUT`/`PATCH`).

## 3. Middleware e autorização

| Camada | Onde | Efeito |
|---|---|---|
| `csrf` (before) | grupo `cms` em `v6/routes/basepro.php:35` | Token CSRF em todos os formulários e AJAX do CMS. |
| `check.route.permission` | construtor de `App\Http\Controllers\Prefecture\Controller` (`v6/app/Http/Controllers/Prefecture/Controller.php:20-23`) | Exige login; sem permissão, redireciona com aviso "Você não tem autorização para acessar esta funcionalidade!". |
| `check.site.manutention` | grupo do portal em `v6/routes/web.php:8` | Coloca o portal em manutenção quando configurado. |

O `check.route.permission` traduz o nome da rota em permissão:

| Rota | Permissão exigida (derivada) |
|---|---|
| `tv-camara.index` | `tv-camara.index` |
| `tv-camara.create` | `tv-camara.create` ou `tv-camara.index` |
| `editar-tv-camara` | `editar-tv-camara` (não existe na matriz de permissões) |
| `tv-camara.destroy` | `tv-camara.destroy` (idem) |
| `datatable.tv-camara` | liberada: `datatable` está em `$openRoutes` |

> **Consequência**: as rotas `tv-camara.destroy`, `tv-camara.setStatus` e
> `ajax.tv-camara.deleteByLote` exigem permissões com nomes que **não existem**
> na matriz `recurso.ação` (o resource é `tv-camara`, com ações `index`,
> `create`, `edit`, `delete`, `inactivate`, `restore`, `show`, `publish`).
> Na prática, apenas `super-admin` (que passa por `Sentinel::inRole`) consegue
> executar essas rotas. Detalhamento em `08-GAPS-E-DIVIDAS.md`.

## 4. Permissões por tela (has_access)

| Tela / botão | Verificação | Arquivo |
|---|---|---|
| Botão "Cadastrar TV Câmara" | `has_access(['tv-camara.create'])` | `v6/resources/views/prefecture/contents/tv-camara/index.blade.php:11` |
| Coluna de ações da tabela | `has_access(['tv-camara.delete'])` | `v6/resources/views/prefecture/contents/tv-camara/index.blade.php:29` |
| Checkbox "selecionar todos" | `has_access(['tv-camara.delete'])` | `v6/resources/views/prefecture/contents/tv-camara/index.blade.php:29` |
| Botão "Excluir Registro(s)" (lote) | `has_access(['tv-camaras.delete'])` | `v6/resources/views/prefecture/contents/tv-camara/index.blade.php:16` |

**Bug confirmado**: o botão de exclusão em lote verifica `tv-camaras.delete`
(com "s"), slug que não existe na matriz de permissões. Para qualquer perfil
que não seja `super-admin`, o botão nunca aparece — a exclusão em lote fica
inacessível. O `has_access()` está em `v6/app/Library/Helper/appHelpers.php:22`
e retorna `true` para `super-admin` (linha 33).

As flags de ação da linha do DataTable vêm de `struct_views()`
(`v6/app/Http/Controllers/Prefecture/DatatableController.php:504-520`), que
calcula `edit`, `delete`, `active` (`restore`) e `inactivate` com
`has_access("tv_camara.edit")`, `has_access("tv_camara.delete")`,
`has_access("tv_camara.restore")` e `has_access("tv_camara.inactivate")` —
ou seja, o prefixo **`tv_camara` com underscore**, diferente do slug real
`tv-camara` do resource. Resultado: as flags vêm sempre `false` e os botões
de editar/excluir/ativar ficam ocultos para não `super-admin`.

## 5. Form Request

Arquivo: `v6/app/Http/Requests/TvCamaraFormRequest.php`.

| Campo | Regra | Mensagem | Efeito |
|---|---|---|---|
| `title` | `required|max:120` | "O campo título é obrigatório." / "o título não deve ter mais que 120 caracteres." | Bloqueia o salvamento. |
| `publication_date` | `required|max:200` | "O campo data de publicação é obrigatório." | Bloqueia o salvamento. |
| `link` | `required|max:200` | "O campo link do vídeo é obrigatório." / "o link do vídeo não deve ter mais que 200 caracteres." | Bloqueia o salvamento. |
| `text` | (sem regra) | — | Aceita `null`; o formulário do CMS o marca como obrigatório, mas a validação não o exige. |
| `status` | (sem regra) | — | Checkbox; quando desmarcado o valor enviado é ausente/`0`. |
| `archive_highlight` | (sem regra) | — | Upload de imagem; só é processado se `hasFile('archive_highlight')` e `isValid()`. |

`authorize()` retorna `true` — a autorização real é feita pelo middleware
`check.route.permission`.

Inconsistência: o input de link no formulário declara `maxlen => 2999`
(`v6/resources/views/prefecture/contents/tv-camara/form.blade.php:68`), mas o
Form Request aceita no máximo 200 caracteres.

## 6. Ações dos controllers

### 6.1 `App\Http\Controllers\Portal\TvCamaraController`

| Ação | Comportamento |
|---|---|
| `index()` | Filtra `status = true`, `deleted_at IS NULL`, `publication_date <= now()`; ordena por `publication_date DESC`; `paginate(12)`. Renderiza `portal.layouts.{template}.tv-camara.index`. |
| `show($slug)` | `TvCamara::where('slug', $slug)->firstOrFail()` (404 se não existir). Calcula `$linkVideo = embedVideo($content->link)`. Renderiza `portal.layouts.{template}.tv-camara.show`. |

Observação: `show()` **não** filtra por `status` nem por `publication_date`:
um vídeo desativado continua acessível por URL direta.

### 6.2 `App\Http\Controllers\Prefecture\TvCamaraController`

| Ação | Comportamento |
|---|---|
| `index()` | `TvCamara::orderBy('publication_date','DESC')->get()`; renderiza `prefecture.contents.tv-camara.index` com o título "TV Câmaras". (A listagem exibida na tela é a do DataTable, alimentada pela view SQL.) |
| `create()` | Renderiza `prefecture.contents.tv-camara.create`. |
| `store()` | Aplica a regra dos 3 ativos, normaliza a data, gera o `slug`, cria o registro, cria a `archive` de destaque (`highlight = 1`) e grava o arquivo em `tv-camara/{id}/destaque`. Flash "Cadastrado com sucesso.". |
| `edit($id)` | `TvCamara::with('archives')->findOrFail($id)`; envia `$destaque` (archive com `highlight = 1`) para a view. |
| `update()` | Mesma lógica de `store`, substituindo o arquivo de destaque quando já existe (apaga o arquivo antigo com `Storage::delete`). Flash "Atualizado com sucesso.". |
| `setStatus()` | Alterna `status` entre `true` e `false` (trata `null` como desativado) usando `contents_id` do request. Retorna a string `"true"` ou `"false"`. |
| `destroy($id)` | Soft delete (`$content->delete()`). Flash "Deletado com sucesso.". |
| `deleteByLote()` | Recebe `toDelete[]`; executa `TvCamara::destroy($idsToDelete)` (soft delete em lote). Responde JSON `{"success": "Registros deletados!"}`. |

### 6.3 `App\Http\Controllers\Prefecture\DatatableController`

| Método | Comportamento |
|---|---|
| `get_tv_camara()` | `DB::table("tv_camara")->get()` → `struct_views($data, 'tv_camara')` → `DataTables::of(...)->make(true)`. |

## 7. Endpoints chamados pelo JS

| Endpoint | Método | Payload | Origem |
|---|---|---|---|
| `tv-camara/set-status` | POST | `{ contents_id: <id> }` | `v6/public/assets/js/custom/utilities/table.js:514` |
| `tv-camara/{id}/edit` | GET (link) | — | `v6/public/assets/js/custom/contents/tv-camara/datatable.js` |
| `tv-camara/delete/{id}` | DELETE | — | `v6/public/assets/js/custom/contents/tv-camara/datatable.js` |
| `ajax.tv-camara.deleteByLote` | POST | `toDelete[]` | `v6/public/assets/js/custom/utilities/deleteAll.js` |

## 8. Ausências

- Não há rota de API (`v6/routes/api.php`) nem `API Resource` para o módulo.
- Não há rota de download do vídeo: o portal apenas embute o link externo.
- Não há rota de paginação incremental (o portal usa paginação server-side do
  Eloquent; o CMS usa DataTables server-side sobre a view).
- Não há rota de "restaurar" (`restore`) para registros removidos
  (soft delete), apesar de a permissão `tv-camara.restore` existir.
