# Obras Paralisadas — Rotas e Controllers

> Módulo **existente apenas na v5**.

---

## 1. Rotas do CMS (`v5/routes/basepro.php`)

Todas as rotas abaixo vivem dentro do grupo aberto em `v5/routes/basepro.php:35`:

```php
Route::group(['prefix' => 'cms', 'before' => 'csrf'], function () {
```

Logo, o prefixo final é `/cms` e **todas** passam pelo middleware de CSRF.
O bloco do módulo está em `v5/routes/basepro.php:516-523`:

```php
// Obras Paralisadas
Route::get('obras-paralisadas', ['uses' => 'Prefecture\ObraParalisadaController@index'])->name('obra-paralisada.index')->middleware('check.logged');
Route::get('obras-paralisadas/cadastrar', ['uses' => 'Prefecture\ObraParalisadaController@create'])->name('obra-paralisada.create')->middleware('check.logged');
Route::get('obras-paralisadas/editar/{id}', ['uses' => 'Prefecture\ObraParalisadaController@edit'])->name('obra-paralisada.edit')->middleware('check.logged');
Route::put('obras-paralisadas/atualizar/{id}', ['uses' => 'Prefecture\ObraParalisadaController@update'])->name('obra-paralisada.update');
Route::post('obras-paralisadas', ['uses' => 'Prefecture\ObraParalisadaController@store'])->name('obra-paralisada.store');
Route::delete('obras-paralisadas/excluir/{id}', ['uses' => 'Prefecture\ObraParalisadaController@destroy'])->name('obra-paralisada.destroy');
Route::delete('obras-paralisadas/{id}/arquivo/{index}', ['uses' => 'Prefecture\ObraParalisadaController@destroyArquivo'])->name('obra-paralisada.destroy-arquivo');
```

| # | Método | URI | Nome | Controller@action | Middleware declarados na rota | Sessão exigida |
|---|---|---|---|---|---|---|
| 1 | GET | `/cms/obras-paralisadas` | `obra-paralisada.index` | `Prefecture\ObraParalisadaController@index` | `check.logged` | sim |
| 2 | GET | `/cms/obras-paralisadas/cadastrar` | `obra-paralisada.create` | `Prefecture\ObraParalisadaController@create` | `check.logged` | sim |
| 3 | GET | `/cms/obras-paralisadas/editar/{id}` | `obra-paralisada.edit` | `Prefecture\ObraParalisadaController@edit` | `check.logged` | sim |
| 4 | PUT | `/cms/obras-paralisadas/atualizar/{id}` | `obra-paralisada.update` | `Prefecture\ObraParalisadaController@update` | **nenhum** | herdada |
| 5 | POST | `/cms/obras-paralisadas` | `obra-paralisada.store` | `Prefecture\ObraParalisadaController@store` | **nenhum** | herdada |
| 6 | DELETE | `/cms/obras-paralisadas/excluir/{id}` | `obra-paralisada.destroy` | `Prefecture\ObraParalisadaController@destroy` | **nenhum** | herdada |
| 7 | DELETE | `/cms/obras-paralisadas/{id}/arquivo/{index}` | `obra-paralisada.destroy-arquivo` | `Prefecture\ObraParalisadaController@destroyArquivo` | **nenhum** | herdada |

### 1.1 Rota auxiliar do DataTable

| Método | URI | Nome | Controller@action | Sessão |
|---|---|---|---|---|
| GET | `/cms/datatable/obra-paralisada` | `datatable.obra-paralisada` | `Prefecture\DatatableController@get_obras_paralisadas` (`v5/routes/basepro.php:474`) | herdada |

O consumo é client-side: `runDatatables('obra-paralisada', collums)` monta a
URL `/cms/datatable/obra-paralisada`
(`v5/public/assets/js/custom/utilities/table.js:120-125`).

## 2. Risco: rotas de escrita sem `check.logged`

As rotas 4, 5, 6 e 7 **não declaram** `middleware('check.logged')`, quebrando o
padrão do restante do CMS (compare com `v5/routes/basepro.php:330-332`, módulo
Estagiários, e `:356-358`, módulo Concursos PSS, onde as três rotas de leitura
trazem `check.logged`).

**O que efetivamente acontece:** `ObraParalisadaController` estende
`App\Http\Controllers\Prefecture\Controller`, e o construtor da classe base
aplica o middleware de permissão a **todas** as actions:

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

E `CheckRoutePermission` (aplicado por herança) faz:

