# Páginas Estáticas — Rotas e Controladores

## 1. Rotas do Portal

| Método | URI | Nome | Controller@ação | Arquivo / linha | Middleware |
|---|---|---|---|---|---|
| GET | `/pagina/{slug}` | `static.show` | `Portal\PortalController@staticShow` | `v6/routes/web.php:227` | `web`, `check.site.manutention` |
| GET | `/{slug}` | `static.show` | `Portal\PortalController@staticShow` | `v6/routes/basepro.php:548` | `web` |

Verificação com `php artisan route:list --name=static.show`:

```
| Method   | URI           | Name        | Action                                                  | Middleware                 |
| GET|HEAD | pagina/{slug} | static.show | App\Http\Controllers\Portal\PortalController@staticShow | web,check.site.manutention |
| GET|HEAD | {slug}        | static.show | App\Http\Controllers\Portal\PortalController@staticShow | web                        |
```

**Rota duplicada com o mesmo nome.** O Laravel registra as duas entradas, mas
a **última** registrada sobrescreve o nome no `RouteNameCollection`; logo,
`route('static.show', 'contato')` gera:

```
http://localhost/contato
```

(verificado em execução: `php -r` com bootstrap do Laravel no container
`v6_6f22e1bd-app-1`). Ou seja, os links gerados pelo sistema apontam para
`/{slug}`, e não para `/pagina/{slug}` — comportamento coerente com o
"Link Permanente" do CMS (`url('/') . '/' . $slug`).

### Slugs reservados

O `switch` em `PortalController::staticShow`
(`v6/app/Http/Controllers/Portal/PortalController.php:294-300`) intercepta:

| Slug | Destino |
|---|---|
| `contato` | `route('contato.index')` |
| `ouvidoria` | `route('ouvidoria.index')` |
| `manual-de-navegacao` | `route('acesso-rapido.index', 'manual-de-navegacao')` |
| `solicitar-informacao` | `route('acesso-rapido.index', 'solicitar-informacao')` |
| `acompanhe-sua-solicitacao` | `route('acesso-rapido.index', 'acompanhe-sua-solicitacao')` |
| `relatorio-estatistico` | `route('relatorio-estatistico')` |

### Rotas relacionadas

| Método | URI | Nome | Controller@ação |
|---|---|---|---|
| GET | `/acessibilidade` | `acessibilidade.index` | `Portal\AccessibilityController@getIndex` |
| GET | `/contato` | `contato.index` | `Portal\ContactController@contato` |
| GET | `/portal/{slug1}/{slug2}/{slug3}` | `conteudo.portal.view` | `Portal\PortalController@contentList` |

> A rota genérica `/portal/{slug1}/{slug2}/{slug3}` **não** tem `case
> 'paginas-estaticas'`; páginas estáticas não são acessíveis por esse
> caminho.

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

Arquivo: `v6/routes/basepro.php`, grupo `/cms` (`v6/routes/basepro.php:35`).

| Método | URI | Nome | Controller@ação | Linha |
|---|---|---|---|---|
| GET | `/cms/pagina-estatica` | `pagina-estatica.index` | `Prefecture\StaticPageController@index` | `v6/routes/basepro.php:176` |
| GET | `/cms/pagina-estatica/create` | `pagina-estatica.create` | `Prefecture\StaticPageController@create` | `v6/routes/basepro.php:176` |
| POST | `/cms/pagina-estatica` | `pagina-estatica.store` | `Prefecture\StaticPageController@store` | `v6/routes/basepro.php:176` |
| GET | `/cms/pagina-estatica/{id}` | `pagina-estatica.show` | `Prefecture\StaticPageController@show` (**método inexistente**) | `v6/routes/basepro.php:176` |
| GET | `/cms/pagina-estatica/{id}/edit` | `pagina-estatica.edit` | `Prefecture\StaticPageController@edit` | `v6/routes/basepro.php:176` |
| PUT | `/cms/pagina-estatica/{id}` | `pagina-estatica.update` | `Prefecture\StaticPageController@update` | `v6/routes/basepro.php:176` |
| DELETE | `/cms/pagina-estatica/{id}` | `pagina-estatica.destroy` | `Prefecture\StaticPageController@destroy` | `v6/routes/basepro.php:176` |
| GET | `/cms/pagina-estatica/editar/{id}` | `editar-pagina` | `Prefecture\StaticPageController@edit` | `v6/routes/basepro.php:229` |
| POST | `/cms/ajax-static-upload-files` | `ajax.static.upload.files` | `Prefecture\StaticPageController@ajaxUploadFiles` | `v6/routes/basepro.php:134` |
| POST | `/cms/ajax-static-delete-files-gallery` | `ajax.delete.static.files.gallery` | `Prefecture\StaticPageController@ajaxDeleteFilesOfGallery` | `v6/routes/basepro.php:135` |
| POST | `/cms/pagina-estatica/delete-por-lote` | `ajax.static-page.deleteByLote` | `Prefecture\StaticPageController@deleteByLote` | `v6/routes/basepro.php:148` |
| GET | `/cms/datatable/paginas_staticas` | — (sem `->name()`) | `Prefecture\DatatableController@get_paginas_estaticas` | `v6/routes/basepro.php:455` |

