# Fila de Espera SUS — Regras e Portabilidade

> Versão analisada: `v5`. Lista de armadilhas observadas no código, com o
> comportamento real (não o desejado) e o efeito para a reconstrução.

## 1. Regras de negócio observáveis

| # | Regra | Onde está | Efeito |
|---|---|---|---|
| R1 | Todo registro criado ou editado é publicado (`is_show = true`). | `store()` e `update()` do controller do CMS | O CMS não tem ação de despublicar; a única forma de retirar um item do portal é excluí-lo. |
| R2 | A listagem pública mostra apenas registros com `is_show = true`. | `FilterFilaEsperaSus::filter()` | Filtro aplicado na raiz da query. |
| R3 | A ordenação padrão do portal é `update_date` descendente e `specialty` ascendente. | `Portal\FilaEsperaSusController@index` | A mesma ordenação vale para a exportação. |
| R4 | A paginação do portal é fixa em 15 registros. | `paginate(15)` | Não há parâmetro de paginação configurável. |
| R5 | Os valores dos selects de filtro vêm de registros **publicados** e **distintos**. | `pluck('specialty', 'specialty')` e `pluck('priority_criteria', 'priority_criteria')` | Se um registro for despublicado/despublicável, a opção some do filtro. |
| R6 | O cartão SUS é mascarado na exibição (3 primeiros e 2 últimos dígitos). | Model (acessor), `DatatableController`, view de tabela do portal, view de detalhe, view de exportação | A máscara é implementada **cinco vezes**, com o mesmo algoritmo, em arquivos diferentes. |
| R7 | A exclusão é lógica (soft delete). | `SoftDeletes` no model | Registros excluídos somem do CMS e do portal, mas permanecem na tabela. |
| R8 | A exclusão em lote apaga os ids enviados. | `destroyAll()` | O contrato entre o JS e o controller está quebrado (ver A3). |
| R9 | O slug é `str_slug(especialidade-procedimento-id)`. | `store()` / `update()` | Garante unicidade prática (o id entra no slug). |
| R10 | Anexos são PDFs, com descrição livre, e o caminho relativo é guardado em JSON. | `FilaEsperaSusRequest` + controller | Não há tabela de anexos nem validação de tamanho. |
| R11 | A listagem do CMS é alimentada por `DB::table()`, fora do model. | `DatatableController@get_fila_espera_sus` | O escopo global de soft delete é reproduzido à mão com `whereNull('deleted_at')`. |
| R12 | A tabela do portal é renderizada em HTML puro, sem JavaScript. | `v5/resources/views/portal/components/fila-espera-sus/table.blade.php` | Não há ordenação, busca ou paginação client-side no portal. |

## 2. Armadilhas reais (bugs confirmados por leitura do código)

### A1 — Slug congelado no update

```php
$data['slug'] = str_slug($item->specialty . '-' . $item->procedure . '-' . $item->id);
$item->update($data);
```

`$item->specialty` e `$item->procedure` são os valores **já gravados**, não
os que vieram no formulário. Alterar a especialidade ou o procedimento
**não** muda o slug nem a URL pública do registro. Correção: calcular o slug
a partir de `$data`.

### A2 — Registro despublicado continua acessível por slug

```php
$item = FilaEsperaSus::where('slug', $slug)
    ->orWhere('id', is_numeric($slug) ? $slug : 0)
    ->where('is_show', true)
    ->firstOrFail();
```

Por precedência do SQL, a cláusula gerada é
`slug = :s OR (id = :i AND is_show = true)`. O `is_show` **não** protege o
caso do slug. Combinado com R1 (tudo é publicado), o efeito prático é
pequeno hoje, mas qualquer regra de despublicação no futuro vaza
imediatamente.

### A3 — Exclusão em lote sem efeito

O controller lê `$request->ids`:

```php
FilaEsperaSus::whereIn('id', $request->ids)->delete();
```

e o JavaScript envia `toDelete`:

```js
data: {
    _token: $('meta[name="csrf-token"]').attr('content'),
    toDelete: toDelete
}
```

(`v5/public/assets/js/custom/utilities/deleteAll.js`). Como `ids` chega
`null`, o `whereIn` é resolvido com array vazio e **nenhum registro é
excluído** — mas a resposta é `response()->json(true)` e a interface
mostra "Registro(s) deletados com sucesso!". Falso sucesso.