```php
// v5/app/Http/Middleware/CheckRoutePermission.php:21-33
if (Sentinel::check()) {
    if (!Sentinel::inRole('super-admin')) {
        if (!$this->hasAccess()) {
            flash('Você não tem autorização para acessar esta funcionalidade!')->warning();
            return redirect()->back();
        }
    }
}
if (!Sentinel::check()) {
    return redirect()->route('login');
}
```

Consequências reais:

| Cenário | Comportamento efetivo |
|---|---|
| Anônimo acessa `POST /cms/obras-paralisadas` | Redireciona (302) para `login`. **Não executa a action.** O CSRF ainda é avaliado antes, então um POST sem token falha com 419. |
| Anônimo acessa `DELETE /cms/obras-paralisadas/excluir/1` | Redireciona para `login`; o `fetch` do JS tenta interpretar HTML como JSON, lança erro e é engolido por `try/catch` em `myFetch` (`v5/public/assets/js/custom/utilities/functions.js:64-88`). O usuário não recebe feedback. |
| Logado sem a permissão | `redirect()->back()` com flash de aviso. |
| Logado com a permissão | Executa a action. |

**Classificação do risco:** acesso anônimo às actions de escrita é **bloqueado
na prática**, mas por um mecanismo **implícito** (herança do controller base),
não por um middleware declarado. Isso cria três problemas concretos:

1. **Fragilidade estrutural:** se alguém trocar a classe base do controller
   (ou remover o `$this->middleware(...)` do construtor), as rotas 4 a 7
   passam a permitir criação, alteração, exclusão de obra e exclusão de anexo
   **sem autenticação**. A rota 5 (`store`) em particular cria registro
   público no portal.
2. **Comportamento divergente:** `check.logged` redireciona anônimo para
   `dashboard.index`; `check.route.permission` redireciona para `login`.
   As respostas HTTP das rotas protegidas não são homogêneas.
3. **Permissão avaliada por nome de rota, não por action:** o middleware
   deriva a permissão de `Route::getCurrentRoute()->getName()` e do segmento
   da URL. Para `obra-paralisada.store`, o segmento 2 é `obras-paralisadas`
   → `obras-paralisadas.index`; para `obra-paralisada.update` (PUT
   `.../atualizar/{id}`) o segmento 3 é `atualizar`, que não está na lista
   `['cadastrar', 'editar']`, então o segmento avaliado é
   `atualizar.index` — permissão que **ninguém possui**. Na prática, quem
   passa é porque o conjunto `hasAnyAccess` inclui `obras.store` /
   `obras.update` herdadas do alias, ou porque a rota é chamada por quem já
   tem acesso amplo. Esse cálculo é frágil e merece recomeço na v7.

**Recomendação para a v7:** declarar `auth` (ou `check.logged`) em **todas**
as rotas de escrita e substituir a derivação por segmento por `Gate` /
`Policy` explícita.

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

Todas dentro do grupo `Route::middleware(['check.site.manutention'])` aberto em
`v5/routes/web.php:5`:

```php
// v5/routes/web.php:210-212
##########Obras Paralisadas###################################################
Route::any('obras-paralisadas', 'Portal\ObrasParalisadasController@index')->name('obras-paralisadas.index');
Route::get('obra-paralisada/{slug}', 'Portal\ObrasParalisadasController@show')->name('obras-paralisadas.show');
```

| Método | URI | Nome | Controller@action | Sessão | Observação |
|---|---|---|---|---|---|
| ANY | `/obras-paralisadas` | `obras-paralisadas.index` | `Portal\ObrasParalisadasController@index` | não | `Route::any` aceita GET, POST, PUT, DELETE etc. na mesma URI. |
| GET | `/obra-paralisada/{slug}` | `obras-paralisadas.show` | `Portal\ObrasParalisadasController@show` | não | URI **singular** (`obra-paralisada`), enquanto o índice é plural. |

Parâmetros aceitos em `/obras-paralisadas`:

| Query string | Tipo | Efeito |
|---|---|---|
| `busca` | string | Filtra por `identificacao` **ou** `motivo_paralisacao`. |
| `type` | string (`csv`, `xls`, `txt`) | Dispara a exportação e **ignora** a paginação. Qualquer outro valor → `abort(404)`. |
| `page` | int | Paginação do Laravel (15 por página). |

Não há API JSON pública para este módulo. O endpoint JSON do CMS
(`/cms/datatable/obra-paralisada`) é o único consumo de JSON, e é autenticado.

## 4. Controllers

### 4.1 `Prefecture\ObraParalisadaController`

**Arquivo:** `v5/app/Http/Controllers/Prefecture/ObraParalisadaController.php` (128 linhas)
**Herda de:** `App\Http\Controllers\Prefecture\Controller` → `check.route.permission`
**Usa:** `App\Http\Traits\FileTrait`

