# A Cidade — Rotas e Controladores

> Versão analisada: `v6`. Todas as URIs são relativas à raiz do domínio.

---

## 1. Rotas do portal público (`v6/routes/web.php`)

```php
// v6/routes/web.php:85-87
Route::prefix('a-cidade')->group(function () {
    Route::any('/{slug}', 'Portal\AboutController@getIndex')->name('a-cidade.index');
});
```

| Método | URI | Nome | Controller@ação | Middleware |
|---|---|---|---|---|
| ANY | `/a-cidade/{slug}` | `a-cidade.index` | `Portal\AboutController@getIndex` | `check.site.manutention` (grupo de `v6/routes/web.php:7`) |

Observações:

- O verbo é `ANY` — GET, POST, PUT e DELETE chegam ao mesmo método. Não há
  proteção CSRF no portal para esta rota.
- **Uma única rota.** Não há rotas de listagem, de categorias, de subpáginas,
  de galeria ou de download. A listagem pública de "A Cidade" é feita pelo
  menu/busca, não por um índice do módulo.
- A rota não tem restrição de parâmetro: `{slug}` aceita qualquer string, e o
  controller devolve a view de erro quando não encontra o conteúdo.
- A v5 tinha **exatamente** a mesma rota (`v5/routes/web.php:76-78`), com o
  mesmo nome e o mesmo controller. O prefixo `a-cidade` **não** foi criado na
  v6 — ele já existia na v5.

## 2. Participação do módulo em outras rotas do portal

O módulo é referenciado por rotas genéricas de navegação e busca:

| Método | URI | Nome | Onde o módulo aparece |
|---|---|---|---|
| GET | `/portal/{slug1}/{slug2}` | `conteudo.portal.show` | `v6/app/Http/Controllers/Portal/PortalController.php:330` — `case 'a-cidade': return redirect(route('a-cidade.index', $slug2));` |
| GET | `/portal/{slug}` | `conteudo.portal.list` | idem |
| POST | `/portal/busca/conteudo` | `busca.por.categoria` | `v6/app/Http/Controllers/Portal/PortalController.php:251` e `:265` — o array fixo de slugs categories é `['noticias', 'a-cidade', 'a-instituicao', 'glossario', 'publicacoes', 'legislacao', 'perguntas-frequentes']` |

Ou seja, o conteúdo de A Cidade **aparece na busca geral do portal** porque o
slug da categoria está nesse array — e o resultado da busca é filtrado por
categoria, ao contrário da página `/a-cidade/{slug}`. O mesmo array fixo
aparece em `v6/app/Http/Filter/FilterSearchAllSite.php:29`, que resolve as
categorias do filtro "todos" da busca.

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

Todas sob `Route::group(['prefix' => 'cms', 'before' => 'csrf'], ...)`.

| Método | URI | Nome | Controller@ação | Middleware |
|---|---|---|---|---|
| GET | `/cms/conteudos/a-cidade` | `listar-conteudo` | `Prefecture\ContentController@genericIndex` | `check.route.permission` |
| GET | `/cms/conteudos/cadastrar/a-cidade` | `cadastrar-conteudo` | `Prefecture\ContentController@genericCreate` | idem |
| GET | `/cms/conteudos/editar/a-cidade/{id}` | `editar-conteudo` | `Prefecture\ContentController@genericEdit` | idem |
| POST | `/cms/conteudo` | `conteudo.store` | `Prefecture\ContentController@store` | idem |
| PUT | `/cms/conteudo/{id}` | `conteudo.update` | `Prefecture\ContentController@update` | idem |
| DELETE | `/cms/conteudo/{id}` | `conteudo.destroy` | `Prefecture\ContentController@destroy` | idem |
| DELETE | `/cms/conteudo/delete/{id}` | `generic.conteudo.delete` | `Prefecture\ContentController@genericDestroy` | idem |
| POST | `/cms/ajax/generic/content/deleteAll` | `ajax.generic.content.deleteAll` | `Prefecture\ContentController@genericDeleteAll` | idem |
| GET | `/cms/datatable/a_cidade` | `datatable.a_cidade` | `Prefecture\DatatableController@get_a_cidade` | idem |

