# Obras Paralisadas — Views e UX

> Módulo **existente apenas na v5**.
> Este arquivo cobre o inventário de views e as telas do CMS e do portal.
> Exportações, endpoints JavaScript, assets e checklist de verificação manual
> estão em `09-PORTAL-EXPORTACOES-E-JS.md`.

---

## 1. Inventário de views

### 1.1 CMS (`v5/resources/views/prefecture/obras-paralisadas/`)

| Arquivo | Função |
|---|---|
| `index.blade.php` | Listagem em DataTable (AdminLTE). Apenas o esqueleto da tabela; os dados vêm de `/cms/datatable/obra-paralisada`. |
| `create.blade.php` | Formulário de cadastro. `Form::open` apontando para `obra-paralisada.store` com `'files' => true`. |
| `edit.blade.php` | Formulário de edição. `Form::model($obra, ['route' => ['obra-paralisada.update', $obra->id], 'method' => 'PUT', 'files' => true])`. |
| `form.blade.php` | Formulário compartilhado entre create e edit (campos, bloco de anexos, listagem dos arquivos já enviados). |

Layout: `@extends('admin.layouts.app')` — AdminLTE 1.22.

### 1.2 Portal (`v5/resources/views/portal/layouts/`)

O controller resolve o template **em tempo de execução** a partir da
configuração `layout`:

```php
// v5/app/Http/Controllers/Admin/Controller.php:39-49
protected function getTemplateActived()
{
    $templateActived = Configuration::where('key', 'layout')->first()->value;

    if (!View::exists('portal.layouts.'.$templateActived.'.home.index')) {
        DB::table('configurations')->where('key', 'pagina_em_manutencao')->update(['value' => 1]);
        abort(404, 'view_inexistente');
    }

    return $templateActived;
}
```

Existem **seis** conjuntos de views do módulo, um por template:

| Template | Arquivos |
|---|---|
| `modelo1` | `v5/resources/views/portal/layouts/modelo1/obras/paralisadas/{index,show,filtro}.blade.php` |
| `modelo2` | `v5/resources/views/portal/layouts/modelo2/obras/paralisadas/{index,show,filtro}.blade.php` |
| `modelo3` | `v5/resources/views/portal/layouts/modelo3/obras/paralisadas/{index,show,filtro}.blade.php` |
| `modelo4` | `v5/resources/views/portal/layouts/modelo4/obras/paralisadas/{index,show,filtro}.blade.php` |
| `modelo5` | `v5/resources/views/portal/layouts/modelo5/obras/paralisadas/{index,show,filtro}.blade.php` |
| `modelo6` | `v5/resources/views/portal/layouts/modelo6/obras/paralisadas/{index,show,filtro}.blade.php` |

Diferença entre os seis conjuntos: apenas o `@extends`, o caminho do
breadcrumb, o `@include` do filtro e (em `modelo5`/`modelo6`) a classe de
alinhamento `text-right` → `text-end` (Bootstrap 5). O **conteúdo** é idêntico
ao `modelo1`.

> **Risco:** o layout `default` **não possui** nem a subpasta `home` nem a pasta
> `obras/paralisadas`. Com `layout = default`, `getTemplateActived()` cai no
> `abort(404, 'view_inexistente')` porque `View::exists('portal.layouts.default.home.index')`
> é falso — e ainda grava `pagina_em_manutencao = 1` na tabela
> `configurations`. Na prática, o módulo só funciona com os layouts
> `modelo1` a `modelo6`.

### 1.3 Componente compartilhado

| Arquivo | Função |
|---|---|
| `v5/resources/views/portal/components/obras-paralisadas/table.blade.php` | Tabela da listagem pública + paginação. É **comum aos seis templates** (não há uma cópia por modelo). |

### 1.4 Dependências de view

| Arquivo | Papel |
|---|---|
| `v5/resources/views/components/inputs/input.blade.php` | Campo de texto/data reutilizado para `identificacao`, `responsavel` e `previsao_reinicio`. |
| `v5/resources/views/components/inputs/textarea.blade.php` | Campo multilinha para `motivo_paralisacao`. |
| `v5/resources/views/portal/layouts/modelo1/layout/layout.blade.php` (e equivalentes) | Layout do portal. |
| `v5/resources/views/portal/layouts/modelo1/layout/breadcrumb.blade.php` (e equivalentes) | Renderização do breadcrumb. |

## 2. Tela do CMS — listagem

`v5/resources/views/prefecture/obras-paralisadas/index.blade.php`:

