# 09 — Motor de Perguntas: tipos, estilos e o `config`

O módulo não tem "código" de formulário: o comportamento de cada pergunta é definido por três dados — **tipo** (`ask_types`), **estilo** (`ask_styles`) e o JSON `config` da linha `asks`. Este arquivo documenta exatamente como cada um é interpretado.

## 1. Catálogo de tipos (`ask_types`)

`v6/app/Enum/Prefecture/DynamicForms/AskTypesEnum.php`:

```php
class AskTypesEnum {
    const TEXT = 1;
    const TEXTAREA = 2;
    const SELECT = 3;
    const MULTI_SELECT = 4;
    const FILE = 5;
    const MULTI_FILE = 6;
    const RADIO = 7;
    const RANGE = 8;
    const CHECKBOX = 9;
    const SATISFACTION_LEVEL = 10;
}
```

| id | `name` (seed) | `description` (seed) | `icon` (seed) | Renderização no portal | `config` esperado |
|---|---|---|---|---|---|
| 1 | `TEXT` | Texto | `fa-font` | `components/text.blade.php` | vazio |
| 2 | `TEXTAREA` | Área de Texto | `fa-align-justify` | `components/textarea.blade.php` | vazio |
| 3 | `SELECT` | Única Escolha | `fa-dot-circle-o` | `components/select.blade.php` (`multiple = false`) | `{options:[{value,text}]}` |
| 4 | `MULTI_SELECT` | Escolhas | `fa-list-ul` | `components/select.blade.php` (`multiple = true`) | `{options:[{value,text}]}` |
| 5 | `FILE` | Único arquivo | `fa-file-o` | `components/file.blade.php` (`multiple = false`) | `{isImg:bool}` |
| 6 | `MULTI_FILE` | arquivos | `fa-files-o` | `components/file.blade.php` (`multiple = true`) | `{isImg:bool}` |
| 7 | `RADIO` | Radio | `fa-dot-circle-o` | `components/radio.blade.php` | `{options:[{value,text}]}` |
| 8 | `RANGE` | Intervalo | `fa-arrows-h` | `components/range.blade.php` | `{min,max}` |
| 9 | `CHECKBOX` | Checkbox | `fa-check-square-o` | `components/checkbox.blade.php` | vazio |
| 10 | `SATISFACTION_LEVEL` | Nível de Satisfação | `fa-line-chart` | `components/satisfaction-level.blade.php` | vazio (rótulos fixos) |

O seed oficial de 1..9 está em `v6/database/seeds/DynamicForms/AskTypeSeeder.php`; o tipo 10 é inserido à parte por `v6/database/seeds/AddSatisfationLevelOptionOnAskType.php`:

```php
AskType::query()->updateOrCreate(['name' => 'SATISFACTION_LEVEL'],[
    'id' => AskTypesEnum::SATISFACTION_LEVEL,
    'description' => 'Nível de Satisfação',
    'icon' => 'fa-line-chart',
]);
```

## 2. Catálogo de estilos (`ask_styles`)

`v6/app/Enum/Prefecture/DynamicForms/AskStylesEnum.php`:

```php
class AskStylesEnum {
    const CPF = 1;
    const CNPJ = 2;
    const CEP = 3;
    const DATE = 4;
    const CASH = 5;
    const PHONE = 6;
}
```

| id | `name` | `description` | `icon` | Classe CSS | Efeito no portal |
|---|---|---|---|---|---|
| 1 | `CPF` | CPF | `fa-id-card-o` | `cpf` | máscara `999.999.999-99` + validação de dígitos |
| 2 | `CNPJ` | CNPJ | `fa-building-o` | `cnpj` | máscara `AA.AAA.AAA/AAAA-00` + validação de dígitos + `toUpperCase` |
| 3 | `CEP` | CEP | `fa-map-marker` | `cep` | máscara `99.999-999` |
| 4 | `DATE` | Data | `fa-calendar-o` | `date` | máscara `99/99/9999` + validação de data (±100 anos) |
| 5 | `CASH` | Dinheiro | `fa-money` | `cash` | `moneyMask()` no `keyup` |
| 6 | `PHONE` | Celular | `fa-phone` | `phone` | máscara `(99) 99999-9999` / `(99) 9999-9999` |