| Método | Retorno | Comportamento |
|---|---|---|
| `index()` | `View` | Renderiza `prefecture.obras-paralisadas.index`. Não consulta o banco (a listagem vem do DataTable). |
| `create()` | `View` | Renderiza `prefecture.obras-paralisadas.create`. |
| `store(ObraParalisadaRequest $request)` | `RedirectResponse` | Valida, salva os textos, faz upload dos PDFs, grava o JSON, cria o registro, flash de sucesso e redireciona para `obra-paralisada.index`. |
| `edit($id)` | `View` | `ObraParalisada::find($id)`; se não encontrar, flash "Registro não encontrado!" e redireciona para o índice. |
| `update(ObraParalisadaRequest $request, $id)` | `RedirectResponse` | Mescla os anexos novos com os existentes (preservando a ordem), atualiza os textos, flash de sucesso. |
| `destroy($id)` | `JsonResponse` | `findOrFail($id)->delete()` → `{"success":"Registro deletado com sucesso."}` (200) ou `{"erro":"..."}` (500). Soft delete. |
| `destroyArquivo($id, $index)` | `JsonResponse` | Remove **por índice** o item do array JSON; se o array ficar vazio, grava `null`; 404 se o índice não existir; 500 em erro. |

Trecho de upload e persistência (`store`):

```php
$data = $request->only(['identificacao', 'motivo_paralisacao', 'responsavel', 'previsao_reinicio']);

if ($request->hasFile('arquivo')) {
    $items = [];
    $descs = $request->input('arquivo_desc', []);
    foreach ($request->file('arquivo') as $i => $file) {
        $path = $this->saveUpload($file, 'docs', null, null, false);
        $items[] = ['path' => "docs/{$path}", 'description' => $descs[$i] ?? ''];
    }
    $data['arquivo'] = json_encode($items);
}

ObraParalisada::create($data);
```

Remoção de anexo:

```php
$obra = ObraParalisada::findOrFail($id);
$arquivos = json_decode($obra->arquivo, true) ?: [$obra->arquivo];

if (!isset($arquivos[$index])) {
    return response()->json(['erro' => 'Arquivo não encontrado.'], 404);
}

array_splice($arquivos, $index, 1);
$obra->arquivo = !empty($arquivos) ? json_encode(array_values($arquivos)) : null;
$obra->save();
```

### 4.2 `Portal\ObrasParalisadasController`

**Arquivo:** `v5/app/Http/Controllers/Portal/ObrasParalisadasController.php` (118 linhas)
**Herda de:** `App\Http\Controllers\Admin\Controller` (fornece `getTemplateActived()`)

| Método | Visibilidade | Comportamento |
|---|---|---|
| `index(Request $request)` | public | Monta a query, carrega `$allData` (todos), pagina 15, exporta se `type` estiver presente, senão renderiza a view do layout ativo. |
| `show($slug)` | public | `ObraParalisada::where('slug', $slug)->firstOrFail()` → 404 se não existir (inclusive se estiver soft-deleted). |
| `buildQuery(Request $request)` | private | Query base + filtro de busca. |
| `exportar(string $type, $data)` | private | Monta cabeçalho e linhas, despacha para csv/xls/txt; `abort(404)` para tipo desconhecido. |
| `exportCsv/exportXls/exportTxt` | private | Respostas `Response` com `Content-Disposition: attachment`. |

Filtro de busca:

```php
$query->when($request->busca, function ($q) use ($request) {
    $busca = $request->busca;
    $q->where(function ($inner) use ($busca) {
        $inner->whereRaw("translate(lower(identificacao), ' ''àáâãäéèëêíìïîóòõöôúùüûçÇ', '--aaaaaeeeeiiiiooooouuuucc') ilike ?", ['%' . clean_slug($busca) . '%'])
              ->orWhereRaw("lower(motivo_paralisacao) ilike ?", ['%' . mb_strtolower($busca) . '%']);
    });
});
```

Cabeçalho e corpo das exportações:

```php
$headers = ['Identificação da Obra', 'Motivo da Paralisação', 'Responsável', 'Previsão de Reinício'];

$rows = $data->map(function ($o) {
    return [$o->identificacao, $o->motivo_paralisacao, $o->responsavel, $o->previsao_reinicio];
})->toArray();
```

## 5. Form Request

**Arquivo:** `v5/app/Http/Requests/ObraParalisadaRequest.php`
**Classe:** `App\Http\Requests\ObraParalisadaRequest`

`authorize()` retorna **`true`** — a validação de permissão é feita
exclusivamente pelo middleware, nunca pelo Request.

