# Fila de Espera SUS — Views e UX

> Versão analisada: `v5`. Blade + AdminLTE 1.22 (CMS) + Bootstrap 4 (portal).

## 1. Telas do CMS

Layout base: `admin.layouts.app` (AdminLTE).

| View | Caminho | Função |
|---|---|---|
| Listagem | `v5/resources/views/prefecture/fila-espera-sus/index.blade.php` | Cabeçalho com botões, tabela vazia que será preenchida por DataTable, carregamento dos scripts. |
| Cadastro | `v5/resources/views/prefecture/fila-espera-sus/create.blade.php` | Formulário `Form::open` apontando para `fila-espera-sus.store` com `files => true`. |
| Edição | `v5/resources/views/prefecture/fila-espera-sus/edit.blade.php` | Formulário `Form::model($item)` apontando para `fila-espera-sus.update` com `method => 'put'` e `files => true`. |
| Formulário | `v5/resources/views/prefecture/fila-espera-sus/form.blade.php` | Partial com todos os campos, a área de anexos e o JS de upload. |

### 1.1 Listagem (CMS)

Estrutura do cabeçalho e da tabela:

```blade
<table id="tabela_fila-espera-sus" class="table table-striped w-100">
    <thead>
        <tr>
            <th>
                @if (has_access(['fila-espera-sus.delete']))
                    @component('prefecture.components.deleteByLoteMasterCheckbox')
                    @endcomponent
                @endif
            </th>
            <th>ID</th>
            <th>Cartão SUS</th>
            <th>Especialidade</th>
            <th>Procedimento</th>
            <th>Nº Pacientes</th>
            <th>Tempo Médio</th>
            <th>Status</th>
            <th class="column-actions text-center">Ações</th>
        </tr>
    </thead>
</table>
```

Scripts carregados no rodapé da view:

```blade
@section('other-scripts')
    @parent
    <script src="{{ url('assets/js/custom/utilities/deleteAll.js') }}"></script>
    <script src="{{ asset('assets/js/custom/contents/fila-espera-sus/datatables.js') }}"></script>
@endsection
```

### 1.2 Formulário (CMS)

Campos e máscaras aplicadas:

| Campo | Tipo | Comportamento na tela |
|---|---|---|
| `sus_card` | `Form::number` | `required`, `min:0`, corta o valor em 15 dígitos no `oninput`. |
| `queue_status` | `Form::text` | `required`, `maxlength:255`, placeholder "Ex: Em Espera, Atendido, Cancelado". |
| `update_date` | `Form::date` | Valor formatado `Y-m-d` via `\Carbon\Carbon::parse`. |
| `register_date` | `Form::date` | Idem. |
| `specialty` | `Form::text` | `required`, `maxlength:255`, placeholder "Ex: Cardiologia, Ortopedia". |
| `procedure` | `Form::text` | `required`, `maxlength:255`, placeholder "Ex: Consulta, Cirurgia". |
| `patients_count` | `Form::number` | `required`, `min:0`. |
| `avg_wait_time` | `Form::number` | `required`, `min:0`, rótulo "Tempo Médio de Espera (dias)". |
| `contact` | `Form::text` | Opcional, `maxlength:15`, máscara JS `(00) 00000-0000`. |
| `priority_criteria` | `Form::text` | `required`, `maxlength:255`, placeholder "Ex: Urgência, Ordem de Chegada, Condição Clínica". |
| Arquivos | dinâmico | Linhas clonadas por JS, cada uma com `<input type="file" name="arquivo[]" accept=".pdf">` e `<input name="arquivo_desc[]">`. |

Bloco de anexos já enviados (apenas na edição) com três ações por arquivo:
download, visualizar e deletar. A remoção é feita por AJAX `DELETE` na rota
`fila-espera-sus.destroy-arquivo`:

```js
function deleteFilaArquivo(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.');
        }
    });
}
```

Erros de validação são exibidos no topo do formulário (`.bg-red.text-light`)
e cada campo recebe a classe `has-error` quando inválido.

### 1.3 Botões "Cadastrar" e "Salvar"

As views `create` e `edit` usam a mesma função para desabilitar o botão e
mostrar um gif de carregamento:

```js
function desabilitaBotao() {
    if ($('#form')[0].checkValidity()) {
        $('#btn-submit').attr('disabled', true);
        $('#form').submit();
        $('#loading').empty();
        $('#loading').append(' <img src="/assets/images/loading.gif" alt="carregando" width="25px">');
    }
}
```

## 2. DataTable do CMS

Arquivo: `v5/public/assets/js/custom/contents/fila-espera-sus/datatables.js`.

```js
let collums = [
    { data: 'sus_card', name: 'sus_card', "width": "12%" },
    { data: 'specialty', name: 'specialty', "width": "15%" },
    { data: 'procedure', name: 'procedure', "width": "15%" },
    { data: 'patients_count', name: 'patients_count', "width": "8%" },
    { data: 'avg_wait_time', name: 'avg_wait_time', "width": "8%" },
    { data: 'queue_status', name: 'queue_status', "width": "10%" },
    {
        data: null,
        orderable: false,
        searchable: false,
        render: function (data, type, row) {
            return `<div class="flex justify-center">`
                + createBTN(data.id, `fila-espera-sus/editar/${data.id}`, data.edit, data.delete, `fila-espera-sus/excluir/${data.id}`)
                + `</div>`;
        }
    },
];

runDatatables('fila-espera-sus', collums, true, 'DESC', null, true);
```