Trechos reais:

```php
// v6/routes/basepro.php:174
Route::resource('conteudo', 'Prefecture\ContentController');

// v6/routes/basepro.php:235-237
Route::get('conteudos/{slug}', ['uses' => 'Prefecture\ContentController@genericIndex'])->name('listar-conteudo');
Route::get('conteudos/cadastrar/{slug}', ['uses' => 'Prefecture\ContentController@genericCreate'])->name('cadastrar-conteudo');
Route::get('conteudos/editar/{slug}/{id}', ['uses' => 'Prefecture\ContentController@genericEdit'])->name('editar-conteudo');

// v6/routes/basepro.php:396,414
Route::group(['prefix' => 'datatable', 'as' => 'datatable.'], function () {
    // ...
    Route::get('/a_cidade', 'Prefecture\DatatableController@get_a_cidade')->name('a_cidade');
```

O módulo **não tem rota de criação/edição própria**: o `Route::resource('conteudo')`
é genérico e serve ~30 módulos de conteúdo. O slug da URL é o que decide a view
e as regras de validação.

> Os links gerados pela grade são **relativos** (`cadastrar/a-cidade`,
> `editar/a-cidade/{id}`) e resolvem contra a URL atual
> (`/cms/conteudos/a-cidade`) para `/cms/conteudos/cadastrar/a-cidade` e
> `/cms/conteudos/editar/a-cidade/{id}`. Funciona, mas é frágil: muda a URL
> base e os botões quebram.

## 4. Rotas da API de dados abertos

**Não existe endpoint para A Cidade.** `v6/routes/api.php` expõe apenas:

| Prefixo | Controller |
|---|---|
| `v1/secretarias` | `Swagger\V1\Portal\SecretariaController@index` |
| `v1/orgaos` | `Swagger\V1\Portal\OrgaoController@index` |
| `v1/legislacoes` | `Swagger\V1\Portal\LegislationController@index` |
| `v1/noticias` | `Swagger\V1\Portal\NewController@index` |
| `v1/publicacoes` | `Swagger\V1\Portal\PublicationController@index` |

Não há `v1/a-cidade`, nem `v1/instituicoes`. O conteúdo institucional do
município é público na web, mas **não** está disponível em formato de dados
abertos — lacuna registrada em `08-GAPS-E-DIVIDAS.md`.

## 5. Breadcrumbs (`v6/routes/breadcrumbs.php`)

```php
// v6/routes/breadcrumbs.php:265-270
// Home > A Cidade >
Breadcrumbs::register('a-cidade', function ($breadcrumbs, $content) {
    $breadcrumbs->parent('home');
    $breadcrumbs->push('A Cidade', url('/#'));
    $breadcrumbs->push($content->title);
});
```

O nome do registro de breadcrumb coincide com o da categoria (`a-cidade`) e é
resolvido pela view:

```blade
{{-- v6/resources/views/portal/layouts/modelo1/informacoes-instituicao/acidade/show.blade.php:4 --}}
{{ Breadcrumbs::view('portal.layouts.modelo1.layout.breadcrumb', $contentNew->categoryContents->slug, $contentNew) }}
```

Ou seja, o breadcrumb é escolhido **pelo slug da categoria do conteúdo**, e não
por um literal `a-cidade`. Um conteúdo de outra categoria servido em
`/a-cidade/{slug}` vai usar o breadcrumb **dela** — outro sintoma da falta de
filtro por categoria.

`url('/#')` no primeiro nível é um **link morto** (leva à home).

## 6. Controladores

### 6.1 `Portal\AboutController` (portal público)

Arquivo: `v6/app/Http/Controllers/Portal/AboutController.php` (45 linhas).