| Campo | Regra | Efeito no formulário |
|---|---|---|
| `identificacao` | `required\|string\|max:255` | Texto obrigatório, até 255 caracteres. |
| `motivo_paralisacao` | `required\|string` | Texto obrigatório, sem limite de tamanho (coluna `text`). |
| `responsavel` | `required\|string\|max:255` | Texto obrigatório. |
| `previsao_reinicio` | `required\|string\|max:255` | Obrigatório; gravado como string. |
| `arquivo` | `nullable\|array` | Lista de arquivos (input `name="arquivo[]"`). |
| `arquivo.*` | `file\|mimes:pdf\|max:10240` | Cada arquivo precisa ser PDF e ter no máximo 10 MB. |
| `arquivo_desc` | `nullable\|array` | Lista de descrições (`name="arquivo_desc[]"`). |
| `arquivo_desc.*` | `nullable\|string\|max:255` | Descrição de até 255 caracteres. |

Mensagens customizadas (as demais usam as traduções do Laravel):

| Chave | Texto |
|---|---|
| `identificacao.required` | O campo Identificação da Obra é obrigatório. |
| `motivo_paralisacao.required` | O campo Motivo da Paralisação é obrigatório. |
| `responsavel.required` | O campo Responsável é obrigatório. |
| `previsao_reinicio.required` | O campo Previsão de Reinício é obrigatório. |
| `arquivo.*.mimes` | Os arquivos devem estar no formato PDF. |
| `arquivo.*.max` | Cada arquivo não pode ser maior que 10MB. |

Observação: o `accept=".pdf"` do `<input type="file">` é apenas uma dica de
navegador; a validação real vem de `mimes:pdf`. E o Request **não** valida que
o número de `arquivo_desc` seja igual ao de `arquivo` — o controller usa
`$descs[$i] ?? ''`, então descrições extras são silenciosamente descartadas e
arquivos sem descrição ficam com string vazia.

## 6. Permissões

O módulo **não tem** recurso próprio em `resources`. As permissões são
derivadas do nome da rota e traduzidas por alias.

| Nome usado no código | Tradução efetiva | Origem |
|---|---|---|
| `obra-paralisada.create` (botão "Cadastrar") | `obra-paralisada.create` **ou** `obras.create` | `resolve_permission_aliases()` em `v5/app/Library/Helper/appHelpers.php:44-64` |
| `obra-paralisada.edit` (botão editar da linha) | idem com `obras.edit` | idem |
| `obra-paralisada.delete` (botão excluir da linha) | idem com `obras.delete` | idem |
| `obra-paralisada.index` (acesso à rota) | idem com `obras.index` | idem |

```php
$map = [
    'obra-paralisada' => 'obras',
    'obras-paralisadas' => 'obras',
    'categorias-obras' => 'obras',
];
```

Recurso no banco v5: `resources.id = 13`, `slug = 'obras'`, `name = 'Obras'`.
**Não existe** `resources` com slug `obras-paralisadas` nem na v5 nem na v6.

Guard: `cartalyst/sentinel`. Papéis considerados pelo middleware:
`super-admin` (bypass total) e demais papéis com `hasAnyAccess`.

## 7. API Resources / presenters

**Não existem.** O módulo não define nenhum `JsonResource`, transformer ou
presenter. As duas respostas JSON são montadas à mão com `response()->json()`:

| Endpoint | Corpo | Status |
|---|---|---|
| `DELETE /cms/obras-paralisadas/excluir/{id}` | `{"success":"Registro deletado com sucesso."}` | 200 |
| `DELETE /cms/obras-paralisadas/excluir/{id}` (erro) | `{"erro":"Ocorreu um erro ao tentar deletar o registro."}` | 500 |
| `DELETE /cms/obras-paralisadas/{id}/arquivo/{index}` | `{"success":"Arquivo removido com sucesso."}` | 200 |
| `DELETE /cms/obras-paralisadas/{id}/arquivo/{index}` (índice inexistente) | `{"erro":"Arquivo não encontrado."}` | 404 |
| `DELETE /cms/obras-paralisadas/{id}/arquivo/{index}` (erro) | `{"erro":"Erro ao remover o arquivo."}` | 500 |

O JSON do DataTable (`/cms/datatable/obra-paralisada`) é gerado pelo
`yajra/laravel-datatables` a partir de `stdClass` montados em
`struct_views($data, 'obra-paralisada')`
(`v5/app/Http/Controllers/Prefecture/DatatableController.php:508-524`), que
acrescenta as chaves `edit`, `delete`, `active` e `inactivate` conforme as
permissões do usuário logado.