> A rota do datatable **não** tem `->name()`, ao contrário das outras do
> grupo `datatable.` — por isso o JS chama a URL literal.

## 3. Breadcrumbs

`v6/routes/breadcrumbs.php:186-190`:

```php
//Home > Página Estática
Breadcrumbs::register('pagina-estatica', function ($breadcrumbs, $content) {
    $breadcrumbs->parent('home');
    $breadcrumbs->push($content->title, route('static.show', $content->slug));
});
```

O breadcrumb é renderizado por
`v6/resources/views/portal/statics/show.blade.php:3-5`:

```blade
@section('breadcrumb')
    {{ Breadcrumbs::view("portal.layouts.$modelo.layout.breadcrumb", 'pagina-estatica', $content) }}
@endsection
```

> Quando a página não existe (`$null = true`), a mesma view é renderizada
> com `$content` **indefinido** -> `route('static.show', $content->slug)`
> quebraria; na prática o breadcrumb é renderizado com o `$content` nulo e
> pode gerar erro. **Não verificado em execução** (a view só é alcançada
> com `content` presente na rota normal).

## 4. Validação — `StaticPageRequest`

`v6/app/Http/Requests/StaticPageRequest.php:28-70`:

```php
public function rules(Request $request)
{
    $rules = [
        'title' => 'required',
        'slug' => [new ConditionalRequiredSlug($request->slug)],
        'link' => [new ConditionalRequiredLink($this->input('is_external'), 'Link')],
    ];

    if($request->file('archive')){
        foreach ($request->file('archive') as $file) {
            if(isset($file[0])){
                $extension = $file[0]->getClientOriginalExtension();
            } else {
                continue;
            }

            $tipos = ['rar', 'zip', 'doc', 'docx', 'pdf', 'odt', 'xls', 'xlsx', 'xlsb', 'xslm', 'ppt', 'pptx'];
            if (!in_array($extension, $tipos)) {
                $response = back()
                    ->withErrors(['archive' => "Os arquivos selecionados devem ser do tipo: rar, zip, doc, docx, pdf, odt, xls, xlsx, xlsb, xslm, ppt, pptx"])
                    ->withInput();
                throw new HttpResponseException($response);
            }
        }
    }
    return $rules;
}
```

| Regra | Tipo | Efeito |
|---|---|---|
| `title` | `required` | Título obrigatório |
| `slug` | `ConditionalRequiredSlug` | Recusa slug já usado por outra página, por uma `category_contents`, ou reservado |
| `link` | `ConditionalRequiredLink` | Exigida apenas quando o checkbox `is_external` está marcado (valor `on`) |
| `archive.*` | extensões fechadas | Erro amigável antes de gravar |

Implementação de `ConditionalRequiredSlug`
(`v6/app/Rules/ConditionalRequiredSlug.php:36-50`):

```php
public function passes($attribute, $value)
{
    $static_page = StaticPage::where('slug', $value)->first();
    $category_contents = CategoryContent::where('slug', $value)->first();
    // Caminho da URL após o domínio
    $caminho = $_SERVER['REQUEST_URI'];
    $partesDaUrl = explode("/", $caminho);
    $id =  end($partesDaUrl);

    if ($static_page['id'] != null && $static_page['id'] != $id || isset($category_contents) || !isset($value) || $value == 'diario-oficial' || $value == 'requerimento-de-diarias' || $value == 'portal-da-transparencia' || $value == 'licitacoes')  {
        return false;
    }
    return true;
}
```

Slugs **reservados** pela regra: `diario-oficial`, `requerimento-de-diarias`,
`portal-da-transparencia` e `licitacoes`. Mensagem de erro: "URL já
existente".

Implementação de `ConditionalRequiredLink`
(`v6/app/Rules/ConditionalRequiredLink.php:23-29`):

```php
public function passes($attribute, $value)
{
    if($this->checkValue != 'on'){
        return true;
    }

    return !empty($value);
}
```