```php
// v6/app/Http/Controllers/Portal/AboutController.php:18-43
class AboutController extends Controller
{
    use GetInformationsPrefectureTrait;

    public function getIndex($slug)
    {
        $contentNew = $this->getContentDataWith($slug);

        if (!$contentNew) return view("admin.errors.conteudo-nao-encontrado");

        $contentNew->load('linksUnique');
        $linkVideo = isset($contentNew->linksUnique) ? embedVideo($contentNew->linksUnique->link): null;
        $title = $contentNew->title;
        $this->updateClicks($contentNew->id);

        return view(
            'portal.layouts.' . $this->getTemplateActived() . '.informacoes-instituicao.acidade.show',
            compact('contentNew', 'title', 'linkVideo')
        );
    }
}
```

Pontos verificados:

| Item | Constatação |
|---|---|
| Docblock | `"Classe criada com objetivo de disponibilizar a tela de notícias."` — **copiado** do controller de notícias; não descreve o módulo. |
| `use App\Http\Traits\NewsTrait;` | Importado mas **nunca usado** (o controller usa apenas `GetInformationsPrefectureTrait`). |
| `use Illuminate\Support\Facades\Route;` | Importado mas **não usado**. |
| Erro de conteúdo ausente | `view("admin.errors.conteudo-nao-encontrado")` → **HTTP 200**, não 404. O arquivo existe em `v6/resources/views/admin/errors/conteudo-nao-encontrado.blade.php`. |
| Variáveis para a view | `contentNew`, `title`, `linkVideo`. A view do layout `default` espera `$content` — incompatível (ver `04-VIEWS-E-UX.md`). |

### 6.2 `GetInformationsPrefectureTrait` (trait compartilhado)

Arquivo: `v6/app/Http/Traits/GetInformationsPrefectureTrait.php`.

| Método | Linha | Papel no módulo |
|---|---|---|
| `getContentDataWith($param)` | `:23-26` | **Consulta central.** `Content::with('archiveHighlight','archiveGallery','categoryContents:id,name,slug')->where('slug',$param)->first()` — filtra **apenas por slug do conteúdo**. |
| `getTemplateActived()` | `:92-102` | Lê `configurations.layout`; aborta 404 e liga `pagina_em_manutencao` se faltar `portal.layouts.<layout>.home.index`. |
| `updateClicks($idContent)` | `:104-108` | `SELECT` + `UPDATE contents SET clicks = clicks + 1` — **duas queries** por visita. |

```php
// v6/app/Http/Traits/GetInformationsPrefectureTrait.php:23-26
public function getContentDataWith($param)
{
    return  Content::with('archiveHighlight', 'archiveGallery', 'categoryContents:id,name,slug')->where('slug', $param)->first();
}
```

```php
// v6/app/Http/Traits/GetInformationsPrefectureTrait.php:104-108
public function updateClicks($idContent)
{
    $content = Content::where('id', $idContent)->first();
    Content::where('id', $content->id)->update(['clicks' => $content->clicks + 1]);
}
```

O `updateClicks()` não é atômico: `SELECT` e `UPDATE` são separados. Duas
visitas simultâneas para o mesmo conteúdo podem **perder** um incremento.

### 6.3 `Prefecture\ContentController` (CMS — CRUD)

Arquivo: `v6/app/Http/Controllers/Prefecture/ContentController.php`.

