# Licitantes Sancionados — Views e UX

> Todas as views existem **apenas na v5**.

---

## 1. Views do CMS

Diretório: `v5/resources/views/prefecture/licitantes-sancionados/`

| Arquivo | Função | Renderizado por |
|---|---|---|
| `index.blade.php` | Listagem (tabela vazia preenchida por JS) | `LicitanteSancionadoController@index` |
| `create.blade.php` | Formulário de cadastro | `@create` |
| `edit.blade.php` | Formulário de edição | `@edit` |
| `form.blade.php` | **Parcial** compartilhado entre create e edit | `@include` dentro de create/edit |

Todas estendem o layout AdminLTE:

```blade
{{-- v5/resources/views/prefecture/licitantes-sancionados/index.blade.php --}}
@extends('admin.layouts.app')
```

`admin.layouts.app` (`v5/resources/views/admin/layouts/app.blade.php`) estende
`adminlte::page`, carregando AdminLTE 1.22, jQuery, DataTables + Buttons, Toastr,
SweetAlert2 e os utilitários `assets/js/custom/utilities/table.js`, `exec.js` e
`functions.js`.

### 1.1 `index.blade.php` — o que o operador vê

- Caixa "Licitantes Sancionados" com título 24px e botão "Cadastrar" (verde) exibido
  apenas com `has_access(['licitante-sancionado.create'])`.
- Tabela `id="tabela_licitante-sancionado"` com cabeçalho: ID, Ano, Nº Sanção,
  Nome / Razão Social, CPF / CNPJ, Início, Fim, Ações. O `<tbody>` é vazio — o
  DataTable injeta as linhas.
- Bloco `@section('js')` carregando `assets/js/custom/config/licitantes-sancionados/datatable.js`.

> O cabeçalho declara uma coluna "ID", mas o `datatable.js` **não** define coluna de
> ID: a coluna `value` (row_number da view SQL) é inserida automaticamente por
> `runDatatables`. O cabeçalho e os dados estão desalinhados. Ver `08-GAPS-E-DIVIDAS.md`, G7.

### 1.2 `create.blade.php` / `edit.blade.php`

```blade
{{-- v5/resources/views/prefecture/licitantes-sancionados/create.blade.php --}}
{{ Form::open(['route' => 'licitante-sancionado.store', 'files' => true, 'id' => 'form']) }}
@include('prefecture.licitantes-sancionados.form')
<div class="box-footer">
    <button class="btn btn-primary btn-flat" id="btn-submit" onclick="desabilitaBotao()">
        {{ trans('adminlte::adminlte.register') }}
    </button>
    <span id="loading" class="ml-2"></span>
    <a class="float-right" href="{{ route('licitante-sancionado.index') }}">Voltar</a>
</div>
{{ Form::close() }}
```

```blade
{{-- v5/resources/views/prefecture/licitantes-sancionados/edit.blade.php --}}
{{ Form::model($sancionado, ['route' => ['licitante-sancionado.update', $sancionado->id], 'files' => true, 'method' => 'PUT', 'id' => 'form']) }}
```

O botão "Voltar" aponta para `licitante-sancionado.index` (singular) — nome correto.

O script `desabilitaBotao()` desabilita o botão e mostra um GIF de loading
(`/assets/images/loading.gif`) durante o envio.

### 1.3 `form.blade.php` — o formulário compartilhado

Campos (via componentes `v5/resources/views/components/inputs/`):

| Componente | Campo | Tamanho | Notas |
|---|---|---|---|
| `components.inputs.input` | `ano` | col-2 | `maxlen 4`, placeholder = ano corrente |
| `components.inputs.input` | `numero_sancao` | col-3 | `maxlen 50`, placeholder `Ex: 002/2026` |
| `components.inputs.input` | `nome_razao_social` | col-7 | `maxlen 255` |
| `components.inputs.input` | `cpf_cnpj` | col-5 | `maxlen 18` |
| `components.inputs.date` | `inicio_sancao` | col-4 | classe `input-date`, máscara `br-data`, formato `d/m/Y` |
| `components.inputs.date` | `fim_sancao` | col-3 | idem |

Regras condicionais de exibição (extraídas do componente `input.blade.php`):

```blade
{{-- v5/resources/views/components/inputs/input.blade.php (trecho) --}}
@if (isset($mandatory) && $mandatory == 'true')
    <strong class="text-red star"><i class="fa fa-asterisk"></i></strong>
@endif
```

