# 03 — Rotas e Controladores do Módulo Diário Oficial

> Rotas conferidas em `v6/routes/basepro.php` e `v6/routes/web.php`.

---

## 1. Grupos de rota

| Arquivo | Group | Prefixo efetivo |
|---------|-------|------------------|
| `v6/routes/basepro.php:35` | `Route::group(['prefix' => 'cms', 'before' => 'csrf'])` | `/cms` |
| `v6/routes/basepro.php:114` | `Route::group(['prefix' => 'ajax'])` (dentro de `cms`) | `/cms/ajax` |
| `v6/routes/basepro.php:396` | `Route::group(['prefix' => 'datatable', 'as' => 'datatable.'])` | `/cms/datatable` |
| `v6/routes/web.php:196` | portal (raiz do site) | `/diario-oficial` |

Middleware relevantes:

- `check.logged` — `v6/app/Http/Middleware/CheckLogged.php`; exige usuário logado.
- `check.route.permission` — `v6/app/Http/Middleware/CheckRoutePermission.php`;
  valida a permissão Sentinel do nome da rota.
- `csrf` — aplicado via `before` no grupo `/cms`.

## 2. Rotas do CMS (`v6/routes/basepro.php`)

### 2.1 Resource `diario-oficial` → `Prefecture\DiarioController`

| Método | URI | Nome | Controller@action |
|--------|-----|------|-------------------|
| GET | `/cms/diario-oficial` | `diario-oficial.index` | `index` |
| GET | `/cms/diario-oficial/create` | `diario-oficial.create` | `create` |
| POST | `/cms/diario-oficial` | `diario-oficial.store` | `store` |
| GET | `/cms/diario-oficial/{id}` | `diario-oficial.show` | `show` (stream do PDF ou JSON de status) |
| GET | `/cms/diario-oficial/{id}/edit` | `diario-oficial.edit` | `edit` |
| PUT/PATCH | `/cms/diario-oficial/{id}` | `diario-oficial.update` | `update` |
| DELETE | `/cms/diario-oficial/{id}` | `diario-oficial.destroy` | `destroy` |

Rota extra:

| Método | URI | Nome | Action |
|--------|-----|------|--------|
| PUT | `/cms/diario-oficial/{id}/publish` | `diario-oficial.publish` | `publish` |
| DELETE | `/cms/diario-oficial/{id}/cover-image` | `diario-oficial.delete.cover-image` | `delete_cover_image` |

### 2.2 Resource `certificado` → `Prefecture\DiarioCertificadosController`

| Método | URI | Nome |
|--------|-----|------|
| GET | `/cms/certificado` | `certificado.index` |
| GET | `/cms/certificado/create` | `certificado.create` |
| POST | `/cms/certificado` | `certificado.store` |
| GET | `/cms/certificado/{id}` | `certificado.show` |
| GET | `/cms/certificado/{id}/edit` | `certificado.edit` |
| PUT/PATCH | `/cms/certificado/{id}` | `certificado.update` |
| DELETE | `/cms/certificado/{id}` | `certificado.destroy` |

### 2.3 Resource `area` → `Prefecture\DiarioAreaController`

| Método | URI | Nome |
|--------|-----|------|
| GET | `/cms/area` | `area.index` |
| GET | `/cms/area/create` | `area.create` |
| POST | `/cms/area` | `area.store` |
| GET | `/cms/area/{id}` | `area.show` |
| GET | `/cms/area/{id}/edit` | `area.edit` |
| PUT/PATCH | `/cms/area/{id}` | `area.update` |
| DELETE | `/cms/area/{id}` | `area.destroy` |

O mesmo controller trata **áreas** (quando chega `name`) e **matérias**
(quando chega `conteudo`/`titulo`/`file`). A edição de matéria usa o sufixo
`_modal` no id, por exemplo `/cms/area/15_modal/edit`.

### 2.4 Resource `subarea` → `Prefecture\DiarioSubAreaController`

