# Formulários Dinâmicos — Views e UX

> **Slug:** `infraestrutura-formulariosdinamicos` · **Versão analisada:** `lotep`
> **Raiz de referência:** `/home/one/projects/maxima/v7/lotep`

---

## 1. Namespace e layouts

O provider registra as views do serviço como `v2-service-formulariodinamico`
(`FormularioDinamicoServiceProvider.php:91`). As telas usam dois layouts do Core:

| Superfície | Layout | Arquivo |
|---|---|---|
| Portal | `v2-portal::app-layout` | `V2/Services/FormularioDinamico/Views/portal/form.blade.php:1-11` |
| CMS | `v2-cms::layouts.app` | `V2/Services/FormularioDinamico/Views/cms/formularios/index.blade.php:76-79` |

O portal desliga `showMainHeader` e `showMainLeftArea`/`showMainRightArea`; o formulário ocupa a
área principal e renderiza o próprio `<h1>`, breadcrumb e conteúdo.

## 2. Portal — `portal/form.blade.php`

Responsabilidades da view:

1. monta breadcrumb (Início → título) e `<h1>`;
2. exibe resumo de erros em `alert-danger` com `role="alert"` quando `$fieldErrors` não está vazio;
3. abre `<form method="POST" enctype="multipart/form-data" novalidate data-df-form>` com CSRF;
4. injeta `{!! $formBody !!}` — o HTML dos campos já renderizado pelo `RendererService`;
5. renderiza o bloco de consentimento LGPD quando `$consentRequired`;
6. botão de envio;
7. registra um script inline para sincronizar `<output>` dos campos `range`.

O bloco de consentimento usa `{!! $consentLabel !!}` (`form.blade.php:80`). O texto vem de
`scoring_config.consent_text` e o default do controller contém um link HTML:

```php
$consentLabel = isset($config['consent_text']) && is_string($config['consent_text'])
    ? $config['consent_text']
    : 'Li e concordo com a <a href="/politica-de-privacidade" target="_blank" rel="noopener">Política de Privacidade</a> e autorizo o tratamento das minhas respostas para gerar o resultado.';
```

(`FormularioDinamicoPortalController.php:158-164`)

**Consequência:** o consentimento é o único ponto em que HTML vindo do banco é impresso sem escape
no motor. Quem tiver `write` no CMS pode inserir markup no texto de consentimento; o Blade não
escapa e não há sanitizador.

O script da view é inline (`@push('scripts')` com `<script defer>`), sem nonce. O editor de faixas
também usa `<script defer>` inline (`_faixas-editor.blade.php:150`). Não há JS externo nem
biblioteca de gráficos.

## 3. Componentes de campo

`V2/Services/FormularioDinamico/Views/components/` contém um componente por tipo:

| Componente | Tipo | Estrutura e acessibilidade |
|---|---|---|
| `field-text.blade.php` | `text` | `<label for>`, `maxlength`, `aria-describedby`, `aria-invalid` |
| `field-textarea.blade.php` | `textarea` | `<textarea rows>`, valor escapado por `{{ }}` |
| `field-select.blade.php` | `select` | `<option>` com placeholder "Selecione..." (obrigatório) ou "Nenhuma" |
| `field-multiselect.blade.php` | `multiselect` | `<select multiple size="5">`, `name="answers[x][]"` |
| `field-radio.blade.php` | `radio` | `<fieldset><legend>`, `required` em cada radio |
| `field-checkbox.blade.php` | `checkbox` | `<fieldset><legend>`, `name="answers[x][]"` |
| `field-range.blade.php` | `range` | `<input type=range step=1>` + `<output>` ligado por `for` |
| `field-satisfaction.blade.php` | `satisfaction` | grupo de radios 1..N, rótulos opcionais em `rules.labels` |
| `field-file.blade.php` | `file` | `<input type=file>`, `accept`, dica de tamanho máximo |

Padrões comuns:

- os IDs são derivados de `name`: `df-{name}`, `df-{name}-help`, `df-{name}-error`;
- `aria-describedby` é montado concatenando ajuda e erro;
- erros são impressos em `div.invalid-feedback` com `role="alert"`;
- o atributo `required` é HTML, mas o `<form>` tem `novalidate` — a validação é sempre server-side.

**Problemas observáveis:**

