# Obras — Views, UX, JS e Exportações

> Versão analisada: `v5`. Layout do CMS: AdminLTE 1.22 (`admin.layouts.app`).
> Layout do portal: Bootstrap 4 (`portal.layouts.modelo1..modelo6`).

---

## 1. Views do CMS

| Arquivo | Função |
|---|---|
| `v5/resources/views/prefecture/works/index.blade.php` | Listagem em DataTable + botões "Categorias de Obras" e "Criar Obra". |
| `v5/resources/views/prefecture/works/create.blade.php` | Wrapper de cadastro (`Form::open(['route' => ['obras.store'], 'files' => true])`). |
| `v5/resources/views/prefecture/works/edit.blade.php` | Wrapper de edição (`Form::open(['route' => ['obras.update', id], 'method' => 'PUT'])`) + script de galeria. |
| `v5/resources/views/prefecture/works/form.blade.php` | Formulário compartilhado entre create/edit (392 linhas). |
| `v5/resources/views/prefecture/works/categories/index.blade.php` | Cadastro de categoria + DataTable de categorias. |
| `v5/resources/views/prefecture/works/categories/form.blade.php` | Campo único `name`. |
| `v5/resources/views/prefecture/works/categories/edit.blade.php` | Wrapper de edição de categoria. |

### 1.1 `index.blade.php` (CMS)

Estrutura: cabeçalho "Lista de Obras", botões condicionais a
`has_access(['obras.create'])` e a tabela `#tabela_listagem_obras` com colunas
**ID, Título, Categoria, Situação, Valor Total, Valor Pago, Ações**. O corpo
`<tbody>` é preenchido pelo DataTable (server-side via view SQL).

Scripts carregados: `v5/public/assets/js/custom/utilities/deleteAll.js` e
`v5/public/assets/js/custom/contents/obras/datatable.js?v=0.0.1`.

### 1.2 `form.blade.php` (CMS) — o coração do módulo

Abas: "Obra" (sempre) e "Galeria" (somente se `isset($data) && $data->gallery == 1`).

Campos do formulário, na ordem em que aparecem:

| Campo | Tipo | Obrigatório | Observação |
|---|---|---|---|
| `status_id` | select | sim | Opções de `WorkStatus`; controlado por `justificationStatusIds`. |
| `justification` | textarea (4 linhas) | condicional | Aparece só quando a situação é Suspensa ou Cancelada. |
| `title` | input | sim | |
| `description` | textarea (3 linhas) | não | |
| `work_category_id` | select | sim | Opções de `WorkCategory`. |
| `edital` | input | sim | |
| `contract` | input | sim | |
| `value` | number (step 0.01, prefixo "R$") | sim | |
| `value_paid` | number (step 0.01, prefixo "R$") | sim | |
| `start_date` | date (máscara `br-data`, `d/m/Y`) | sim | |
| `end_date` | date (máscara `br-data`) | não | |
| `estimated_completion_date` | date (máscara `br-data`) | não | |
| `completion_percentage` | number (min 0, max 100) | não | |
| `cep` | input (máscara `cep`) | sim | Dispara ViaCEP + geocoding no `blur`. |
| `state` | input | sim | |
| `city` | input | sim | |
| `neighborhood` | input | sim | |
| `street` | input | sim | |
| `house_number` | number | sim | Dispara geocoding no `blur`. |
| `complement` | input | não | |
| `latitude` | input | não | Preenchido pelo geocoding. |
| `longitude` | input | não | Preenchido pelo geocoding. |
| `company_holder` | input | sim | |
| `holder` | input | sim | |
| `gallery` | checkbox | — | "Ativar Galeria"; o `value` é `on`. |
| arquivos | dinâmico (JS) | — | Par *input file* + *input de descrição*. |

Bloco de arquivos enviados (visível só na edição, quando há `$archives`):
ícone, título, descrição e três botões — **download**, **visualizar** e
**excluir** (`deleteFile(id)`).