Os componentes recebem `'mandatory' => true` (booleano). A comparação
`$mandatory == 'true'` no Blade converte `true` para `'1'`, que é **diferente** de
`'true'` — logo o asterisco de "campo obrigatório" **não é exibido**. Mesmo problema em
`components/inputs/date.blade.php`. Ver `08-GAPS-E-DIVIDAS.md`, G8.

Bloco de anexos:

- Uma div oculta `.description` (rótulo "Arquivos" + "Descrição") que é exibida via
  JS quando o primeiro arquivo é adicionado.
- `.itens-wrapper` com `.data-itens` inicial (vazio e oculto) onde os inputs são
  inseridos.
- Botão `#btn-adicionar` que insere dinamicamente:

```javascript
// v5/resources/views/prefecture/licitantes-sancionados/form.blade.php
'<input type="file" name="arquivo[]" class="form-control" accept=".pdf">'
'<input type="text" name="arquivo_desc[]" class="form-control" placeholder="Informe a descrição">'
```

- Exclusão de linha via `.js-remove-itens` (evento delegado em `.itens-wrapper`).

Arquivos já enviados (apenas na edição) são listados em `#files-done`, com
`basename($item['path'])` como rótulo, botões de **download** e **visualizar**
(`url('storage/' . $item['path'])`, `target="_blank"`) e botão de **excluir** que faz
`DELETE` AJAX para `route('licitante-sancionado.destroy-arquivo', [$sancionado->id, $arqIndex])`.

```javascript
// v5/resources/views/prefecture/licitantes-sancionados/form.blade.php
function deleteSancionadoArquivo(btn) {
    if (!confirm('Deseja realmente remover este arquivo?')) return;
    var $btn = $(btn);
    var url  = $btn.data('url');
    var 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.'); }
    });
}
```

O token CSRF vem de `<meta name="csrf-token">` presente em
`v5/resources/views/vendor/adminlte/master.blade.php:6`. O `$.ajax` envia o token
como **parâmetro de corpo** (`data: {_token: ...}`), e o `VerifyCsrfToken` do Laravel
lê `_token` do input — funciona.

### 1.4 Máscaras aplicadas no formulário

```javascript
// v5/resources/views/prefecture/licitantes-sancionados/form.blade.php
$('input[name="cpf_cnpj"]').on('input', function () {
    var val = $(this).val().replace(/[^A-Za-z0-9]/g, '');
    $(this).unmask();
    if (val.length <= 11 && /^\d+$/.test(val)) {
        $(this).mask('000.000.000-009');
    } else {
        $(this).mask('AA.AAA.AAA/AAAA-00');
    }
});
$('input[name="ano"]').mask('0000');
```

O CPF/CNPJ alterna entre máscara numérica (11 dígitos) e alfanumérica
(`AA.AAA.AAA/AAAA-00`) conforme o usuário digita — suporta o CNPJ alfanumérico da
Receita Federal. O `maxlength` efetivo do campo é 18, compatível com a máscara.

## 2. JS do DataTable (CMS)

Arquivo: `v5/public/assets/js/custom/config/licitantes-sancionados/datatable.js`

```javascript
let collums = [
    { data: 'ano', render: function (data) { return `<div class='vertical-middle' style='padding:8px'>${data ?? '-'}</div>`; } },
    { data: 'numero_sancao', ... },
    { data: 'nome_razao_social', ... },
    { data: 'cpf_cnpj', ... },
    { data: 'inicio_sancao', render: function (data) {
        if (!data) return '-';
        const d = new Date(data);
        return `<div class='vertical-middle' style='padding:8px'>${d.toLocaleDateString('pt-BR', {timeZone: 'UTC'})}</div>`;
    } },
    { data: 'fim_sancao', ... },
    { data: null, orderable: false, className: 'text-center', render: function (data) {
        return `<div class='flex justify-center'>`
            + createBTN(data.id, `licitantes-sancionados/editar/${data.id}`, data.edit, data.delete, `licitantes-sancionados/excluir/${data.id}`)
            + `</div>`;
    } }
];

runDatatables('licitante-sancionado', collums);
```

| Aspecto | Comportamento |
|---|---|
| Origem dos dados | `GET /cms/datatable/licitante-sancionado` (JSON do Yajra) |
| Ordenação inicial | `runDatatables` usa `order: [[0, 'asc']]` sobre a coluna `value` (row_number) |
| Busca | DataTables client-side sobre o JSON completo (o backend **não** pagina) |
| Exportação | Excel, PDF, CSV e TXT (botões padrão de `runDatatables` em `v5/public/assets/js/custom/utilities/table.js`) |
| Estado | `stateSave: true` (o DataTable memoriza página/ordenamento no `localStorage`) |
| Exclusão | `addDelete()` em `table.js` faz `DELETE` na URL do atributo `target` e mostra `toastr` |
| Datas | Convertidas com `toLocaleDateString('pt-BR', {timeZone: 'UTC'})` — timezone fixo em UTC para não deslocar o dia |