```blade
<table id="tabela_obra-paralisada" class="table table-striped table-hover w-100">
    <thead>
        <tr>
            <th scope="col" width="5%">ID</th>
            <th scope="col">Identificação da Obra</th>
            <th scope="col">Responsável</th>
            <th scope="col">Previsão de Reinício</th>
            <th scope="col" class="text-center" width="15%">Ações</th>
        </tr>
    </thead>
    <tbody></tbody>
</table>
```

O botão "Cadastrar" é condicionado à permissão:

```blade
@if (has_access(['obra-paralisada.create']))
    <a href="{{ route('obra-paralisada.create') }}" class="btn btn-success font-weight-bold btn-sm" role="button">
        <i class="fa fa-plus" aria-hidden="true"></i> Cadastrar
    </a>
@endif
```

O JS da tabela é carregado no `@section('js')`:

```blade
@section('js')
    @parent
    <script src="{{ url('assets/js/custom/config/obras-paralisadas/datatable.js') }}"></script>
@endsection
```

### 2.1 Colunas do DataTable

`v5/public/assets/js/custom/config/obras-paralisadas/datatable.js`:

```js
let collums = [
    {
        data: 'identificacao',
        render: function (data) {
            return `<div class='vertical-middle' style='padding:8px'>${data ?? '-'}</div>`;
        }
    },
    {
        data: 'responsavel',
        render: function (data) {
            return `<div class='vertical-middle' style='padding:8px'>${data ?? '-'}</div>`;
        }
    },
    {
        data: 'previsao_reinicio',
        render: function (data) {
            return `<div class='vertical-middle' style='padding:8px'>${data ?? '-'}</div>`;
        }
    },
    {
        data: null,
        orderable: false,
        className: 'text-center',
        render: function (data) {
            return `<div class='flex justify-center'>`
                + createBTN(data.id, `obras-paralisadas/editar/${data.id}`, data.edit, data.delete, `obras-paralisadas/excluir/${data.id}`)
                + `</div>`;
        }
    }
];

runDatatables('obra-paralisada', collums);
```

| Coluna | Campo | Origem |
|---|---|---|
| 1 | `value` | Sequencial (`$obra->value = $index + 1`), injetado por `runDatatables` via `collums.unshift`. |
| 2 | `identificacao` | `DB::table('obras_paralisadas')`. |
| 3 | `responsavel` | idem |
| 4 | `previsao_reinicio` | idem |
| 5 | (botões) | `data.edit` / `data.delete` vêm de `struct_views()` no `DatatableController`. |

URLs dos botões (relativas à URL atual `/cms/obras-paralisadas`): editar →
`/cms/obras-paralisadas/editar/{id}` (rota `obra-paralisada.edit`); excluir →
`/cms/obras-paralisadas/excluir/{id}` (rota `obra-paralisada.destroy`).

A exclusão é executada por `addDelete()` em
`v5/public/assets/js/custom/utilities/table.js:357-380`, que usa `myFetch(url,
'DELETE')` e mostra o toast com `result.erro` ou `result.success`.

### 2.2 Comportamento do `runDatatables`

`v5/public/assets/js/custom/utilities/table.js:3-153`:

- `ajax.url` = `/cms/datatable/obra-paralisada`, `type: 'GET'`,
  `contentType: 'application/json'`.
- Paginação, busca e ordenação são **client-side** (o servidor devolve todos os
  registros não apagados).
- `stateSave: true` — o DataTable memoriza página, busca e ordenação no
  `localStorage`.
- Botões de exportação embutidos: **Excel**, **PDF**, **CSV** e **TXT**
  (gera `.txt` com tabulação, via `Blob`).
- Título das exportações: `Prefeitura de {cidade} - {data}`.

## 3. Tela do CMS — formulário

### 3.1 Campos

| Campo | Tipo | Componente | Observações |
|---|---|---|---|
| Identificação da Obra | `text` (max 255) | `components.inputs.input` | Placeholder "Nome ou codificação do projeto". |
| Motivo da Paralisação | `textarea` | `components.inputs.textarea` | Placeholder "Justificativa do travamento da obra". |
| Responsável | `text` (max 255) | `components.inputs.input` | Placeholder "Engenheiro, Construtora ou Secretaria". |
| Previsão de Reinício | `date` | `components.inputs.input` com `'tipo' => 'date'` | Coluna é `varchar(255)`. |
| Arquivos | `file[]` + `texto[]` | HTML inline | Múltiplos PDFs com descrição. |

O bloco de arquivos é montado dinamicamente por jQuery no `@section('other-scripts')`
do `form.blade.php`:

```js
$('#btn-adicionar').click(function () {
    var novoInput = $('<div class="data-itens" style="margin-top: 10px;"><div class="row"><div class="col-md-4 col-sm-12 mb-2"><input type="file" name="arquivo[]" class="form-control" accept=".pdf"></div><div class="col-md-4 col-sm-12 mb-2"><input type="text" name="arquivo_desc[]" class="form-control" placeholder="Informe a descrição"></div><button type="button" class="js-remove-itens btn btn-sm btn-danger mb-2" style="margin-left: 15px; height: 35px;"><i class="fa fa-trash"></i></button></div></div>');
    novoInput.hide();
    $('.itens-wrapper .data-itens:last').after(novoInput);
    novoInput.fadeIn('fast');
    $('.description').css('display', 'block');
    return false;
});
```

Características do upload:

- **Não há upload progressivo**: os arquivos ficam no `<form>` e só são enviados
  quando o formulário é submetido (POST/PUT).
- O botão "Adicionar Arquivo" insere **um par por clique**; o botão de lixeira
  remove o par inserido.
- O cabeçalho "Arquivos / Descrição" só aparece (`display: block`) depois do
  primeiro clique.
- O campo de descrição é livre (não há vínculo ForEach entre `arquivo[]` e
  `arquivo_desc[]` além da posição).

### 3.2 Arquivos já enviados (modo edição)

```blade
@if (isset($obra) && $obra->arquivo)
    <div id="files-done">
        <h5>Arquivos enviados</h5>
        @foreach ($arquivosExistentes as $arqIndex => $item)
            <div id="arquivo-row-{{ $arqIndex }}" class="ajax-file-upload-statusbar col-md-8 align-top">
                <i class="fa fa-file-archive-o fa-2x float-left"></i>
                <a href="{{ url('storage/' . $item['path']) }}" download="{{ basename($item['path']) }}" target="_blank" class="btn btn-flat btn-default" title="Download"><i class="fa fa-download"></i></a>
                <a href="{{ url('storage/' . $item['path']) }}" target="_blank" class="btn btn-flat btn-default" title="Visualizar"><i class="fa fa-eye"></i></a>
                <button type="button" class="btn btn-flat btn-default" title="Deletar"
                    data-url="{{ route('obra-paralisada.destroy-arquivo', [$obra->id, $arqIndex]) }}"
                    onclick="deleteObraArquivo(this)"><i class="fa fa-trash"></i></button>
            </div>
        @endforeach
    </div>
@endif
```

A remoção usa AJAX com o token CSRF:

```js
function deleteObraArquivo(btn) {
    if (!confirm('Deseja realmente remover este arquivo?')) return;
    var $btn = $(btn), url = $btn.data('url'), row = $btn.closest('.ajax-file-upload-statusbar');
    $.ajax({
        url: url, type: 'DELETE',
        data: { _token: $('meta[name="csrf-token"]').attr('content') },
        success: function () { row.fadeOut(300, function () { $(this).remove(); }); },
        error: function () { alert('Erro ao remover o arquivo. Tente novamente.'); }
    });
}
```

Os downloads usam `url('storage/' . $item['path'])`, ou seja, o symlink
`public/storage` → `storage/app/public` criado por `php artisan storage:link`
(disk `public`, raiz `storage_path('app/public')`, conforme
`v5/config/filesystems.php:16` e `:52-54`).

### 3.3 Botão de envio

`create.blade.php` e `edit.blade.php` compartilham:

```blade
<button class="btn btn-primary btn-flat" id="btn-submit" onclick="desabilitaBotao()">
    {{ trans('adminlte::adminlte.register') }}
</button>
<span id="loading" class="ml-2"></span>
```

`desabilitaBotao()` desabilita o botão, submete o form e exibe um GIF de
carregamento. O rótulo do botão é a tradução `adminlte::adminlte.register`
("Registrar"), inclusive na tela de **editar**.

## 4. Tela do portal — listagem

`v5/resources/views/portal/layouts/modelo1/obras/paralisadas/index.blade.php`:

```blade
@include('portal.layouts.modelo1.obras.paralisadas.filtro')
@if ($result->isNotEmpty())
    <div class="col-md-12 mt-3 text-right px-0">
        <small class="text-secondary">Salvar como:</small>
        <div class="btn-group" role="group">
            <button type="button" class="btn btn-outline-secondary btn-sm" onclick="exportar('xls')" title="Exportar">xls</button>
            <button type="button" class="btn btn-outline-secondary btn-sm" onclick="gerarPdf()" title="Exportar">pdf</button>
            <button type="button" class="btn btn-outline-secondary btn-sm" onclick="exportar('csv')" title="Exportar">csv</button>
            <button type="button" class="btn btn-outline-secondary btn-sm" onclick="exportar('txt')" title="Exportar">txt</button>
        </div>
    </div>
@endif
@include('portal.components.obras-paralisadas.table')
@if ($result->isEmpty())
    <div class="text-center mt-3"><p>Nenhuma informação encontrada!</p></div>
@endif
```

E, no fim da seção:

```blade
<script>var obrasParalisadas = {!! json_encode($allData) !!};</script>
<script src="{{ asset("assets/js/modulos/jspdf/jspdf.umd.min.js") }}"></script>
<script src="{{ asset("assets/js/modulos/jspdf/jspdf-autotable.min.js") }}"></script>
<script src="{{ asset("assets/js/modulos/jspdf/gerar-pdf-obras-paralisadas.js") }}"></script>
```

### 4.1 Caixa de filtro

`filtro.blade.php` (idêntico nos seis modelos, exceto o `@include` do layout):

```blade
{{ Form::open(['route' => 'obras-paralisadas.index', 'method' => 'GET', 'id' => 'form-filter']) }}
    <input type="text" class="form-control" id="busca" name="busca"
        autocomplete="off" placeholder="Digite para filtrar..."
        value="{{ $busca ?? '' }}">
    <button type="submit" class="btn btn-sm bg-color-3 txt-color-dinamic-3 float-right" style="color:#fff;">Consultar</button>
    <button type="button" onclick="limparFiltros();" class="btn btn-sm float-right mr-2 limpar-filtros">Limpar campos</button>
{{ Form::close() }}
```

```js
function limparFiltros() { $('input[name="busca"]').val(''); }
function exportar(type) {
    var valores = $('#form-filter').serialize();
    window.location.href = '{{ route("obras-paralisadas.index") }}?' + valores + '&type=' + type;
}
```

"Limpar campos" **apaga o campo mas não submete** o formulário: é preciso
clicar em "Consultar" para que a lista volte ao estado original.

### 4.2 Tabela da listagem

`v5/resources/views/portal/components/obras-paralisadas/table.blade.php`:

| Coluna | Conteúdo |
|---|---|
| Identificação da Obra | `$obra->identificacao` |
| Motivo da Paralisação | `Str::limit($obra->motivo_paralisacao, 80)` |
| Responsável | `$obra->responsavel` |
| Previsão de Reinício | `$obra->previsao_reinicio` |
| Detalhes | Botão de lupa para `route('obras-paralisadas.show', $obra->slug)` |

Rodapé com paginação:

```blade
<span class="text-muted" style="font-size: 12px;">
    Exibindo {{ $result->firstItem() }} a {{ $result->lastItem() }} de {{ $result->total() }} registros
</span>
{{ $result->appends(request()->query())->links() }}
```

`appends(request()->query())` preserva o filtro `busca` na paginação — mas
**também** preserva o `type` se ele estiver na query string, o que pode
disparar uma exportação ao navegar entre páginas.

## 5. Tela do portal — ficha

`v5/resources/views/portal/layouts/modelo1/obras/paralisadas/show.blade.php`:

```blade
<table class="table-responsive bidding text-secondary table">
    <tr><th>Identificação da Obra</th><td>{{ $obra->identificacao }}</td></tr>
    <tr><th>Motivo da Paralisação</th><td>{{ $obra->motivo_paralisacao }}</td></tr>
    <tr><th>Responsável</th><td>{{ $obra->responsavel }}</td></tr>
    <tr><th>Previsão de Reinício</th><td>{{ $obra->previsao_reinicio }}</td></tr>
</table>
```

O bloco de anexos:

```blade
@if ($obra->arquivo)
    <div class="card p-3 mb-3">
        <h4 class="text-secondary mb-4">Arquivos para Download</h4>
        <ul class="list-unstyled">
            @foreach ($arquivos as $item)
                <li class="d-block p-1">
                    <a href="{{ url('storage/' . $item['path']) }}" target="_blank" class="text-dark">
                        <small class="d-inline-block ml-3 pt-1">{{ !empty($item['description']) ? $item['description'] : basename($item['path']) }}</small>
                    </a>
                </li>
            @endforeach
        </ul>
    </div>
@endif
```

Observação de UX: quando há descrição, ela substitui o nome do arquivo como
rótulo do link; o nome do arquivo some da tela.