> **Divergência de validação do tamanho de arquivo:** a tela anuncia
> "Tamanho máximo: 128MB", mas o `SaveRequest` limita a **2048 KB (2 MB)** e
> restringe as extensões a `jpg,jpeg,png,pdf,doc,docx,xls,xlsx,zip`.

O `justification` é controlado por um bloco `@php` no Blade:

```php
$justificationStatusIds = $statuses->whereIn('name', ['Suspensa', 'Cancelada'])->pluck('id')->values()->toArray();
$currentStatusId = isset($data) ? (int) data_get($data, 'status_id') : (int) old('status_id');
$showJustification = in_array($currentStatusId, $justificationStatusIds);
```

Esse array é injetado no JS por
`<script>const justificationStatusIds = @json($justificationStatusIds);</script>`.

### 1.3 `edit.blade.php` (CMS) — galeria

Além do formulário, o `edit.blade.php` carrega
`v5/public/assets/plugins/jquery-upload-files/jquery.uploadfile.min.js` e
`v5/public/assets/js/custom/contents/midias/index.js` e traz um `<script defer>` que:

1. lê `let goto = "{{ $goto }}";` e, se for `galeria`, clica na aba
   `#galeria`;
2. chama `chargeGallery({{ $data->id }})`, que faz `GET /cms/galerias-obras/{id}`
   e carrega as imagens em lotes de 50 com **scroll infinito**
   (`IntersectionObserver` sobre um sentinel `.sentinel`);
3. chama `setupGallery({{ $data->id }})` → `setupUpload()`, que inicializa o
   `#multipleupload` (jQuery Upload File) com
   `url: "/cms/galerias-obras"`, `acceptFiles: "image/jpeg, image/png, image/jpg"`
   e `formData: { _token, id }`.

### 1.4 `v5/resources/views/prefecture/works/categories/index.blade.php` (CMS)

Formulário de cadastro de categoria no topo (mesma tela) + DataTable
`#tabela_listagem_categorias_obras` com colunas **ID, Nome, Ações**.

## 2. Views do portal

### 2.1 Organização por template

O controller resolve a view dinamicamente:

```php
return view('portal.layouts.' . $this->getTemplateActived() . '.obras.index', ...);
```

`getTemplateActived()` (`v5/app/Http/Controllers/Admin/Controller.php:39`) lê
`Configuration('layout')`. O valor atual no banco é **`modelo1`**.

| Template | `index.blade.php` | `show.blade.php` | `filtro.blade.php` |
|---|---|---|---|
| `modelo1` | sim | sim | **não** — usa o componente compartilhado |
| `modelo2` | sim | sim | **não** — usa o componente compartilhado |
| `modelo3` | sim | sim | sim |
| `modelo4` | sim | sim | sim |
| `modelo5` | sim | sim | sim |
| `modelo6` | sim | sim | sim |

> **Os templates `default`, `newsbit`, `barra-privacidade`, `busca-diarias`,
> `calendar-events`, `contratos`, `diario-oficial`, `dynamics-forms` e
> `surveys` NÃO têm pasta `obras/`.** Se `Configuration('layout')` for
> apontado para qualquer um deles, o portal de obras quebra com "view not
> found". O `getTemplateActived()` aborta com 404 (`view_inexistente`) quando
> `portal.layouts.{layout}.home.index` não existe — e nenhum dos templates sem
> pasta `obras/` tem `home/`, o que protege o caso por acidente. Na prática,
> **só `modelo1..modelo6` atendem o portal de obras**.

### 2.2 Diferenças entre `modelo1..modelo6`

| Item | modelo1 | modelo2 | modelo3 | modelo4 | modelo5 | modelo6 |
|---|---|---|---|---|---|---|
| `extends` | `portal.layouts.modelo1.layout.layout` | `...modelo2...` | `...modelo3...` | `...modelo4...` | `...modelo5...` | `...modelo6...` |
| Filtro | `portal.components.obras.filtro` | `portal.components.obras.filtro` | `layouts.modelo3.obras.filtro` | `layouts.modelo4.obras.filtro` | `layouts.modelo5.obras.filtro` | `layouts.modelo6.obras.filtro` |
| `<section>` | `class="bg0 p-b-110 w-100"` | idem | idem | idem | idem | `id="licitacoes"` |
| Bloco extra | — | — | — | — | link "Ir para licitações até 2018" (`$oldBidding`) | — |
| Filtro: wrapper | `<div class="card p-3">` | idem | `<div class="card p-3 border-color-3">` + CSS próprio | `<div class="card p-3">` + CSS próprio | idem | `<div class="col-sm-12 px-0"><div class="card p-3">` |