Os botões de linha usam **URLs relativas** (`licitantes-sancionados/editar/{id}`),
o que resolve para `/cms/licitantes-sancionados/editar/{id}` porque a página atual
está em `/cms/licitantes-sancionados`. A URL de exclusão é passada como atributo
`target` (parâmetro `del_url` de `createBTN`) e consumida por `addDelete()`.

## 3. Views do portal

### 3.1 Organização

O portal resolve a view dinamicamente pelo template ativo
(`configurations.layout` = `modelo1` no banco v5):

```php
// v5/app/Http/Controllers/Portal/LicitantesSancionadosController.php:44-48
return view(
    'portal.layouts.' . $this->getTemplateActived() . '.licitacao.sancionados.index',
    compact('title', 'result', 'anos', 'anoSelecionado', 'busca', 'dtInicio', 'dtFim')
);
```

Arquivos por template (`v5/resources/views/portal/layouts/<modelo>/licitacao/sancionados/`):

| Template | `index.blade.php` | `show.blade.php` | `filtro.blade.php` |
|---|---|---|---|
| `modelo1` | ✔ | ✔ | ✔ |
| `modelo2` | ✔ | ✔ | ✔ |
| `modelo3` | ✔ | ✔ | ✔ |
| `modelo4` | ✔ | ✔ | ✔ (versão **reduzida**, sem datas) |
| `modelo5` | ✔ | ✔ | ✔ |
| `modelo6` | ✔ | ✔ | ✔ |
| `default` | ✖ | ✖ | ✖ |
| `newsbit` | ✖ | ✖ | ✖ |

O template `default` **não tem** pasta `licitacao/`; o `newsbit` tem
`licitacao/`, mas **sem** `sancionados/`. Se a prefeitura configurar um desses
layouts, a rota do portal retorna *View not found*.

### 3.2 `index.blade.php` (portal)

```blade
{{-- v5/resources/views/portal/layouts/modelo1/licitacao/sancionados/index.blade.php --}}
@extends('portal.layouts.modelo1.layout.layout')

@section('breadcrumb')
    {{ Breadcrumbs::view('portal.layouts.modelo1.layout.breadcrumb', 'licitantes-sancionados') }}
@endsection

@section('content')
    <section id="sancionados" class="bg0 p-b-110 w-100">
        <p class="h4 text-uppercase txt-color-2 font-or">Licitantes Sancionados</p>
        <i class="border-title d-block bg-color-1 mb-4"></i>
        <div class="p-b-30">
            <div class="p-r-10 p-r-0-sr991">
                @include('portal.layouts.modelo1.licitacao.sancionados.filtro')
                <br>
                @include('portal.components.sancionados.table')

                @if ($result->isEmpty())
                    <div class="text-center mt-3">
                        <p>Nenhuma informação encontrada!</p>
                    </div>
                @endif
            </div>
        </div>
    </section>
@endsection
```

A **tabela** é um componente **compartilhado** entre todos os templates:
`v5/resources/views/portal/components/sancionados/table.blade.php`. Já o `index` e o
`show` são **copiados** template a template (só o `@extends` e a linha do
`Breadcrumbs::view` mudam; o `include` do filtro aponta para o template local).

### 3.3 `filtro.blade.php` (card de filtros)

```blade
{{ Form::open(['route' => 'licitantes-sancionados.index', 'method' => 'GET', 'id' => 'form-filter']) }}
```

| Campo | Query param | Tipo | Comportamento |
|---|---|---|---|
| Ano | `ano` | `<select>` | "Todos" + anos distintos existentes (`$anos`), ordenado desc |
| Buscar por Nome ou CPF/CNPJ | `busca` | `<input type="text">` | Filtro textual do controller |
| Início da Sanção — De | `dt_inicio` | `<input type="date">` | `whereDate('inicio_sancao','>=', ...)` |
| Início da Sanção — Até | `dt_fim` | `<input type="date">` | `whereDate('inicio_sancao','<=', ...)` |

Botões: "Consultar" (submit) e "Limpar campos" (`limparFiltros()` em JS, que zera
todos os campos do formulário).

**Diferenças entre templates** (verificado com `diff`):

- `modelo4`: o filtro **não tem** os campos `dt_inicio`/`dt_fim`; o layout do card
  usa `col-md-3` para Ano e `col-md-9` para a busca (contra `col-md-2`/`col-md-4` no
  `modelo1`).