| Método | URI | Nome |
|--------|-----|------|
| GET | `/cms/subarea` | `subarea.index` |
| GET | `/cms/subarea/create` | `subarea.create` |
| POST | `/cms/subarea` | `subarea.store` |
| GET | `/cms/subarea/{id}` | `subarea.show` |
| GET | `/cms/subarea/{id}/edit` | `subarea.edit` |
| PUT/PATCH | `/cms/subarea/{id}` | `subarea.update` |
| DELETE | `/cms/subarea/{id}` | `subarea.destroy` |

### 2.5 Rotas específicas (grupo `ajax`)

| Método | URI | Nome | Action | Middleware |
|--------|-----|------|--------|------------|
| GET | `/cms/ajax/dados-diario/{id}` | `dados_diario` | `dados_diario` | `check.logged` |
| POST | `/cms/ajax/diario-oficial` | `diario.destroy` | `destroy` | — |
| PUT | `/cms/ajax/diario-oficial/{id}/sort` | `diario.sort` | `sort` | — |
| GET | `/cms/ajax/diario-generation-status/{id}` | `diario.generation-status` | `generationStatus` | `check.logged` |

### 2.6 Rotas específicas (fora do grupo `ajax`)

| Método | URI | Nome | Action |
|--------|-----|------|--------|
| POST | `/cms/diario/destroyAll` | `diario.destroyAll` | `destroyAll` |
| POST | `/cms/area/destroyAll` | `area.destroyAll` | `destroyAll` |
| POST | `/cms/subarea/destroyAll` | `subarea.destroyAll` | `destroyAll` |
| POST | `/cms/diario/status` | (sem nome) | `toogle_status` |
| GET | `/cms/diario/configuracoes` | `diario-config.index` | `get_configuration` |
| PUT | `/cms/diario/configuracoes/update` | `diario-config.update` | `update_configuration` |
| DELETE | `/cms/diario/configuracoes/header-img/{id}` | `diario-config.delete.header-img` | `delete_header_img` |
| DELETE | `/cms/diario-area/{id}` | (sem nome) | `destroyAreas` |

> ⚠️ **Rotas duplicadas** em `v6/routes/basepro.php`: `diario/configuracoes`,
> `diario/configuracoes/update` e `diario-area/{id}` são declaradas **duas
> vezes** (blocos nas linhas 255-259 e 261-263). A segunda declaração é
> inofensiva (mesmo nome e mesmo action), mas é dívida técnica.

### 2.7 DataTables

| Método | URI | Nome | Action | View PostgreSQL |
|--------|-----|------|--------|-----------------|
| GET | `/cms/datatable/diario_oficial` | `datatable.diario_oficial` | `DatatableController@get_diario_oficial` | `diario_oficial` |
| GET | `/cms/datatable/areas_diario` | `datatable.areas_diario` | `DatatableController@get_areas_diario` | `areas_do_diario` |
| GET | `/cms/datatable/sub_areas_diario` | `datatable.sub_areas_diario` | `DatatableController@get_sub_areas_diario` | `sub_areas_do_diario` |

### 2.8 Rotas de refresh/preview (declaradas em `v6/routes/web.php`)

| Método | URI | Nome | Action |
|--------|-----|------|--------|
| GET | `/refresh-diario/{id}` | `refresh-PDF` | `Prefecture\DiarioController@refresh_PDF` |
| GET | `/refresh-diario-preview/{id}` | `refresh-PDF-previe` | `Prefecture\DiarioController@refresh_PDF_preview` |
| GET | `/loading-diario-preview` | `loading-diario-preview` | `Prefecture\DiarioController@loading` |

> Estas rotas do CMS estão registradas no arquivo do **portal** e **não** têm
> prefixo `/cms`, nem exigem `check.logged` no grupo. `refresh_PDF_preview`
> aceita `?force=1` para regerar mesmo com status `completed`.

## 3. Rotas do portal (`v6/routes/web.php`)