O estilo é convertido em classe CSS pelo helper global `v6/app/Helpers/functions.php:1065`:

```php
if (!function_exists('setQuestionnaireFieldStyle')) {
    function setQuestionnaireFieldStyle($styleId)
    {
        if($styleId == AskStylesEnum::CPF) { return 'cpf'; }
        if($styleId == AskStylesEnum::CNPJ) { return 'cnpj'; }
        if($styleId == AskStylesEnum::CASH) { return 'cash'; }
        if($styleId == AskStylesEnum::CEP) { return 'cep'; }
        if($styleId == AskStylesEnum::DATE) { return 'date'; }
        if($styleId == AskStylesEnum::PHONE) { return 'phone'; }
        return '';
    }
}
```

E aplicado no campo de texto (`v6/resources/views/portal/layouts/dynamics-forms/questionnaire/components/text.blade.php`):

```blade
<input
    type="text"
    class="form-control {{ $required . " " . setQuestionnaireFieldStyle($styleId) }}"
    id="{{ $identifier . '-' . $id }}"
    name="{{ $name }}"
    placeholder="{{$placeholder ?? 'Responda aqui'}}" value="{{ $value }}"
```

> **Limitação real**: o estilo só é aplicado ao tipo **TEXT**. `TEXTAREA`, `SELECT`, `RADIO`, `RANGE` etc. ignoram `ask_style_id`.

### 2.1 Aplicação das máscaras (JS)

`v6/public/assets/js/custom/portal/dynamic-form/form-utils.js`:

```js
$("input.phone").ready(function () {
    var tamanho = $("input.phone")?.val()?.length;
    if (tamanho < 15) {
        $("input.phone").mask("(99) 9999-9999");
    } else if (tamanho >= 15) {
        $("input.phone").mask("(99) 99999-9999");
    }
});
// trecho elidido
$("input.cpf").mask("999.999.999-99");
$("input.cnpj").mask("AA.AAA.AAA/AAAA-00");
$("input.cep").mask("99.999-999");
$("input.date").mask("99/99/9999");
$("input.cash").on("keyup", function (e) {
    const element = e.target;
    element.value = moneyMask(element.value);
});
```

A validação de CPF/CNPJ/data acontece no `change` (com *debounce* de 100 ms) e insere um `.invalid-feedback`:

```js
if (element.classList.contains('cpf') && !cpfValidation(element.value)) {
    element.classList.add('is-invalid');
    element.insertAdjacentHTML('afterend', '<div class="invalid-feedback">CPF inválido</div>');
    return;
}
```

`moneyMask` está em `v6/public/assets/js/custom/utilities/functions.js` (carregado pelo layout do portal, ex.: `v6/resources/views/portal/layouts/modelo1/layout/scripts.blade.php`).

## 3. O `config` JSON

### 3.1 Como o CMS **cria** o `config`

`v6/public/assets/js/custom/contents/formularios-dinamicos/questionarios/structure.js` (método `save` do `AskController`):

```js
const content = {
    id: copy?.id,
    ask_category_id: copy?.categoryId,
    order: order,
    size: copy?.size,
    ask_type_id: copy?.typeId,
    ask_style_id: copy?.styleId,
    title: copy?.title,
    placeholder: copy?.placeholder,
    description: copy?.description,
    required: copy?.required,
    value: copy?.value,
    config: {}
}
const type = parseInt(copy?.typeId);
if ([
    3, // select
    4, // multiple select
    7  // radio
].includes(parseInt(type))) {
    const contentOptions = ask?.orderOptions()?.map((option) => {
        return { value: option?.value, text: option?.text };
    });
    content['config'] = { options: contentOptions };
}

if ([
    5, // file
    6  // multiple file
].includes(parseInt(type))) {
    content['config'] = { isImg: copy?.isImg };
}

if (
    type == 8 // range
) {
    content['config'] = { min: copy?.min, max: copy?.max };
}
```

Resumo: **o `config` só é preenchido para os tipos 3, 4, 7 (`options`), 5 e 6 (`isImg`) e 8 (`min`/`max`)**. Para os demais, `config` é `{}` (gravado como `[]`/null pelo cast do model).

### 3.2 Como o CMS **edita** o `config`