| Método | Linha | Papel no módulo |
|---|---|---|
| `__construct` | `:58-70` | Aplica `check.route.permission`; instancia `Content`, `CategoryContent`, `Archive`, `TypesSizes`; injeta `IContentRepository`. |
| `genericIndex($slug)` | `:89` | `if ($slug === 'a-cidade') return $this->returnIndex('A Cidade', $slug);` |
| `returnIndex($title, $slug, $alt_click)` | `:121-126` | `generateLog()`, `menuClicks($slug)`, `view("prefecture.contents.$slug.index")`. |
| `genericCreate($slug)` / `genericEdit($slug, $id)` | `:140`, `:155` | Delegam para `genericGenerate()`. |
| `genericGenerate($slug, $id)` | `:166` | `if ($slug === 'a-cidade') return $this->returnData('A Cidade', $slug, null, [], $id);` |
| `returnData($title, $slug, $self, $extra, $id)` | `:275-...` | Monta `categoryContent`, `content`, `gallery`, `archives`, `category`, `destaque`; escolhe a view `create` ou `edit`. |
| `store(ContentRequest)` | `:616` | Transação; slug com colisão `-<max(id)+1>`; grava `contents`; cria `links` (vídeo) e `archives` (destaque e galeria); move imagens do Summernote. |
| `update(ContentRequest, $id)` | `:937` | Atualiza o conteúdo e refaz `link`/`archives` (`:1063-1110`). |
| `genericDestroy($id)` | `:1369` | Soft delete + apaga os `menu_items` que referenciam o conteúdo. |
| `genericDeleteAll(Request)` | `:70-78` | Exclusão em lote via `IContentRepository::destroyAll()`. |

### 6.4 `Prefecture\DatatableController` (CMS — listagem)

Arquivo: `v6/app/Http/Controllers/Prefecture/DatatableController.php`.

```php
// :246-250
public function get_a_cidade()
{
    $acidade = DB::table('a_cidade')->get();
    return DataTables::of($this->struct_views($acidade, 'a-cidade'))->make(true);
}
```

`struct_views()` (`:504-520`) injeta por linha: `edit`, `delete`, `active`
(`restore`) e `inactivate`, todos via `has_access()`.

## 7. Form Requests e validação

`v6/app/Http/Requests/ContentRequest.php` — o `switch ($slug)` decide as
regras. O caso de A Cidade é **compartilhado** com `orgaos`:

```php
// v6/app/Http/Requests/ContentRequest.php:205-214
case 'orgaos':
case 'a-cidade':
    $rules = [
        "title" => "required|max:120",
        "category_contents_id" => "required|numeric",
        "text" => "required",
        "link" => "max:2999",
        "archive_highlight" => "max:20000|mimes:jpg,jpeg,png,gif,webp",
    ];
    break;
```

| Campo | Regra | Mensagem (de `messages()`) |
|---|---|---|
| `title` | `required|max:120` | "Por favor, informe um título." / "O campo título ultrapassou o número máximo de caracteres permitidos." |
| `category_contents_id` | `required|numeric` | "Não foi possível recuperar a categoria selecionada, tente novamente." |
| `text` | `required` | "Por favor, digite algum texto." |
| `link` | `max:2999` | "O campo URL ultrapassou o número máximo de caracteres permitidos." |
| `archive_highlight` | `max:20000|mimes:jpg,jpeg,png,gif,webp` | "Por favor, selecione uma imagem com tamanho menor que 20MB." |

Observações:

- `authorize()` devolve `true` incondicionalmente
  (`v6/app/Http/Requests/ContentRequest.php:19-22`); a autorização real vem do
  middleware `check.route.permission`.
- O `$slug` que comanda o `switch` **não vem da rota**: é derivado de
  `category_slug` da requisição ou de `CategoryContent::find($request->get('category_contents_id'))->slug`
  (`v6/app/Http/Requests/ContentRequest.php:32-40`).
- `category_contents_id` é validado apenas como numérico — **não** é validado
  contra a lista de categorias permitidas. Um operador pode gravar conteúdo em
  qualquer categoria pela rota `POST /cms/conteudo`.
- Não há validação de unicidade do `slug` nem de formato do vídeo: o campo
  `link` aceita qualquer texto até 2999 caracteres. A "validação" de YouTube é
  apenas a mensagem do formulário (`v6/resources/views/prefecture/contents/a-cidade/form.blade.php:70`).
- `galery` (checkbox) não tem regra: pode chegar qualquer valor.