| Método | URI | Nome | Action | Retorno |
|--------|-----|------|--------|---------|
| GET | `/diario-oficial` | `portal-diario.index` | `Portal\DiarioController@index` | Lista paginada (10 por página) |
| GET | `/diario-oficial/viewer/search` | `portal-diario.search` | `search` | JSON de edições (máx. 20) |
| GET | `/diario-oficial/viewer/{id}/file-info` | `portal-diario.file-info` | `fileInfo` | JSON com tamanho e nº de páginas |
| GET | `/diario-oficial/viewer/{id}/search-pdf` | `portal-diario.search-pdf` | `searchPdf` | JSON com páginas e trechos |
| GET | `/diario-oficial/{id}` | `portal-diario.show` | `showPDF` | Viewer do PDF |
| GET | `/diario-oficial/{id}/auditoria` | `portal-diario.auditoria` | `auditoria` | Trilha de auditoria |
| POST | `/diario-oficial/filtro` | `portal-diario.fill` | `filtroGET` | Coleção de edições filtradas |

Filtros aceitos por `filtroGET`: `data_inicial`/`data_ini`, `data_final`/`data_fim`,
`termo`, `edicao`. Apenas edições com `status = true` aparecem no portal.

Breadcrumbs em `v6/routes/breadcrumbs.php`: nome `diario-oficial` (portal),
com o item "Diário Oficial" apontando para `portal-diario.index`.

## 4. Form Requests

| Request | Regras | Quem usa |
|---------|--------|----------|
| `DiarioOficialRequest` | `edicao` required; `autor` required; `email` required; `data_publicacao` required; `lei_validadora` nullable; `id_certificado` → `SelectRule`; `columns_number` nullable numeric in 1,2,3; `line_style` nullable in solid,dashed,dotted; `line_color` nullable regex `^#[0-9A-Fa-f]{6}$`; `archive_highlight` nullable image max 5120 KB | `DiarioController@store` e `@update` |
| `DiarioAreasRequest` | `name` required | `DiarioSubAreaController@store`/`@update` |
| `SaveAreaRequest` | `conteudo` nullable; `titulo` nullable; `id_area` nullable exists `diario_areas,id`; `id_subarea` nullable exists `diario_sub_areas,id`; `id_diario` nullable exists `diarios,id`; `type` nullable in text,file; `file` nullable file mimes pdf max 5120 required_if type,file | `DiarioAreaController@store`/`@update` (matérias) |
| `SortRequest` | `id` required integer exists `diarios,id`; `areas` required array; `areas.*.id` required integer exists `diario_has_areas,id`; `areas.*.position` required integer | `DiarioController@sort` |
| `SaveConfigRequest` | `ativos` required boolean; `lei_padrao` required; `certificado_padrao` required exists `diario_certificados,id`; `logo_position` required in center,left,right; `header_img` file mimes jpeg,jpg,png max 5120 | `DiarioController@update_configuration` |
| `CertificadoRequest` | `certificado` required; `password` required | `DiarioCertificadosController@store` |

`SortRequest`, `SaveAreaRequest` e `SaveConfigRequest` sobrescrevem
`failedValidation()` para responder **JSON 422** em vez de redirecionar.

## 5. API Resources (formato de saída)

`GET /cms/ajax/dados-diario/{id}` devolve `GetDiarioResource`:

```json
{
  "id": 12,
  "areas": [ { "id": 3, "name": "EDITAIS E AVISOS" } ],
  "contents": [
    { "id": 88, "title": "Portaria nº 123", "content": "<p>…</p>",
      "columns_number": 1, "id_area": 3, "id_subarea": 1 }
  ]
}
```

- `GetDiarioResource` (`v6/app/Http/Resources/DiarioOficial/GetDiarioResource.php`)
- `ListAreasAndSubareasResource` (`v6/app/Http/Resources/DiarioOficial/ListAreasAndSubareasResource.php`)
- `ListAreaContentResource` (`v6/app/Http/Resources/DiarioOficial/ListAreaContentResource.php`)