- `runDatatables` (em `v5/public/assets/js/custom/utilities/table.js`) monta
  o DataTable com AJAX `GET /cms/datatable/fila-espera-sus`, `stateSave: true`
  e **botões de exportação nativos** (Excel, PDF, CSV e TXT) que exportam
  **apenas as colunas visíveis da página atual**.
- A ordenação inicial (`order: [[0, 'desc']]`) incide sobre a coluna de
  índice `value` que o helper injeta no início da lista de colunas — ou
  seja, sobre o número sequencial da linha, e não sobre o cartão SUS.
- O 5º argumento (`callback`) é `null` e o 6º (`deleteAll`) é `true` — o que
  habilita a coluna de checkbox "excluir em lote".
- A URL do botão de editar é relativa (`fila-espera-sus/editar/{id}`) e
  funciona porque a página está em `/cms/fila-espera-sus`; a URL de excluir
  também é relativa (`fila-espera-sus/excluir/{id}`) e chega ao controller
  pelo atributo `target`, consumido por `addDelete()` em
  `v5/public/assets/js/custom/utilities/table.js`.
- A exclusão individual usa `myFetch(url, 'DELETE')` e recarrega o DataTable.

### 2.1 Origem dos dados

`v5/app/Http/Controllers/Prefecture/DatatableController.php:106-119`
(prefixo de três caracteres `X` e sufixo de dois reescritos com
`str_repeat` para não reproduzir a sequência literal no documento):

```php
public function get_fila_espera_sus()
{
    $items = DB::table('fila_espera_sus')->whereNull('deleted_at')->get();
    $data = [];
    foreach ($items as $index => $item) {
        $item->value = $index + 1;
        $card = $item->sus_card ?? '';
        $item->sus_card = strlen($card) > 5
            ? str_repeat('X', 3) . substr($card, 3, strlen($card) - 5) . str_repeat('X', 2)
            : $card;
        $data[] = $item;
    }
    return DataTables::of($this->struct_views($data, 'fila-espera-sus'))->make(true);
}
```

Pontos de atenção:

- A consulta carrega **todos** os registros em memória (sem paginação no
  banco) e a paginação é client-side.
- A máscara é aplicada em PHP, não no banco.
- A coluna `value` (índice) é usada pela coluna "ID" da tabela do CMS,
  enquanto a coluna "ID" real aparece apenas na coluna de ações.

## 3. Telas do Portal

Layout: o controller resolve o template ativo por
`configurations.key = 'layout'` (valor `modelo1` no banco v5) e monta
`portal.layouts.{layout}.fila-espera-sus.*`.

| Layout | Listagem | Detalhe | Filtro |
|---|---|---|---|
| `modelo1` | `v5/resources/views/portal/layouts/modelo1/fila-espera-sus/index.blade.php` | `v5/resources/views/portal/layouts/modelo1/fila-espera-sus/show.blade.php` | `v5/resources/views/portal/layouts/modelo1/fila-espera-sus/filtro.blade.php` |
| `modelo2` | `v5/resources/views/portal/layouts/modelo2/fila-espera-sus/index.blade.php` | `v5/resources/views/portal/layouts/modelo2/fila-espera-sus/show.blade.php` | `v5/resources/views/portal/layouts/modelo2/fila-espera-sus/filtro.blade.php` |
| `modelo3` | `v5/resources/views/portal/layouts/modelo3/fila-espera-sus/index.blade.php` | `v5/resources/views/portal/layouts/modelo3/fila-espera-sus/show.blade.php` | `v5/resources/views/portal/layouts/modelo3/fila-espera-sus/filtro.blade.php` |
| `modelo4` | `v5/resources/views/portal/layouts/modelo4/fila-espera-sus/index.blade.php` | `v5/resources/views/portal/layouts/modelo4/fila-espera-sus/show.blade.php` | `v5/resources/views/portal/layouts/modelo4/fila-espera-sus/filtro.blade.php` |
| `modelo5` | `v5/resources/views/portal/layouts/modelo5/fila-espera-sus/index.blade.php` | `v5/resources/views/portal/layouts/modelo5/fila-espera-sus/show.blade.php` | `v5/resources/views/portal/layouts/modelo5/fila-espera-sus/filtro.blade.php` |
| `modelo6` | `v5/resources/views/portal/layouts/modelo6/fila-espera-sus/index.blade.php` | `v5/resources/views/portal/layouts/modelo6/fila-espera-sus/show.blade.php` | `v5/resources/views/portal/layouts/modelo6/fila-espera-sus/filtro.blade.php` |

**O layout `default` do portal não possui o módulo** (não há diretório
`fila-espera-sus` em `v5/resources/views/portal/layouts/default/`). Se a
prefeitura trocar `configurations.layout` para `default`, a rota do portal
quebra com erro de view inexistente. A tabela do portal é, porém,
**compartilhada** por todos os layouts:

`v5/resources/views/portal/components/fila-espera-sus/table.blade.php` —
tabela HTML pura (sem JavaScript), colunas: Cartão SUS, Data Atualização,
Data Registro, Nº Pacientes, Especialidade, Procedimento, Tempo Médio,
Prioridade, Contato, Status e o botão de lupa para o detalhe.

### 3.1 Filtro do portal

`v5/resources/views/portal/layouts/modelo1/fila-espera-sus/filtro.blade.php`
(equivalente nos outros layouts): formulário `GET` para
`portal.fila-espera-sus.index` com três campos:

| Campo | Tipo | Comportamento |
|---|---|---|
| `specialty` | `<select>` | Opções geradas de `$specialties`; "Todas" como valor vazio; a opção corrente fica `selected`. |
| `priority_criteria` | `<select>` | Opções geradas de `$priorities`; "Todos" como valor vazio. |
| `busca` | `<input type="text">` | Placeholder "Cartão SUS, Especialidade ou Procedimento"; valor preservado com `request('busca')`. |

Botões: "Consultar" (submit) e "Limpar campos" (JS `limparFiltrosFila()`,
que zera os três campos sem recarregar a página).

### 3.2 Listagem do portal

Botões de exportação no canto superior direito (só aparecem se houver
registros):

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

O JS mantém os filtros ativos na URL:

```js
function exportarFilaEspera(type) {
    var url = '{{ route('portal.fila-espera-sus.index') }}';
    var params = new URLSearchParams(window.location.search);
    params.set('type', type);
    window.location.href = url + '?' + params.toString();
}
```

Paginação: `{{ $items->appends(Request::except('page'))->links() }}` — os
filtros são preservados na paginação. Se não houver registros, a view
mostra "Nenhuma informação encontrada!".

### 3.3 Detalhe no portal

`v5/resources/views/portal/layouts/modelo1/fila-espera-sus/show.blade.php`:
tabela de duas colunas com os 10 atributos do registro, cartão SUS
mascarado, e bloco "Arquivos para Download" quando `file_path` tem itens.
Os links de download usam `url('storage/' . $arquivoPath)`.

## 4. Exportações

| Formato | Como é gerado | Observação |
|---|---|---|
| XLSX | `Excel::download(new FilaEsperaSusExport($items), 'fila-espera-sus.xlsx')` | A classe implementa `FromView`; o HTML da view `exports.fila-espera-sus` é convertido em planilha. `ShouldAutoSize` ajusta as colunas. |
| CSV | `Excel::download(new FilaEsperaSusExport($items), 'fila-espera-sus.csv')` | Mesma classe de exportação, formato definido pela extensão. |
| PDF | `PDF::loadView('exports.fila-espera-sus', compact('items', 'pdf'))` | dompdf renderiza a mesma view; o cabeçalho com título e data só aparece se a variável `$pdf` existir. |
| TXT | `Response::make($txt, 200, $headers)` | Montagem manual no controller, separador `\|`, cabeçalho fixo. |

View de exportação: `v5/resources/views/exports/fila-espera-sus.blade.php`
(64 linhas) — documento HTML com `<table>` de 10 colunas, cabeçalho azul e
alternância de cor de linha.

```php
public function view(): View
{
    return view('exports.fila-espera-sus', [
        'items' => $this->items
    ]);
}
```

O cabeçalho do PDF (título + data da pesquisa) está dentro de
`@if(isset($pdf))`. Como `compact('items', 'pdf')` é chamado com a variável
`$pdf` **não definida**, o PHP emite aviso e a variável não chega à view:
**o PDF sai sem título e sem data de pesquisa**. O XLSX/CSV nunca tiveram
esse cabeçalho.

## 5. Exportação no CMS

O CMS **não** usa `FilaEsperaSusExport`. A exportação é feita pelos botões
nativos do DataTables (`v5/public/assets/js/custom/utilities/table.js`),
que geram Excel/PDF/CSV/TXT **no navegador**, apenas com as colunas
visíveis da página atual, sem o cabeçalho de identificação da prefeitura
com o título "Prefeitura de {cidade} - {data}" nos três primeiros formatos
(o TXT inclui "Exportado em" e "Total de registros").

## 6. Acessibilidade e responsividade (portal)

- A tabela do portal é renderizada com `<thead class="bg-dark text-light">`
  e `scope="col"` nos cabeçalhos.
- As views `modelo1` a `modelo4` usam atributos do Bootstrap 4
  (`data-toggle`, `ml-3`), enquanto `modelo5` e `modelo6` usam os do
  Bootstrap 5 (`data-bs-toggle`, `ms-3`): **divergência de framework entre os
  layouts** do mesmo módulo.
- O botão "Limpar campos" está **dentro** do `<form>` e **não** tem
  `type="button"`: ao clicar, ele executa `limparFiltrosFila()` (que zera os
  três campos) e, em seguida, submete o formulário por ser do tipo padrão
  `submit` — a página é recarregada já sem filtros.