## 8. Permissões exigidas

| Permissão | Onde é exigida |
|---|---|
| `a-cidade.index` | Menu lateral do CMS (`v6/app/Providers/MenuServiceProvider.php:179-180`) e `check.route.permission` em `GET /cms/conteudos/a-cidade`. |
| `a-cidade.create` | Botão "Cadastrar Conteúdo" (`v6/resources/views/prefecture/contents/a-cidade/index.blade.php:11-14`). |
| `a-cidade.edit` | Botão de edição por linha (via `struct_views`). |
| `a-cidade.delete` | Seleção múltipla e botão de exclusão por linha. |
| `a-cidade.restore` / `.inactivate` | Flags `active` / `inactivate` por linha. |

### 8.1 Como o middleware resolve a permissão

```php
// v6/app/Http/Middleware/CheckRoutePermission.php:41-100
public function hasAccess()
{
    $resourceRoutes = ['edit' => 'update', 'create' => 'store', 'delete' => 'destroy'];
    $openRoutes = ['login', 'logout', 'datatable', 'dashboard', 'error', 'ajax', 'info-request-status-update'];
    $access = Route::getCurrentRoute()->getName();
    // ...
    if (str_contains($accessSegment, '_')) $accessSegment = str_replace('_', '-', $accessSegment);
    return Sentinel::check()->hasAnyAccess([$access, $accessSegment]);
}
```

Para `GET /cms/conteudos/a-cidade`, o nome da rota é `listar-conteudo` e o
segmento 3 da URL é `a-cidade` → `hasAccessSegment()` devolve `a-cidade.index`.
Para `GET /cms/conteudos/cadastrar/a-cidade` → `a-cidade.create`; para
`GET /cms/conteudos/editar/a-cidade/{id}` → `a-cidade.edit`.

O middleware é herdado de `Prefecture\Controller`:

```php
// v6/app/Http/Controllers/Prefecture/Controller.php:20-24
public function __construct()
{
    $this->middleware('check.route.permission');
}
```

`ContentController` e `DatatableController` estendem essa base — logo **todas**
as rotas do CMS do módulo passam pelo middleware.

### 8.2 Dupla chave de exibição no menu do CMS

```php
// v6/app/Providers/MenuServiceProvider.php:46
if (has_access("$menu->slug.index") && isset($menu->modulo->status) && $menu->modulo->status) {
```

O módulo precisa de **duas** condições: a permissão `a-cidade.index` **e**
`modulos.status = true`. Na base atual:

| `modulos.id` | nome | slug | `status` |
|---|---|---|---|
| 12 | A Instituição | a-instituicao | **true** |
| 13 | A Cidade | a-cidade | **false** |

Ou seja, **A Cidade não aparece no menu lateral do CMS**, mesmo com as
permissões concedidas ao papel `administrador`. O operador só chega à tela
digitando a URL `/cms/conteudos/a-cidade`.

## 9. Saídas de API / presenters

| Item | Formato |
|---|---|
| Listagem do CMS | JSON do Yajra DataTables: `{data: [{value, titulo, id, slug, edit, delete, active, inactivate}], ...}` — `make(true)` devolve o JSON pronto. |
| `ContentPresenter` | `v6/app/Http/Presenters/ContentPresenter.php`, aplicado por `PresentableTrait` no model `Content`. Não é usado pelo controller do portal (que lê o model cru). |
| Portal | HTML renderizado por Blade. Não há JSON público para A Cidade. |

## 10. Resumo das rotas por superfície

| Superfície | Quantidade de rotas do módulo | Observação |
|---|---|---|
| Portal público | 1 (`/a-cidade/{slug}`) | `ANY`; sem listagem pública. |
| CMS | 9 (genéricas) | Todas compartilhadas com os demais módulos de conteúdo. |
| API de dados abertos | 0 | Ausente. |
| Breadcrumb | 1 registro | `a-cidade`, resolvido por `categoryContents->slug`. |