- `modelo5` e `modelo6`: o bloco de botões usa `d-flex justify-content-end` e
  inverte a ordem (Limpar antes de Consultar); a função `limparFiltros()` zera
  apenas `ano` e `busca`.
- `modelo1`, `modelo2`, `modelo3`: idênticos entre si (só o `include` muda).

Consequência: **os filtros de data só existem em 4 de 6 templates**, e o comportamento
do botão "Limpar campos" varia. Ver `08-GAPS-E-DIVIDAS.md`, G9.

### 3.4 `portal/components/sancionados/table.blade.php` (tabela compartilhada)

| Coluna | Origem |
|---|---|
| Ano | `$sancionado->ano` |
| Nº Sanção | `$sancionado->numero_sancao` |
| Nome / Razão Social | `$sancionado->nome_razao_social` |
| CPF / CNPJ | `$sancionado->cpf_cnpj` |
| Início | `\Carbon\Carbon::parse($sancionado->inicio_sancao)->format('d/m/Y')` |
| Fim | `\Carbon\Carbon::parse($sancionado->fim_sancao)->format('d/m/Y')` |
| Detalhes | Link para `route('licitantes-sancionados.show', $sancionado->slug)` (ícone de lupa) |

Paginação no rodapé:

```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 os filtros na paginação.

### 3.5 `show.blade.php` (ficha)

Tabela com Ano, Número da Sanção, Nome / Razão Social, CPF / CNPJ, Início da Sanção e
Fim da Sanção. Abaixo, o card "Arquivos para Download" lista os anexos:

```blade
@if ($sancionado->arquivo)
    @php
        $arquivos = json_decode($sancionado->arquivo, true) ?: [$sancionado->arquivo];
        $arquivos = array_map(function ($i) { return is_array($i) ? $i : ['path' => $i, 'description' => '']; }, $arquivos);
    @endphp
    <div class="card p-3 mb-3">
        <h4 title="Arquivos" 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" ...>
                        <small class="d-inline-block ml-3 pt-1">{{ !empty($item['description']) ? $item['description'] : basename($item['path']) }}</small>
                    </a>
                </li>
            @endforeach
        </ul>
    </div>
@endif
```

O rótulo do link é a **descrição** quando informada, senão o nome do arquivo. No
final, um bloco com "Voltar" para `licitantes-sancionados.index`.

## 4. O que o cidadão vê (resumo)

1. `/licitantes-sancionados` → título, card de filtros, tabela paginada (15/página).
2. Filtros: Ano, busca por nome/CPF-CNPJ, intervalo de data de início da sanção.
3. Clique na lupa → `/licitante-sancionado/{slug}` → ficha + card de downloads.
4. Sem resultados → "Nenhuma informação encontrada!" (no `index`) ou
   "Nenhum dado encontrado." (na tabela, no `<tbody>` alternativo).
5. **Sem** menu no portal (não há entrada em `menu_items`/`links` no banco v5) e
   **sem** exportação.

## 5. O que o operador vê (resumo)

1. Menu "Licitações" → "Sancionados" (`/cms/licitantes-sancionados`).
2. DataTable com Excel/PDF/CSV/TXT, colunas Ano, Nº Sanção, Nome, CPF/CNPJ, Início,
   Fim e ações por linha.
3. Botão "Cadastrar" → formulário com máscaras de CPF/CNPJ, ano e data.
4. Bloco dinâmico de anexos PDF com descrição; já enviados com download/visualizar/excluir.
5. Exclusão por linha → `DELETE` → `toastr` de sucesso e recarga do DataTable.

## 6. Exportações

| Onde | Formatos | Como é gerado |
|---|---|---|
| CMS | Excel, PDF, CSV, TXT | Botões padrão de `runDatatables` (`v5/public/assets/js/custom/utilities/table.js`) — **client-side**, sobre os dados já carregados no navegador |
| Portal | — | Não há exportação |

Como o backend retorna **todos** os registros de uma vez (`DB::table(...)->get()`), a
exportação do CMS inclui a totalidade da base, não apenas a página visível.

## 7. Layouts do portal utilizados

| Template | Layout Blade | Breadcrumb |
|---|---|---|
| `modelo1` | `portal.layouts.modelo1.layout.layout` | `portal.layouts.modelo1.layout.breadcrumb` |
| `modelo2`..`modelo6` | `portal.layouts.modeloN.layout.layout` | `portal.layouts.modeloN.layout.breadcrumb` |

Os "layouts" citados no brief (`modelo1..modelo5`, `default`) correspondem, no código,
a `portal/layouts/<modelo>/layout/layout.blade.php` — o diretório `layout/` dentro de
cada template.