### A4 — PDF sem título e sem data

`PDF::loadView('exports.fila-espera-sus', compact('items', 'pdf'))` chama
`compact()` com a variável `$pdf` inexistente: o PHP 7.2 emite o aviso
`compact(): Undefined variable: pdf` e a variável não chega à view.
O bloco `@if(isset($pdf))` da view de exportação nunca é renderizado, e o
PDF sai sem o título "Fila de Espera SUS - {nome da prefeitura}" e sem a
data da pesquisa.

### A5 — Extensão do anexo não sanitizada

```php
$filename = Str::random(16) . '.' . $file->getClientOriginalExtension();
$file->move($destDir, $filename);
```

A regra `arquivo.* => mimes:pdf` valida o **conteúdo** do arquivo (extensão
inferida pelo `fileinfo`), não a extensão enviada pelo navegador. Um PDF
válido enviado com o nome `algo.php` é gravado como `xyz.php` dentro de
`public/storage/fila-espera-sus/`. Como o diretório fica sob o webroot, a
extensãooriginal do arquivo pode ser executada pelo servidor web.

### A6 — Anexos servidos do `public/` sem controle de acesso

O upload grava em `public_path('storage/fila-espera-sus')` e as views
montam a URL `url('storage/' . $path)`. Os PDFs ficam **publicamente
acessíveis** a quem conhecer o nome do arquivo (aleatório de 16 caracteres,
`Str::random(16)`, o que é aceitável como entropia) e ficam no diretório
servido pelo webserver, sem expiração, sem URL assinada e sem registro de
download. Documentos de saúde não devem sair do servidor de aplicação.

### A7 — Cartão SUS completo pesquisável e gravado em log

- O filtro `busca` do portal faz `sus_card ilike %termo%`: um visitante
  pode testar números de cartão e observar a diferença de resultados
  (enumeração de dados de saúde).
- `$logFillable = true` no model grava **todas** as colunas — inclusive
  `sus_card` e `contact` — no `properties` da tabela `activity_log`, que
  é lida pela tela de "Registros de Atividades" do CMS.

### A8 — `is_show` no `$fillable` e sempre sobrescrito

`is_show` está no `$fillable` do model e o controller sobrescreve o valor
com `true` — o que é o comportamento correto, mas impede que o CMS
despublique. Em uma reconstrução, `is_show` deve ser explícito na tela
(ou removido do `$fillable`).

### A9 — Consultas sem paginação no banco

- `DatatableController@get_fila_espera_sus` faz `->get()` na tabela
  inteira.
- O `index` do portal, no caminho de exportação, faz `->get()` em todos os
  registros filtrados e então gera XLSX/PDF/TXT. Com a tabela vazia hoje
  não há impacto; com volume real, há risco de exaustão de memória.

### A10 — Layout `default` sem o módulo

`getTemplateActived()` monta
`portal.layouts.{layout}.fila-espera-sus.index`. Só `modelo1` a `modelo6`
possuem o diretório do módulo; o layout `default` não. Trocar
`configurations.layout` para `default` derruba a rota do portal.

### A11 — Divergência Bootstrap 4 / Bootstrap 5 entre layouts

`modelo1` a `modelo4` usam `data-toggle` e `ml-3`; `modelo5` e `modelo6`
usam `data-bs-toggle` e `ms-3`. A reconstrução precisa escolher um único
framework de front para todos os layouts.

### A12 — Acessor de máscara não utilizado

`getSusCardMaskedAttribute()` (acessor `sus_card_masked`) existe no model
e **não é usado em lugar nenhum**; a máscara é recalculada em cinco
pontos. Código morto.

### A13 — `FileTrait` importado e não usado

`Prefecture\FilaEsperaSusController` usa `use FileTrait;` mas implementa o
upload na mão (`Str::random`, `$file->move`). O trait só é um resíduo.

### A14 — Permissões inexistentes para duas rotas

`fila-espera-sus.destroyAll` e `fila-espera-sus.destroy-arquivo` exigem
permissões que não existem na tabela `permissions` (só há 8 verbos CRUD).
Na prática, apenas `super-admin` consegue usar esses botões.