> `v5/resources/views/portal/layouts/modelo5/obras/index.blade.php:17` referencia `$oldBidding`, variável que
> **nunca é enviada** pelo controller — o `isset()` protege e o bloco nunca
> aparece.

### 2.3 Componentes compartilhados do portal

| Arquivo | Função |
|---|---|
| `v5/resources/views/portal/components/obras/filtro.blade.php` | Formulário de filtros (De/Até, Status, Contrato, Categoria, Edital) + funções JS `exportar(type)` e `toCleanFields()`. |
| `v5/resources/views/portal/components/obras/tabela.blade.php` | Tabela de listagem com microdata (`itemprop="status"`, `"title"`, `"contract"`, `"edital"`, `"start_date"`, `"end_date"`). |
| `v5/resources/views/portal/components/obras/arquivos.blade.php` | Bloco "Arquivos para Download" da ficha pública. |
| `v5/resources/views/portal/components/galerias/index.blade.php` | Galeria de fotos da ficha pública (lightGallery), filtrando por extensão de imagem. |

`exportar()` no filtro:

```javascript
function exportar(type) {
    var valores = $('#form-filter').serialize();
    valores = valores + '&type=' + type;
    window.location.href = 'exportacao-obras?' + valores;
}
```

> A URL é **relativa** (`exportacao-obras?...`). Ela resolve para
> `/exportacao-obras?...` a partir da raiz do site — funciona porque a rota do
> portal é `exportacao-obras` (sem prefixo). Se o portal fosse hospedado em
> subpasta, a URL quebraria.

`toCleanFields()` limpa `dt_inicial`, `dt_final`, `status`, `contract` e
`category` — **não limpa `edital`**.

### 2.4 Ficha pública (`show.blade.php`)

Blocos (todos com `data_get()`/acesso direto):

| Bloco | Origem |
|---|---|
| Situação | `$work->status->name`, com classe de cor calculada comparando com strings **minúsculas** |
| Motivo/Justificativa | `$work->justification` (só se não vazio) |
| Categoria | `$work->workCategory->name` |
| Título | `$work->title` |
| Descrição | `$work->description` (só se não vazio) |
| Edital / Contrato | `$work->edital` / `$work->contract` |
| Valor R$ / Valor Pago R$ | formatados com `number_format((float) str_replace(['.', ','], ['', '.'], ...), 2, ',', '.')` |
| Percentual Concluído | barra de progresso com `completion_percentage` |
| Data de início / fim / previsão | `date('d/m/Y', strtotime(...))` |
| CEP / Estado / Cidade / Bairro / Rua / Número / Complemento | campos simples |
| Responsável da Empresa / da Prefeitura | `company_holder` / `holder` |
| Arquivos para Download | include de `v5/resources/views/portal/components/obras/arquivos.blade.php` |
| Galeria de fotos | include de `v5/resources/views/portal/components/galerias/index.blade.php` (só se `gallery == true`) |

> **Bug de comparação de situação:** o Blade compara
> `data_get($work, 'status.name') == 'aberta'` e
> `== 'em andamento'`, mas as situações no banco são `Aberta` e `Em Andamento`
> (com maiúsculas). A comparação **nunca** é verdadeira, então **todas** as
> obras caem no estilo `text-secondary` — as cores `text-success` (Aberta) e
> `text-warning` (Em Andamento) são código morto.
>
> **Acesso direto a `$work->status->name` e `$work->workCategory->name`:** se a
> FK for posta em `NULL` (ON DELETE SET NULL), a ficha **estoura erro 500** por
> acesso a propriedade de `null`.