Mensagem: "O campo Link é obrigatório ao marcar a opção Redirecionar para
Link Externo".

Mensagem: `'title.required' => "O campo título é obrigatório."`
(`v6/app/Http/Requests/StaticPageRequest.php:64-70`).

## 5. `StaticPageController` — métodos

| Método | Linha | Comportamento |
|---|---|---|
| `deleteByLote` | `:42-66` | Filtra `status_create = false OR NULL`; `destroyAll()`; log; resposta JSON |
| `index` | `:67-83` | Log, `menuClicks('pagina-estatica')`, view com `StaticPage::all()` (ignora soft delete? não — `all()` do modelo já exclui deletados) |
| `create` | `:85-91` | View de criação |
| `store` | `:125-180` | Transação: `upInsert` + anexos + log + `Link`; redireciona para `editar-pagina` com `#galeria` se houver galeria |
| `ajaxDeleteFilesOfGallery` | `:182-200` | `Storage::delete("/statics/{slug}/galeria/{nome}")` + `Archive::delete()` + log |
| `ajaxUploadFiles` | `:202-232` | Grava em `statics/{slug}/galeria/` com nome `img_{YmdHi}{4 rand}.{ext}` |
| `edit` | `:234-261` | Carrega `archives` e `gallery`; log de acesso |
| `update` | `:263-359` | Sanitiza a slug, faz `merge(['is_external' => bool])`, `upInsert($request, $id)`, trata `view_pdf`, anexos, log, `Link` |
| `destroy` | `:361-398` | Só exclui se `status_create == 0`; remove `menu_items`; resposta JSON |
| `extractedToArchive` | `:401-427` | Anexos do formulário -> `archives` com `static_pages_id` |

> **Rota quebrada:** `Route::resource('pagina-estatica', ...)` gera
> `GET /cms/pagina-estatica/{pagina_estatica}` com o nome `pagina-estatica.show`,
> mas `StaticPageController` **não implementa** `show()` (métodos existentes:
> `deleteByLote`, `index`, `create`, `store`, `ajaxDeleteFilesOfGallery`,
> `ajaxUploadFiles`, `edit`, `update`, `destroy`, `extractedToArchive`).
> Acessar essa URL produz erro 500 (`BadMethodCallException`). Há ainda a
> rota explícita `pagina-estatica/editar/{id}` (`editar-pagina`) usada pelo
> JavaScript da listagem.

## 6. `DatatableController::get_paginas_estaticas`

`v6/app/Http/Controllers/Prefecture/DatatableController.php:440-444`:

```php
public function get_paginas_estaticas()
{
    $paginas_estaticas = DB::table("paginas_estaticas")->get();
    return DataTables::of($this->struct_views($paginas_estaticas, 'pagina-estatica'))->make(true);
}
```

- Usa a **view SQL** `paginas_estaticas` (via `DB::table`, que **não**
  aplica `SoftDeletes` — mas a view já filtra `deleted_at IS NULL`).
- `struct_views($data, 'pagina-estatica')` acrescenta `edit`, `delete`,
  `active` e `inactivate` conforme as permissões do resource
  `pagina-estatica`.
- Não há paginação server-side: todas as páginas são carregadas.

## 7. Permissões

| Permissão | Verificação | Efeito |
|---|---|---|
| `pagina-estatica.create` | `has_access(['pagina-estatica.create'])` em `v6/resources/views/prefecture/static-pages/index.blade.php:12` | Exibe "Cadastrar Página Estática" |
| `pagina-estatica.delete` | `has_access(['pagina-estatica.delete'])` (linha 19) | Exibe a exclusão individual e em lote |
| `pagina-estatica.edit` | `has_access("$permission.edit")` (`DatatableController::struct_views`) | Habilita o botão de edição na datatable |
| `pagina-estatica.delete` / `restore` / `inactivate` | idem | Flags para o JS |

Base: `Resource` `pagina-estatica` -> `modulo_id = 28`
(`v6/database/seeds/ResourcesTableSeeder.php:270-277`;
`v6/database/migrations/2024_11_01_150206_create_modulos_table.php:192-196`).

> Atenção: o slug do **módulo** é `paginas-estaticas` (plural) e o do
> **recurso** é `pagina-estatica` (singular). As permissões usam o slug do
> recurso.

## 8. API

Este módulo **não expõe API pública**. Não há rota em `v6/routes/api.php`
para páginas estáticas.

## 9. Recursos de API

Nenhum. O módulo não usa `JsonResource`.