## 3. Acoplamentos

### 3.1 Dependências (o módulo usa)

| Dependência | Ponto de acoplamento |
|---|---|
| `CheckRoutePermission` | Nome das rotas e nomes dos recursos de permissão. |
| `has_access()` / `resolve_permission_aliases()` | Botões da view do CMS e flags por linha do DataTable. |
| `activity()` (spatie) e `LogsActivity` | Auditoria do model e da listagem. |
| `menuClicks()` | Contador de acessos em `menu_orders`. |
| `MenuServiceProvider` | Item de menu do CMS. |
| `DatatableController` | Endpoint JSON da tabela do CMS. |
| `Excel` / `dompdf` | Exportações do portal. |
| `Configuration` (`configurations.key = 'layout'`) | Escolha do layout do portal. |
| `Carbon` | Formatação de datas no formulário do CMS. |

### 3.2 Dependências (o módulo é usado por)

- `v5/routes/breadcrumbs.php` (trilha do portal).
- `v5/app/Providers/MenuServiceProvider.php:380-382` (menu do CMS).
- `v5/app/Http/Controllers/Prefecture/DatatableController.php` (JSON do CMS).
- Nenhum outro módulo do portal consome a tabela `fila_espera_sus`.

## 4. Código duplicado

| Trecho duplicado | Onde |
|---|---|
| Máscara do cartão SUS | Model, `DatatableController`, `portal/components/fila-espera-sus/table.blade.php`, `show.blade.php` (6 layouts) e `exports/fila-espera-sus.blade.php`. |
| Normalização do JSON de `file_path` (`array_map` sobre `is_array($i)`) | `update()`, `destroyArquivo()`, `form.blade.php`, `show.blade.php` (6 layouts). |
| Estrutura do formulário | `create.blade.php` e `edit.blade.php` são quase idênticos (diferem no `Form::open`/`Form::model` e no texto do botão). |
| Função `desabilitaBotao()` | Repetida em `create.blade.php` e `edit.blade.php`. |
| Telas do portal | 6 layouts × 3 views (18 arquivos) com o mesmo conteúdo e pequenas diferenças de markup. |

## 5. Onde a v6 divergiu

A v6 **removeu o módulo inteiro**. Não há controllers, model, request,
export, filtros, views de CMS, views de portal, rota de DataTable, rota de
portal nem migration equivalente. A tabela `fila_espera_sus` também não
existe no banco da v6, e o resource `fila-espera-sus` (id 17 na v5)
desapareceu do bloco de permissões. Detalhamento em `08-GAPS-E-DIVIDAS.md`.

## 6. Checklist de portabilidade para PHP 8 / Laravel 11-12

| Item legado | Situação em PHP 8 / Laravel 11-12 | Substituição obrigatória |
|---|---|---|
| `str_slug()` | Removido do Laravel 5.8+ e do PHP 8 | `Str::slug()` |
| `array_where()` (usado no middleware de permissão) | Removido no Laravel 5.8 | `array_filter()` ou Policy |
| `Form::number` / `Form::text` (laracasts/presenter 0.2) | Abandonado | Componentes Blade com `<input>` puro |
| `Cartalyst\Sentinel` | Não suporta PHP 8 | Laravel Breeze/Jetstream com `Gate`/`Policy` |
| `whereNull('deleted_at')` manual | Manter apenas se o model não for usado | Usar o model (escopo global de soft delete) |
| `spatie/laravel-activitylog` 2.8 | PHP 8 exige a linha 4.x | Reimplementar com `spatie/laravel-activitylog` 4.x ou auditoria própria |
| `maatwebsite/excel` 3.1 | PHP 8 exige a linha 3.1.5+ (Excel 1.x) | `maatwebsite/excel` 3.1 com PHP 8.1 ou `openspout` |
| `barryvdh/laravel-dompdf` 0.8 | PHP 8 exige 2.x | `barryvdh/laravel-dompdf` 2.x |
| `yajra/laravel-datatables` 1.5 | PHP 8 exige 10.x/11.x | `yajra/laravel-datatables` 11 |
| `str_slug` do slug com id | — | `Str::slug($specialty.'-'.$procedure.'-'.$id)` recalculado a partir dos **novos** valores |