## 3. JavaScript

### 3.1 `v5/public/assets/js/custom/contents/obras/datatable.js`

Define as colunas do DataTable (Título, Categoria, Situação, Valor Total, Valor
Pago e a coluna de ações) e chama:

```javascript
runDatatables('listagem_obras', collums, true, 'ASC', null, false);
```

`runDatatables` (`v5/public/assets/js/custom/utilities/table.js`) faz
`collums.unshift({data: 'value', ...})` — a coluna "ID" da tabela é o
`row_number()` da view SQL, **não** o `id` real (que é usado nas ações).

A coluna de ações monta:
- um link externo para `/obras/{id}` (ficha pública), via `createSingleButton`;
- `createBTN(data.id, '/cms/obras/{id}/edit', data.edit, data.delete, '/cms/obras/{id}')`
  → botão de editar + botão de excluir (com `data-target` apontando para a rota
  de DELETE).

### 3.2 `v5/public/assets/js/custom/contents/obras/categorias-datatable.js`

```javascript
runDatatables('listagem_categorias_obras', collums, true, 'ASC', null, false);
```

Colunas: Nome + Ações (`createBTN(data.id, '/cms/categorias-obras/{id}/edit', data.edit, data.delete, '/cms/categorias-obras/{id}')`).

### 3.3 `v5/public/assets/js/custom/contents/obras/script.js` (279 linhas)

| Bloco | O que faz |
|---|---|
| `error(message)` / `errorBox` / `errorInput` | Exibe erros de validação no `#error-area`. |
| `loadRepresentation()` | Cria o `<img>` de "Carregando..." (`/assets/images/loading.gif`). |
| `blur` no `#cep` | Chama `https://viacep.com.br/ws/{cep}/json/`; em sucesso preenche `state`, `city`, `neighborhood`, `street`; depois chama `getGeolocationData()` com o endereço completo. Em `data.erro` → "CEP não encontrado."; em erro de rede → "Erro de rede.". Durante a chamada desabilita os inputs preenchidos. |
| `blur` no `#house_number` | Chama `getGeolocationData()` com o endereço incluindo o número. |
| `getGeolocationData(endereco)` | `GET /cms/obras/get-geolocation-data?address=...`; escreve `lat`/`lng` ou a string **"não encontrado"** (que é enviada ao servidor e reprovada pela regra `max:20`? — "não encontrado" tem 14 caracteres, passa). |
| `#btn-adicionar` | Clona um par *input file* (`name="archive[N][file]"`) + *input de descrição* (`name="archive[N][description]"`) dentro de `.itens-wrapper`. |
| `showButton(id)` / `setArchive(button, id)` | Controle do toggle "Ativar exibição na página" (`archive[][view]`). |
| `toggleJustification()` | Mostra/esconde `#justification-wrapper` conforme `justificationStatusIds`. |
| `deleteFile(id)` | `POST /cms/galeria/delete/{id}` (rota do módulo `archives`) e remove o elemento `#file-{id}` da tela. |

> **O `script.js` de obras é uma cópia adaptada do
> `v5/public/assets/js/custom/utilities/helpers/add_archive.js`** — este último **não**
> é carregado pela view de obras, e o `script.js` reimplementa `showButton`,
> `setArchive` e o handler do `#btn-adicionar`. As duas implementações divergem
> no `name` do input de arquivo (`archive[N][file]` no de obras × `archive[][]`
> no helper genérico).
>
> **`getGeolocationData` escreve a string `"não encontrado"`** em latitude e
> longitude quando o geocoding falha. Essa string é enviada ao servidor e
> **persistida** em `works.latitude`/`works.longitude`, o que polui o mapa
> (as linhas com latitude não-nula mas inválida entram no `$forMap`).

### 3.4 `v5/public/assets/js/custom/portal/work/geo.js`

Mapa Leaflet (via `LeafletGeo`):

1. Mostra um overlay "Carregando dados do IBGE...";
2. Se `obras.length === 0 || !estado` → mensagem "Nenhuma obra encontrada para
   exibir no mapa.";
