# Obras Paralisadas — Portal: Exportações, JavaScript e UX

> Complemento de `04-VIEWS-E-UX.md`. Módulo **existente apenas na v5**.

---

## 1. Exportações do portal

Quatro formatos são oferecidos na listagem pública. Três são gerados **no
servidor** (rotas `obras-paralisadas.index` com o parâmetro `type`) e um é
gerado **no navegador** (jsPDF).

| Formato | Onde é gerado | Endereço | Cabeçalho HTTP |
|---|---|---|---|
| `xls` | servidor | `GET /obras-paralisadas?...&type=xls` | `application/vnd.ms-excel; charset=UTF-8` |
| `csv` | servidor | `GET /obras-paralisadas?...&type=csv` | `text/csv; charset=UTF-8` |
| `txt` | servidor | `GET /obras-paralisadas?...&type=txt` | `text/plain; charset=UTF-8` |
| `pdf` | navegador | botão `gerarPdf()` | download `obras-paralisadas.pdf` |

As quatro exportações usam exatamente as mesmas quatro colunas:

```
Identificação da Obra | Motivo da Paralisação | Responsável | Previsão de Reinício
```

### 1.1 Disparo

O JS do filtro (presente nos seis modelos) serializa o formulário e navega:

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

O botão de PDF é diferente — não navega, chama a função local:

```blade
<button type="button" class="btn btn-outline-secondary btn-sm" style="cursor: pointer;" onclick="gerarPdf()" title="Exportar">pdf</button>
```

Os botões só são renderizados quando há ao menos um registro:

```blade
@if ($result->isNotEmpty())
```

### 1.2 CSV (servidor)

```php
private function exportCsv(array $headers, array $rows)
{
    $content = implode(';', $headers) . "\n";
    foreach ($rows as $row) {
        $content .= implode(';', array_map(function($v) { return '"' . str_replace('"', '""', $v) . '"'; }, $row)) . "\n";
    }

    return response($content, 200, [
        'Content-Type'        => 'text/csv; charset=UTF-8',
        'Content-Disposition' => 'attachment; filename="obras-paralisadas.csv"',
    ]);
}
```

| Aspecto | Comportamento |
|---|---|
| Separador | `;` (ponto e vírgula) — padrão do Excel em locale pt-BR. |
| Escape | Aspas duplas duplicadas (`""`), conforme RFC 4180. |
| Cabeçalho | **Não** é escapado (vai cru), enquanto as linhas são. |
| Quebra de linha | `\n` (LF), não CRLF. |
| BOM UTF-8 | **Ausente** — ao abrir no Excel, acentos podem aparecer quebrados. O `xls` tem BOM; o `csv` não. |
| Campos com `;` ou aspas | Correctamente escapados por estarem dentro de aspas. |

### 1.3 XLS (servidor)

```php
private function exportXls(array $headers, array $rows)
{
    $xls  = '<table><thead><tr>';
    $xls .= implode('', array_map(function($h) { return "<th>{$h}</th>"; }, $headers));
    $xls .= '</tr></thead><tbody>';
    foreach ($rows as $row) {
        $xls .= '<tr>' . implode('', array_map(function($v) { return "<td>{$v}</td>"; }, $row)) . '</tr>';
    }
    $xls .= '</tbody></table>';

    return response("\xEF\xBB\xBF" . $xls, 200, [
        'Content-Type'        => 'application/vnd.ms-excel; charset=UTF-8',
        'Content-Disposition' => 'attachment; filename="obras-paralisadas.xls"',
    ]);
}
```

| Aspecto | Comportamento |
|---|---|
| Formato real | HTML com BOM UTF-8 (`\xEF\xBB\xBF`), **não** um `.xls` binário. O Excel abre pelo sniffing do cabeçalho porque detecta HTML, mas o formato é enganoso. |
| Escape | **Nenhum**: os valores são interpolados no HTML sem `htmlspecialchars`. Um `motivo_paralisacao` com `<` ou `&` é injetado literalmente. |
| Estilo | Sem CSS inline; a formatação depende do interpretador. |

### 1.4 TXT (servidor)

```php
private function exportTxt(array $headers, array $rows)
{
    $content = '';
    foreach ($rows as $row) {
        foreach ($headers as $i => $h) {
            $content .= "{$h}: " . ($row[$i] ?? '') . "\n";
        }
        $content .= "\n";
    }

    return response($content, 200, [
        'Content-Type'        => 'text/plain; charset=UTF-8',
        'Content-Disposition' => 'attachment; filename="obras-paralisadas.txt"',
    ]);
}
```