> A resource **não** expõe `path`, `type`, `file_in` nem `position`: o modal de
> matérias depende do HTML renderizado no Blade, não do JSON.

## 6. Controladores

### 6.1 `Prefecture\DiarioController` (`v6/app/Http/Controllers/Prefecture/DiarioController.php`, 648 linhas)

| Método | Comportamento |
|--------|---------------|
| `__construct(IDiarioConfigRepository)` | Aplica `check.route.permission` **exceto** `dados_diario`, `destroyAreas` e `generationStatus`; chama `menuClicks('diario-oficial')` |
| `index()` | View `prefecture.contents.diario-oficial.index` |
| `create()` | View `create` com áreas, subáreas, próxima edição (`max(edicao) + 1`), lista de edições, certificados e `preinfo` (configurações) |
| `store(DiarioOficialRequest)` | Salva, anexa capa, marca `pending`, loga atividade, redireciona para `edit` |
| `show($id, Request)` | `$force = !in_array(generation_status, ['completed','processing'])`; chama `generatePDF($id, $force, true, download=1)` |
| `edit($id)` | View `create` com os dados da edição |
| `update(DiarioOficialRequest, $id)` | Salva, anexa capa, `pending`, compara campos e loga a alteração campo a campo; `acao = enviar_revisao` move o workflow para "em revisão" |
| `publish($id)` | Workflow de 4 olhos (ver seção 7) |
| `toogle_status(Request)` | Alterna `status`; respeita a chave de configuração `diario_oficial_segregacao_funcoes`; **lê `contents_id`** (nome legado de outro módulo) |
| `destroy($id)` / `destroyAll(Request)` | Soft delete, resposta JSON |
| `destroyAreas($id)` | Soft delete de uma matéria (`diario_has_areas`) |
| `dados_diario($id)` | `GetDiarioResource` |
| `sort($id, SortRequest)` | Atualiza `position` das matérias em transação; loga "Reordenação de matérias"; marca `pending` |
| `get_configuration()` / `update_configuration(SaveConfigRequest)` / `delete_header_img($id)` | Configurações globais e imagem de cabeçalho |
| `refresh_PDF($id)` | Regera o PDF e redireciona para a listagem |
| `refresh_PDF_preview($id, Request)` | Geração síncrona com status `processing` → `completed`/`failed` |
| `generationStatus($id)` | JSON de polling; marca `failed` se `processing` há mais de 10 minutos |
| `delete_cover_image($id)` | Remove o `Archive` da capa e zera `cover_image_id` (transação) |
| `loading()` | View de loading do preview |

### 6.2 `Prefecture\DiarioAreaController` (`v6/app/Http/Controllers/Prefecture/DiarioAreaController.php`)

CRUD de áreas **e** de matérias no mesmo controller. A distinção é feita pelo
conteúdo do request: `name` presente → área; `conteudo`/`titulo`/`file`
presente → matéria. O método `processContent()` percorre o HTML do Summernote,
localiza imagens em base64 ou referenciadas por URL, faz upload e compressão.

### 6.3 `Prefecture\DiarioSubAreaController` (`v6/app/Http/Controllers/Prefecture/DiarioSubAreaController.php`)

CRUD simples de subáreas, validado por `DiarioAreasRequest`.

### 6.4 `Prefecture\DiarioCertificadosController` (`v6/app/Http/Controllers/Prefecture/DiarioCertificadosController.php`)

No `store`: valida o arquivo enviado, gera o CRT com `openssl pkcs12`, confere
`openssl_x509_read` e a presença de CN; em falha faz `forceDelete` do registro,
limpa o diretório e registra log. Métodos implementados: `index`, `create`,
`store`, `destroy` (as demais actions do resource não têm implementação).

### 6.5 `Prefecture\DatatableController` (`v6/app/Http/Controllers/Prefecture/DatatableController.php`)

Fornece os dados das três listagens do CMS e monta os botões de ação conforme
as permissões do usuário (`struct_views`).