3. Centraliza o mapa no estado (`latLongBrStates[estado]`, deslocado em
   `-0.1 / -1.2`, zoom 8);
4. Camada de satélite do Google (`http://{s}.google.com/vt/lyrs=s&...`);
5. Um marcador por obra, com popup contendo Obra, Categoria, Situação, Valor,
   Valor Pago e link "Mais detalhes" para `/obras/{id}`; ao clicar, o mapa
   centraliza no marcador (zoom 18);
6. Busca as malhas do IBGE (`servicodados.ibge.gov.br/api/v4/malhas/estados/{uf}`)
   e a lista de municípios, com **cache em `localStorage`** (`geojson.{uf}` e
   `cities.{uf}`), e desenha o contorno do município via `L.geoJSON`.

> A camada de satélite usa **HTTP** (não HTTPS) — mixed content em páginas
> HTTPS.
>
> O `$forMap` que alimenta o mapa traz `value` e `value_paid` já prefixados
> com "R$ ", mas o `gerar-pdf-obras.js` espera `element.value` e
> `element.paid_value` — nomes diferentes.

### 3.5 `v5/public/assets/js/modulos/jspdf/gerar-pdf-obras.js`

```javascript
window.jsPDF = window.jspdf.jsPDF;

function gerarPdf() {
    const doc = new jsPDF();

    const head = ['Título', 'Categoria', 'Situação', 'Valor', 'Valor Pago', 'Data Início', 'Data Fim'];

    let i = false;
    doc.autoTable({
        head: head,
        body: obras.map(element => [
            element.title,
            element.category,
            element.status,
            element.value,
            element.paid_value,
            element.start_date,
            element.end_date,
        ]),
        startY: doc.lastAutoTable.finalY,
        styles: { fontSize: 6, }
    })

    doc.save('licitações.pdf');
}
```

> **Defeitos confirmados:**
> 1. `element.paid_value` **não existe** no payload — o PHP produz
>    `value_paid`. A coluna "Valor Pago" sai **vazia** no PDF.
> 2. `doc.save('licitações.pdf')` — nome de arquivo **copiado do módulo de
>    licitações**.
> 3. `startY: doc.lastAutoTable.finalY` — na **primeira** execução
>    `lastAutoTable` é `false` (inicializado pela biblioteca), e
>    `false.finalY` é `undefined`; o `getStartY()` do autotable cai no default
>    (`margin.top`), então funciona por acidente.
> 4. `head` tem 7 títulos e o body produz 7 valores, mas o `head` não
>    corresponde exatamente aos campos (`start_date`/`end_date` já vêm
>    formatados `d/m/Y` do PHP, ok).
> 5. O PDF usa o array `obras` injetado no HTML
>    (`const obras = JSON.parse('@json($forMap->toArray())');`), que contém
>    **apenas as obras com latitude e longitude** — as obras sem
>    georreferenciamento **não aparecem no PDF**.


## 5. Resumo do que o usuário vê

| Persona | Tela | Ações disponíveis |
|---|---|---|
| Servidor (CMS) | `/cms/obras` | DataTable; criar; editar; excluir; cadastrar categoria; anexar arquivos; subir fotos na galeria; consultar CEP e geocodificação automática. |
| Servidor (CMS) | `/cms/categorias-obras` | Criar categoria; editar; excluir (bloqueado se houver obras vinculadas). |
| Cidadão (portal) | `/obras` | Filtrar; ver mapa; abrir a ficha; exportar xls/pdf/csv/txt. |
| Cidadão (portal) | `/obras/{id}` | Ler dados; baixar arquivos; ver galeria de fotos. |

---

> **Continua em `10-EXPORTACOES-E-ENDPOINTS.md`:** a tabela de **endpoints
> consumidos pelo front** (DataTable, geocodificação, galeria, exclusão de
> arquivo, APIs externas do ViaCEP/Google/IBGE) e o detalhamento das
> **exportações** (Excel/CSV do servidor via `WorksExport`, TXT gerado à mão e
> o PDF do navegador com jsPDF).