- Quando um campo tem mais de uma mensagem, o mesmo `id="df-{name}-error"` é repetido para cada
  mensagem (`field-text.blade.php`, bloco final). IDs duplicados quebram `aria-describedby`, que
  aponta para o primeiro.
- O `<legend>` de `field-satisfaction` não tem `id`, mas o container declara
  `aria-labelledby="df-{name}-legend"` — referência órfã (`field-satisfaction.blade.php:20`).
- `field-checkbox` e `field-radio` não declaram `aria-required` no grupo (`fieldset`), apenas nos
  inputs; `field-text` e `field-select` declaram no input.

## 4. Portal — `portal/result.blade.php`

- Usa o mesmo layout do formulário, com `aria-live="polite"` no `<article>`.
- Mostra "Formulário enviado", o título/mensagem e um botão "Refazer o formulário".
- A mensagem é impressa com `{!! v2_autolink($result['message']) !!}`; o helper escapa o texto e
  converte apenas URLs em links (`result.blade.php:33`).
- **Nunca** exibe `score`, `max_score` nem `result_level`. O nome da faixa também não aparece; só a
  `message` da faixa, quando preenchida, substitui o agradecimento padrão
  (`FormularioDinamicoPortalController.php:137-140`).

## 5. CMS — telas

### 5.1 `cms/formularios/index.blade.php`

- `v2-cms::layouts.app` com breadcrumb "Infraestrutura → Formulários Dinâmicos".
- Cards de flash (`success`, `info`, `error`) com `data-auto-dismiss="6000"`.
- Busca por título/slug e ordenação por `status|title|slug|submissoes_count|created_at`; o
  controller valida a coluna contra allowlist e a direção (`asc|desc`).
- Tabela com colunas Status, Título, Slug, Submissões e Ações. A renderização das células usa
  closures que montam HTML com `e()` em cada valor (`index.blade.php:14-67`).
- Ações: editar, relatórios, submissões, "ver no portal" (só ativo) e remover com diálogo
  `data-v2-confirm` (componente do CMS).
- O rodapé exibe a paginação de 20 itens.

### 5.2 `cms/formularios/create.blade.php` e `edit.blade.php`

- Formulário de metadados (título, slug, descrição, status).
- Incluem `_campos-editor` e `_faixas-editor`.
- Em `edit`, o botão "Remover formulário" envia por `form="df-delete-form"` com `confirm()` inline
  (`edit.blade.php:89-93`) — handler JS inline, sem CSP.
- O `@stack('crud-scripts')` no fim permite que os partials injetem JS.

### 5.3 `_campos-editor.blade.php`

- Oferece **três** tipos: `radio` ("Alternativa"), `textarea` ("Dissertativa") e `checkbox`
  ("Múltipla escolha") (`_campos-editor.blade.php:5-9`). Tipos antigos não listados são preservados
  como opção extra quando já existem no formulário.
- Cada linha tem `order`, `type`, `name`, `label`, `required`, `help` e, para tipos com opções, um
  editor de linhas `opts[][text]` + `opts[][nota]` (0–100).
- O campo "nota" só fica habilitado quando "Usar pontuação" está ligado; sem isso não há scoring.
- Um `<template id="df-campo-template">` serve de molde para linhas novas via JS.
- O `weight` da pergunta **não** é editável nesta tela.

### 5.4 `_faixas-editor.blade.php`

- Linha do tempo 0–100% com segmentos coloridos; clique adiciona fronteira, arraste move,
  setas reordenam.
- Campos por faixa: nome, cor (`#RRGGBB`), mensagem ao usuário; o `de` é calculado (primeira faixa
  começa em 0, cada seguinte no teto anterior + 1) e a última sempre fecha em 100.
- Mostra "Nota mínima" e "Nota máxima" calculadas pelo controller com a mesma lógica do
  `ScoringService` (`FormularioDinamicoCmsController.php:487-528`).
- Bloco inteiro fica `disabled` quando "Usar pontuação" está desligado.
- O script inline tem ~600 linhas e concentra toda a interação da barra.

### 5.5 `relatorios.blade.php`

- Cards: total de submissões, score médio, dias com submissões (janela de 30).
- **Donut por pergunta** em SVG: os segmentos e a legenda são **pré-computados em PHP** e impressos
  com `{!! !!}`; cada `<circle>` tem `<title>` e a legenda usa `e()` nos textos
  (`relatorios.blade.php:84-111`).