No mesmo arquivo, o `AskController.fill()` reidrata os campos a partir do `config` salvo:

```js
const content = {
    id: object?.id,
    // trecho elidido
    min: object?.config?.min,
    max: object?.config?.max,
    isImg: object?.config?.isImg,
    config: {}
}

this.fillFields();

if (object?.config?.options) {
    object?.config?.options.map((option, i) => {
        option.order = i;
        this.options[i] = option;
    });
}
```

E o método `specialConfig()` decide quais campos extras mostrar conforme o tipo escolhido:

```js
if (typeField.getValue() == 1 // text
) {
    this.showStyleField();
}

if ([
    3, // select
    4, // multiple select
    7  // radio
].includes(parseInt(typeField.getValue()))) {
    this.showSelectField();
}

if ([
    5, // file
    6  // multiple file
].includes(parseInt(typeField.getValue()))) {
    this.showFileField();
}

if (
    typeField.getValue() == 8 // range
) {
    min.required = true;
    max.required = true;
    this.showRangeField();
}

if (![4, 5, 6, 9].includes(parseInt(typeField.getValue()))) {
    this.showValueField();
}
```

> Note que o `styleId` (máscara) só aparece para o tipo **TEXT**; e o campo "valor padrão" (`value`) não aparece para 4, 5, 6 e 9.

### 3.3 Como o **servidor** salva o `config`

`SaveQuestionnaireController` repassa o `config` cru para o model:

```php
$createdAsk = Ask::query()->updateOrCreate(["id" => data_get($ask, "id")], [
    'questionnaire_id' => data_get($questionnaire, 'id'),
    'ask_category_id' => data_get($createdCategory, 'id'),
    'order' => data_get($ask, 'order'),
    'size' => data_get($ask, 'size', 12),
    'ask_type_id' => data_get($ask, 'ask_type_id'),
    'title' => data_get($ask, 'title'),
    'placeholder' => data_get($ask, 'placeholder'),
    'description' => data_get($ask, 'description'),
    'required' => data_get($ask, 'required'),
    'ask_style_id' => data_get($ask, 'ask_style_id'),
    'value' => data_get($ask, 'value'),
    'config' => data_get($ask, 'config'),
]);
```

O cast `'config' => 'array'` do model serializa para `json` no banco. **Não há validação de chaves** — qualquer estrutura é aceita.

## 4. Como o **portal** interpreta o `config`

| Tipo | Trecho real (Blade) | Efeito |
|---|---|---|
| SELECT / MULTI_SELECT | `v6/resources/views/portal/layouts/dynamics-forms/questionnaire/components/select.blade.php`: `@php $options = data_get($config, 'options'); @endphp` + `@foreach ($options as $option) <option value="{{ data_get($option, 'value') }}">{{ data_get($option, 'text') }}</option>` | Monta o `select`; com `multiple` usa `name="x[]"` e `size="5"`. |
| RADIO | `v6/resources/views/portal/layouts/dynamics-forms/questionnaire/components/radio.blade.php`: `@php $options = data_get($config, 'options'); @endphp` | Um `input[type=radio]` por opção; o valor vai para um hidden `name="{ask}"`. |
| RANGE | `v6/resources/views/portal/layouts/dynamics-forms/questionnaire/components/range.blade.php`: `$min = data_get($config, 'min', 0); $max = data_get($config, 'max', 100);` | `min`/`max` do `input[type=range]`; **defaults 0 e 100** quando ausentes. |
| FILE / MULTI_FILE | `v6/resources/views/portal/layouts/dynamics-forms/questionnaire/components/file.blade.php`: `@if(data_get($config, 'isImg', false)) accept="image/*" @else accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar,.7z" @endif` | Restringe o `accept` do input de arquivo. |
| TEXT | usa apenas `ask_style_id` (não o `config`) | — |
| CHECKBOX | não usa `config`; o hidden tem `value="false"` | — |
| SATISFACTION_LEVEL | não usa `config`; rótulos fixos 0..4 | — |

## 5. Como a **exportação** interpreta o `config`

`v6/app/Exports/DynamicForms/FilledQuestionnaire/ResponsesExport.php`:

```php
if ($type == AskTypesEnum::SATISFACTION_LEVEL) {
    $levels = ['Muito Insatisfeito', 'Insatisfeito', 'Neutro', 'Satisfeito', 'Muito Satisfeito'];
    return $levels[$value] ?? $value;
}
if ($type == AskTypesEnum::SELECT) {
    $option = collect($config['options'])->where('value', $value)->first();
    return $option['text'] ?? $value;
}
if ($type == AskTypesEnum::MULTI_SELECT) {
    return implode(', ', collect($config['options'])
        ->whereIn('value', $value)
        ->pluck('text')
        ->values()
        ->toArray());
}
```

E o cabeçalho da planilha exclui dois tipos:

```php
$this->headers = $this->questionnaire->asks->filter(function ($item) {
    $type = $item->ask_type_id;
    return !in_array($type, [AskTypesEnum::MULTI_FILE, AskTypesEnum::RADIO]);
})->sortBy('id')->pluck('title')->values()->toArray();
```

> Consequência: as respostas de **RADIO** e **MULTI_FILE** **não aparecem** na planilha (as colunas são removidas).

## 6. Como o **resultado público** interpreta o `config`

`v6/public/assets/js/custom/portal/dynamic-form/resultado.js`:

```js
if([
    3, // select
    4, // multiple select
    7  // radio
].includes(parseInt(type))) {
    if(!config?.options || !Array.isArray(config?.options) || config?.options.length == 0){
        return;
    }
    const response = startPieChartStruct(
        view?.askTitle, view?.responsesCount, view?.responses, config?.options, view?.identifier
    );
    startCategory.append(response);
}
```

E o nível de satisfação usa um conjunto **fixo** de opções, ignorando o `config`:

```js
if ([
    10, // satisfaction_level
].includes(parseInt(type))) {
    const response = startPieChartStruct(
        view?.askTitle, view?.responsesCount, view?.responses,
        [
            {value: '0', text: 'Muito Insatisfeito'},
            {value: '1', text: 'Insatisfeito'},
            {value: '2', text: 'Neutro'},
            {value: '3', text: 'Satisfeito'},
            {value: '4', text: 'Muito Satisfeito'}
        ],
        view?.identifier,
        true
    );
    startCategory.append(response);
}
```

## 7. Quadro-resumo do `config` por tipo

| `ask_type_id` | Chaves do `config` | Quem lê |
|---|---|---|
| 1 TEXT | — | — (usa `ask_style_id`) |
| 2 TEXTAREA | — | — |
| 3 SELECT | `options[].value`, `options[].text` | portal (select), resultado (pizza), export |
| 4 MULTI_SELECT | `options[].value`, `options[].text` | portal (select multiple), resultado (pizza), export |
| 5 FILE | `isImg` | portal (accept) |
| 6 MULTI_FILE | `isImg` | portal (accept) — **excluído da planilha** |
| 7 RADIO | `options[].value`, `options[].text` | portal (radio), resultado (pizza) — **excluído da planilha** |
| 8 RANGE | `min`, `max` | portal (input range) |
| 9 CHECKBOX | — | — |
| 10 SATISFACTION_LEVEL | — | rótulos fixos no portal e na exportação |

## 8. Regras implícitas e armadilhas do motor

1. `config.options` **não** é validado no servidor (ver `08-GAPS-E-DIVIDAS.md`, B1): um SELECT sem opções é salvo e renderiza um `select` vazio.
2. `min`/`max` chegam do JS como **string** (campo `Input`); o Blade os interpola direto no atributo, sem `intval`.
3. `config` é **reescrito** pelo CMS sempre que a pergunta é salva: o `config` do banco é substituído pelo que o JS montou. Campos não previstos no `structure.js` são perdidos a cada edição.
4. O tipo **TEXT** é o único que aceita `ask_style_id`; os demais aceitam o valor no banco, mas o estilo é ignorado na renderização.
5. O CMS **não** possui função de duplicar pergunta (não há ação "duplicar" em `structure.js`/`listeners.js`); cada pergunta é editada individualmente e o `config` é reconstruído do zero pelo `AskController.save()` a partir dos campos do modal.
6. Não há "condicional" (ex.: "se responder X, mostre Y") nem validação regex customizada por pergunta — o motor é estritamente por tipo.