### 6.6 `Portal\DiarioController` (`v6/app/Http/Controllers/Portal/DiarioController.php`, 450 linhas)

| Método | Comportamento |
|--------|---------------|
| `index()` | `paginate(10)`, `orderBy('edicao','DESC')`, eager load `certificado` e `complemento` |
| `showPDF($id)` | Monta o caminho do PDF; navegação por **número da edição** (`<` / `>`), não por data; se o arquivo não existe, marca `generation_status = 'pending'` e mostra a tela de espera |
| `logPortalView($diario)` | Grava "Visualização no portal" na trilha de auditoria, **no máximo uma vez por edição por sessão** (chave `diario_portal_view_{id}`) |
| `search(Request)` | `ILIKE` em `conteudo`, `titulo`, `edicao`, `autor`, `lei_validadora`; mínimo de 3 caracteres; limite de 20 |
| `fileInfo($id)` | Tamanho do arquivo e contagem de páginas via `preg_match_all('/\/Page\W/', $content)` |
| `searchPdf(Request, $id)` | Extrai o texto do PDF com Ghostscript (`txtwrite`), normaliza acentos e devolve páginas, ocorrências e trechos |
| `extractPdfText($pdfPath, $diario)` | Cache JSON em `storage/app/diario-oficial/pdf-text/`, invalidado pelo `mtime` do PDF; copia o PDF para um nome ASCII antes de chamar o Ghostscript |
| `auditoria($id)` | Trilha pública via `getPublicAuditTrail($id)` |
| `filtroGET(Request)` | Filtra por datas, termo e edição; só `status = true`; adiciona `valorExtenso` (data por extenso) |

## 7. Permissões exigidas

### 7.1 Menu (`v6/app/Providers/MenuServiceProvider.php:215`)

| Item | Permissão |
|------|-----------|
| "Diários" → `/cms/diario-oficial` | `diario-oficial.index` |
| "Áreas do Diário" → `/cms/area` | `diario-area.index` |
| "Subáreas do Diário" → `/cms/subarea` | `diario-subarea.index` |

### 7.2 Papéis (seed `v6/database/seeds/AddDiarioOficialWorkflowRolesSeeder.php`)

| Papel | Permissões |
|-------|-----------|
| `redator-diario-oficial` | `diario-oficial.index`, `.create`, `.edit`, `.show`, `diario-area.index`, `.create`, `.edit`, `.delete`, `diario-subarea.index`, `.create`, `.edit`, `.delete` |
| `revisor-diario-oficial` | `diario-oficial.index`, `diario-oficial.publish`, `diario-oficial.show` |

A permissão `diario-oficial.publish` é criada por
`v6/database/seeds/AddDiarioOficialPublishPermissionSeeder.php`.

### 7.3 Ações nos DataTables (`DatatableController::struct_views`)

| Ação | Chave | Regra |
|------|-------|-------|
| Editar | `{permission}.edit` | `diario-oficial` / `diario-area` |
| Excluir diário | — | papel `super-admin` **ou** `administrador` |
| Excluir área/subárea | `{permission}.delete` | `diario-area` |
| Ativar | `{permission}.restore` | — |
| Inativar | `{permission}.inactivate` | — |

> ⚠️ `get_sub_areas_diario()` resolve a permissão com `struct_views($data, 'diario-area')`,
> ou seja, **subáreas são autorizadas com `diario-area.*`**. Ver `08-GAPS-E-DIVIDAS.md`.

### 7.4 Regras de workflow (dentro do controller)

| Regra | Consequência |
|-------|-------------|
| Só `status_workflow = 2` (em revisão) pode ser publicado | Caso contrário, HTTP 422 |
| Quem criou (`created_by_user_id`) ou atualizou (`updated_by_user_id`) a edição não pode publicá-la | HTTP 403 |
| Publicar define `status = true`, `permission_edit = false`, `published_by_user_id` e `published_at` | — |
| `toogle_status` só reativa quando a segregação está desligada **ou** a edição já está publicada | Senão devolve `false` |