- Cores de faixa e de segmento vêm de uma paleta fixa de 14 cores; a cor da faixa não é usada no
  donut (só na barra de faixas do editor e na tabela de distribuição, quando presente).
- **Barras de pontuação média por pergunta** (quando há mais de uma pergunta), com percentual.
- Tabela de distribuição por faixa com `min_percent`/`max_percent`, cor e contagem.
- Sem biblioteca de gráficos e sem chamada AJAX: tudo renderizado no servidor.

### 5.6 `submissoes.blade.php`

- Filtro por `result_level` (opções derivadas das submissões da página, não de todas as faixas).
- Tabela com data, score, percentual (mesmo `floor` de 4 casas do diagnóstico), faixa (título
  legível via `levelTitles`), origem e link para o detalhe.
- Exportar CSV aparece só para `write`.
- Há um `</div>` a mais no bloco do cabeçalho de filtros (`submissoes.blade.php:31-45`), que fecha
  prematuramente o container; o HTML final fica desbalanceado.

### 5.7 `show.blade.php`

- Resumo: data, score/max, faixa (código de `result_level`, não o título), origem, consentimento e
  hash do IP (com `title` explicando HMAC).
- Respostas listadas por `name` cru (`answers`), sem rótulo da pergunta e sem formatação por tipo
  (`show.blade.php:49-60`). Um `select` e um `checkbox` aparecem como texto/lista separada por
  vírgula.

## 6. Exportação CSV

Fluxo: botão em `submissoes.blade.php` → `GET .../submissoes/export` → controller busca **todas**
as submissões do formulário com `orderBy('created_at')->get()` → `ExportService::toCsv()` →
`response($csv, 200, Content-Type: text/csv; charset=UTF-8)`.

- BOM UTF-8 para Excel.
- Cabeçalho fixo e `answers` como JSON.
- `Content-Disposition` usa `formulario-{slug}-{YmdHis}.csv`.
- Sem streaming: a resposta inteira é montada em memória (`php://temp`), e o controller já carregou
  todas as submissões antes disso.

## 7. UX do módulo legado

O módulo legado **não tem views próprias**. O namespace `v2-formularios::` só é registrado no teste,
via `view()->addNamespace('v2-formularios', __DIR__.'/stubs/views')`
(`tests/Feature/V2/Modules/Infraestrutura/FormulariosDinamicos/FormularioCmsControllerTest.php:52-54`).
Em produção, qualquer tentativa de renderizar `v2-formularios::cms.forms.*` falharia; por isso as
actions foram convertidas em redirecionamentos.

Os stubs de teste (`stubs/views/cms/forms/{index,create,edit}.blade.php`) são o único resquício de
UI do legado e existem apenas para o teste antigo continuar passando.

## 8. i18n e textos

- Todas as strings do motor estão **hardcoded em português**: mensagens de validação por tipo
  (`Este campo é obrigatório.`, `Selecione uma opção.`), rótulos do editor, cabeçalhos do CSV,
  breadcrumbs e botões.
- Não há `__()`/`@lang`, não há pasta `Lang/` no serviço e não há `labels` por locale nos campos.
- `rules.labels` em `satisfaction` aceita rótulos arbitrários por número, mas é um único idioma por
  formulário.

## 9. Acessibilidade — o que existe e o que falta

**Existe:** `<label for>` em todos os tipos simples; `<fieldset>/<legend>` em grupos; `aria-invalid`
e `aria-describedby` em erro; `role="alert"` nas mensagens; `<output>` para range; `sr-only` para
"(obrigatório)"; texto alternativo em ícones via `aria-hidden`; contraste declarado no link de
consentimento (`.df-consent a { color: #a01010; }`).

**Falta/risco:** IDs de erro duplicados; `aria-labelledby` órfão em satisfaction; mensagens de erro
sem associação ao campo quando há múltiplas; foco não é movido para o resumo de erros; o diálogo de
exclusão usa `confirm()` nativo (não acessível de forma consistente); barras do relatório são
`div` com `style` inline e sem `role="img"`/rótulo (só o SVG tem); e o donut depende de paleta fixa
sem verificação de contraste.