Formato "rótulo: valor" com uma linha em branco entre os registros.

### 1.5 PDF (navegador)

`v5/public/assets/js/modulos/jspdf/gerar-pdf-obras-paralisadas.js`:

```js
window.jsPDF = window.jspdf.jsPDF;

function gerarPdf() {
    if (!window.obrasParalisadas || !obrasParalisadas.length) return;

    const doc = new jsPDF();

    doc.autoTable({
        head: [['Identificação da Obra', 'Motivo da Paralisação', 'Responsável', 'Previsão de Reinício']],
        body: obrasParalisadas.map(function (o) {
            return [o.identificacao, o.motivo_paralisacao, o.responsavel, o.previsao_reinicio];
        }),
        styles: { fontSize: 7, cellPadding: 2 },
        columnStyles: {
            0: { cellWidth: 50 },
            1: { cellWidth: 70 },
            2: { cellWidth: 40 },
            3: { cellWidth: 30 }
        }
    });

    doc.save('obras-paralisadas.pdf');
}
```

| Aspecto | Comportamento |
|---|---|
| Dependências | `v5/public/assets/js/modulos/jspdf/jspdf.umd.min.js` e `v5/public/assets/js/modulos/jspdf/jspdf-autotable.min.js`, carregados em cada `index.blade.php` do portal. |
| Origem dos dados | `window.obrasParalisadas`, injetado no HTML como `json_encode($allData)`. |
| Cobertura | **Todos** os registros filtrados (sem paginação), os mesmos do `xls`/`csv`/`txt`. |
| Larguras | 50 + 70 + 40 + 30 = 190 mm; a orientação é retrato (A4 = 210 mm com margens), então a tabela fica apertada. |
| Fonte | 7 pt, `cellPadding: 2`. |
| Quebra de página | Automática pelo `autoTable`. |
| Caracteres | A fonte padrão do jsPDF é Helvetica (Latin-1); caracteres fora desse conjunto podem sair mal impressos. |
| Robustez | `if (!window.obrasParalisadas || !obrasParalisadas.length) return;` — sem registros, não gera arquivo nem avisa o usuário. |

## 2. Payload JSON injetado no HTML

```blade
<script>var obrasParalisadas = {!! json_encode($allData) !!};</script>
```

| Aspecto | Comportamento |
|---|---|
| Conteúdo | Todos os registros da query filtrada, com **todos** os 11 campos do model, incluindo `arquivo` (JSON com os caminhos internos dos anexos) e `deleted_at` (null). |
| Volume | Cresce linearmente com a base. Sem paginação, sem limite, sem cache. |
| Exposição | Os caminhos `docs/<arquivo>` ficam visíveis no código-fonte da página — é o mesmo caminho usado para download, portanto permite inferir a URL de documentos que o cidadão talvez não devesse listar. |
| Codificação | `json_encode` sem `JSON_HEX_TAG`: um texto contendo `</script>` quebraria a página. |

## 3. JavaScript: endpoints chamados

| Endpoint | Método | Origem | Quando |
|---|---|---|---|
| `/cms/datatable/obra-paralisada` | GET | `runDatatables` (`v5/public/assets/js/custom/utilities/table.js:120-125`) | Ao abrir a listagem do CMS. |
| `/cms/obras-paralisadas/excluir/{id}` | DELETE | `addDelete()` (`table.js:357-380`) | Clique na lixeira de uma linha do CMS. |
| `/cms/obras-paralisadas/{id}/arquivo/{index}` | DELETE | `deleteObraArquivo()` (`form.blade.php`) | Clique na lixeira de um arquivo anexado. |
| `/obras-paralisadas?busca=...` | GET | `Form::open` do filtro (`filtro.blade.php`) | Clique em "Consultar". |
| `/obras-paralisadas?...&type=xls\|csv\|txt` | GET | `exportar(type)` (`filtro.blade.php`) | Clique nos botões de exportação do servidor. |
| `/obra-paralisada/{slug}` | GET | link da tabela (`components/obras-paralisadas/table.blade.php`) | Clique na lupa. |

### 3.1 Ausência de interatividade dinâmica

- **Não há** polling, long-poll, websocket, SSE nem requisição periódica.
- **Não há** filtro dinâmico (typeahead): a busca só é aplicada ao submeter o
  formulário.
- **Não há** upload progressivo no CMS: os PDFs são enviados junto com o
  `POST`/`PUT` do formulário.
- O único estado persistido no cliente é o `stateSave` do DataTable
  (página, busca e ordenação), guardado no `localStorage` pelo plugin.

### 3.2 Camadas de cache

| Camada | Aplicação no módulo |
|---|---|
| APCu | Cache de opcode/configuração do Laravel. |
| Redis | Usado pela sessão e por filas; o módulo não cacheia consultas. |
| `stateSave` do DataTable | Único cache de estado da interface (CMS). |
| Cache HTTP | Nenhum cabeçalho `Cache-Control` é enviado nas respostas do portal. |

## 4. Assets envolvidos

| Arquivo | Tipo | Onde é usado |
|---|---|---|
| `v5/public/assets/js/custom/config/obras-paralisadas/datatable.js` | JS próprio do módulo | CMS, via `@section('js')` do `index.blade.php` |
| `v5/public/assets/js/modulos/jspdf/gerar-pdf-obras-paralisadas.js` | JS próprio do módulo | Portal, `index.blade.php` de cada modelo |
| `v5/public/assets/js/modulos/jspdf/jspdf.umd.min.js` | vendor jsPDF | Portal |
| `v5/public/assets/js/modulos/jspdf/jspdf-autotable.min.js` | vendor jspdf-autotable | Portal |
| `v5/public/assets/js/custom/utilities/table.js` | JS compartilhado do CMS | `runDatatables`, `createBTN`, `addDelete` |
| `v5/public/assets/js/custom/utilities/functions.js` | JS compartilhado do CMS | `myFetch` (requisições AJAX com CSRF) |
| `v5/resources/views/portal/components/obras-paralisadas/table.blade.php` | partial Blade | Listagem pública (comum aos seis modelos) |

## 5. Acessibilidade e responsividade (observações)

- O botão "Cadastrar" usa `role="button"` redundante em um `<a>`.
- O campo de busca do portal tem `<label for="busca">`, e o input usa
  `autocomplete="off"`, que os navegadores modernos ignoram em formulários com
  token.
- A tabela da listagem pública usa `<tbody>` **dentro** do `@foreach` — o HTML
  fica com múltiplos `<tbody>` em vez de um só, o que é tolerado pelos
  navegadores mas invalida a estrutura semântica.
- O `Str::limit($obra->motivo_paralisacao, 80)` corta o texto no limite padrão
  de 80 caracteres, usando o sufixo de reticências padrão do Laravel.
- O botão de exclusão de anexo depende de `window.confirm`, que é bloqueado em
  alguns navegadores embutidos (iframes com sandbox) e não é customizável.
- A exclusão de anexo usa `alert()` em caso de erro, o que interrompe a
  navegação e não segue o padrão de toasts usado no resto do CMS.
- Não há rótulo `aria-label` nos botões de ícone da listagem do CMS (editar /
  excluir); a informação está apenas no `data-title` do tooltip.

## 6. Checklist de verificação manual no portal

1. Abrir `/obras-paralisadas` com a base vazia: a listagem exibe "Nenhuma
   informação encontrada!" e **nenhum** botão de exportação.
2. Com registros: os quatro botões de exportação aparecem.
3. Buscar por um termo sem acento e confirmar o resultado.
4. Buscar por um termo com acento e confirmar o comportamento descrito em
   `05-REGRAS-E-PORTABILIDADE.md`, seção 2.3.
5. Clicar em "Limpar campos" e observar que a lista **não** muda até clicar em
   "Consultar".
6. Exportar em `csv` e abrir no Excel: verificar a ausência de BOM e o
   separador `;`.
7. Exportar em `xls` e abrir: verificar se o Excel reconhece o HTML.
8. Exportar em `txt`: conferir o formato "Rótulo: valor".
9. Clicar em "pdf": confirmar que o arquivo `obras-paralisadas.pdf` é gerado
   **no navegador**, sem nova requisição ao servidor.
10. Abrir a ficha de uma obra e baixar um anexo: confirmar que o arquivo
    responde em `/storage/docs/<arquivo>`.
11. Remover um anexo pelo CMS e tentar baixar o arquivo antigo pela URL: o
    arquivo **continua acessível** (bug B04).
