# 10 — Fluxo de Preenchimento no Portal

Ciclo completo: **GET do questionário → escolha/anexo → validação no cliente → POST das respostas → tela de resultado**.

Este arquivo detalha as etapas 1 a 4 (abertura do formulário, HTML gerado, upload antecipado e validação no cliente). As etapas 5 e 6 (gravação da resposta e tela de resultado), o diagrama do fluxo completo e o checklist de equivalência funcional estão em `12-GRAVACAO-E-RESULTADO.md`.

## 1. Etapa 1 — abrir o formulário

```
GET /formularios/{slug}
```

Controller: `v6/app/Http/Controllers/Portal/DynamicForms/Questionnaire/QuestionnaireController.php`

```php
public function edit(string $slug)
{
    $title = 'Formulário';
    $model = $this->getTemplateActived();
    $modelNumber = str_replace('modelo', '', $model);
    $questionnaire = Questionnaire::query()->with('categories', 'asks')->where('status', true)->where('slug', $slug)->first();
    abort_unless($questionnaire, 404, 'Formulário não encontrado');
    return view("portal.layouts.$model.dynamics-forms.questionnaire.edit", compact('questionnaire', 'title', 'model', 'modelNumber'));
}
```

Regras:

- Só questionários com `status = true` são servidos; os demais devolvem **404**.
- O **slug não é validado** antes da consulta (ao contrário de `FilledQuestionnairesController@show`). A busca é `->first()` — se houver slugs duplicados (o índice unique foi removido), o **primeiro** registro é servido.
- O layout (`modelo1`..`modelo6`) vem de `Configuration` key `layout` via `getTemplateActived()` (`v6/app/Http/Controllers/Admin/Controller.php:39`).

Renderização (partial compartilhado `v6/resources/views/portal/layouts/dynamics-forms/questionnaire/render.blade.php`):

```blade
<input type="hidden" name="questionnaire_id" value="{{ data_get($questionnaire, 'id') }}">

@if (data_get($questionnaire, 'categories') && data_get($questionnaire, 'categories')->count() > 0)
    @php
        $categories = $questionnaire->categories
            ->sortBy(function ($category) {
                return [$category->parent_id ?? 0, $category->order];
            })
            ->filter(function ($category) {
                return !$category->parent_id;
            });
    @endphp

    @include('portal.layouts.dynamics-forms.questionnaire.components.categories', [
        'categories' => $categories,
        'questionnaire' => $questionnaire,
    ])
@endif
```

Ordenação: **raízes primeiro** (`parent_id` nulo), por `[parent_id ?? 0, order]`. As categorias filhas são renderizadas pelo `components/asks.blade.php`, que busca os filhos da categoria da **última** pergunta renderizada:

```blade
@php
    $ask = $asks->last();
    $childCategories = $questionnaire->categories->filter(function ($category) use ($ask) {
        return $category->parent_id == data_get($ask, 'ask_category_id');
    });
@endphp
```

> Limitação real: a subcategoria aparece logo após a categoria da última pergunta da categoria pai — a hierarquia é limitada a um nível na prática.

## 2. Etapa 2 — o formulário no HTML

O partial `components/categories.blade.php` agrupa por categoria:

```blade
@foreach ($categories as $category)
    @php
        $asks = $questionnaire->asks->filter(function ($ask) use ($category) {
            return $ask->ask_category_id === $category->id;
        });
    @endphp

    <div class="col-sm-12">
        <fieldset>
            <legend>{{ data_get($category, 'name') }}</legend>
            <div class="row mx-1">
                @include('portal.layouts.dynamics-forms.questionnaire.components.asks', [
                    'asks' => $asks,
                    'questionnaire' => $questionnaire,
                ])
            </div>
        </fieldset>
    </div>
@endforeach
```

O `components/asks.blade.php` monta o `name` de cada input a partir do `identifier` da pergunta:

```blade
@php
    $resource = [
        'identifier' => data_get($questionnaire, 'identifier'),
        'id' => data_get($ask, 'id'),
        'title' => data_get($ask, 'title'),
        'placeholder' => data_get($ask, 'placeholder'),
        'name' => data_get($ask, 'identifier'),
        'description' => data_get($ask, 'description'),
        'value' => data_get($ask, 'value'),
        'required' => data_get($ask, 'required') ? 'required' : '',
    ];
@endphp
```

E despacha para o componente do tipo (exemplo real):

```blade
@includeWhen(
    $ask->ask_type_id == AskTypesEnum::SELECT,
    'portal.layouts.dynamics-forms.questionnaire.components.select',
    array_merge($resource, [
        'config' => data_get($ask, 'config'),
        'multiple' => false,
    ]))
```

O `id` do HTML é `"{identifier_do_questionnaire}-{id_da_pergunta}"` e o `name` é **apenas** o `identifier` da pergunta.

Campos especiais:

| Tipo | Campos HTML gerados |
|---|---|
| CHECKBOX | `input[type=checkbox]` (sem `name`) + `input[type=hidden] name="{ask}" value="false"` |
| RADIO | `input[type=radio] name="{ask}-option"` + `input[type=hidden] name="{ask}"` |
| SATISFACTION_LEVEL | `input[type=hidden] name="{ask}"` + 5 botões com `data-index="{ask}"` e `data-value="0..4"` |
| FILE | `input[type=file]` (sem `name`) + `input[type=hidden] name="{ask}"` (ou `name="{ask}[]"`) |

## 3. Etapa 3 — anexar um arquivo (upload antecipado)

Quando o cidadão escolhe um arquivo, o JS envia **imediatamente** para o servidor (`v6/public/assets/js/custom/portal/dynamic-form/form-utils.js`):

```js
const uploadFile = (element, file, multiple = false, files = null) => {
    Swal.fire({
        title: 'Carregando arquivo',
        html: 'Aguarde enquanto carregamos o(s) arquivo(s)!',
        timerProgressBar: true,
        didOpen: async () => Swal.showLoading(),
    });
    const { name, type } = file;
    const xhr = new XMLHttpRequest();
    const form = new FormData();
    xhr.open('POST', '/formularios/ask-file', true);
    form.append('file', file);
    form.append('identifier', String(element.getAttribute('data-index')).replace('[]', ''));
    form.append('title', name);
    form.append('type', type);
    xhr.setRequestHeader('X-CSRF-TOKEN', document.querySelector('meta[name="csrf-token"]').getAttribute('content'));
    xhr.send(form);
    xhr.onload = () => {
        // trecho elidido
        const value = response?.ask_id;
        if (xhr.status === 200 && value) {
            // trecho elidido
            const hiddenInput = document.querySelector(`[name='${element.getAttribute('data-index')}']`);
            hiddenInput.setAttribute('data-name', name)
            hiddenInput.value = response?.ask_id;
        }
```

Servidor: `v6/app/Http/Controllers/Portal/DynamicForms/AskFile/AskFileController.php`

```php
public function save(SaveRequest $request)
{
    $customName = sprintf(
        "%s-%s-%s.%s",
        time(),
        rand(pow(10, 8), pow(10, 9) - 1),
        uniqid(),
        $request->file->extension()
    );
    $ask = Ask::query()->where('identifier', $request->get('identifier'))->first();
    $questionnaire = $ask->questionnaire;
    $dir = sprintf(
        '/content/formularios-dinamicos/%s/arquivos/%s/',
        data_get($questionnaire, 'identifier'),
        data_get($ask, 'identifier')
    );
    $this->saveUpload($request->file, $dir, null, $customName);
    $fullPath = $dir.$customName;
    $askFile = AskFile::query()->create([
        'ask_id' => data_get($ask, 'id'),
        'title' => $request->get('title'),
        'path' => $fullPath
    ]);
    // trecho elidido
    return response()->json([
        'success' => ['message' => 'Arquivo salvo com sucesso!'],
        'ask_id' => data_get($askFile, 'id'),
        'path' => $fullPath
    ]);
}
```

Características:

- Validação (`v6/app/Http/Requests/Portal/DynamicForms/AskFile/SaveRequest.php`): `identifier` required + `exists:asks,identifier`; `title` required; `type` required; `file` required file **max 20971520** (20 MB).
- O caminho é `/content/formularios-dinamicos/{identifier_do_questionnaire}/arquivos/{identifier_da_pergunta}/{timestamp}-{rand}-{uniqid}.{ext}`.
- O `path` gravado começa com `/` e é relativo a `storage/app/public`; o painel monta a URL como `'/storage' . path`.
- `AskFile` é criado **sem** `filled_questionnaire_id` (fica `null`): o vínculo só acontece no envio (etapa 4).
- O `accept` do input depende de `config.isImg` (ver `09-MOTOR-DE-PERGUNTAS.md`).

## 4. Etapa 4 — validação no cliente e envio

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

```js
$('#submitForm').on('click', () => {
    sendTrigger((data, xhr) => {
        xhr.open('POST', '/formularios/fill-questionnaire/save', true);
        xhr.setRequestHeader('X-CSRF-TOKEN', document.querySelector('meta[name="csrf-token"]').getAttribute('content'));
        xhr.send(data);
        xhr.onload = () => {
            const response = JSON?.parse(xhr?.responseText);
            if (xhr.status === 200) {
                handleMessages(Object.values(response?.success)?.[0], 'success');
                setTimeout(() => { stopLoad(); $('#submitForm').attr('disabled', true); }, 1750);
                return;
            }
            stopLoad();
            handleMessages(Object.values(response?.errors)?.[0], 'error');
        }
    })
});
```

`sendTrigger()` (em `form-utils.js`) executa a validação e monta o `FormData`:

```js
const sendTrigger = (callBackFunction) => {
    startLoad();
    const isValid = validate();
    if (isValid.status === false) {
        setTimeout(() => {
            stopLoad();
            handleMessages('Hás campos inválidos no formulário. Por favor, verifique-os', 'error');
        }, 500);
        return;
    }
    const xhr = new XMLHttpRequest();
    const data = new FormData(document.getElementById('dynamicForm'));

    const arrayData = Array.from(data.values());
    arrayData.splice(0, 2);
    const hasAnyValue = arrayData.find((value) => value != '');
    if (!hasAnyValue) {
        setTimeout(() => {
            stopLoad();
            handleMessages('Preencha ao menos um campo', 'error');
        }, 500);
        return;
    }

    data.forEach((value, key) => {
        const element = document.querySelector(`[name='${key}']`);
        if (element.getAttribute("type") == 'checkbox') {
            data.set(key, value === 'on');
        }
    });

    callBackFunction(data, xhr);
}
```

Regras da validação (`validate()`):

- Campo marcado com a classe `required` e vazio → `is-invalid` + "Campo obrigatório".
- Radio: se o hidden da pergunta estiver vazio → inválido.
- Checkbox: o hidden fica com o valor `'false'` quando desmarcado → inválido se `required`.
- Arquivo: se o input de arquivo é `required` e não tem arquivo selecionado → inválido.
- CPF/CNPJ/Data: validados novamente antes do envio; erro → `{ status: false }` e o envio é abortado.
- "Preencha ao menos um campo": os dois primeiros campos do `FormData` (`questionnaire_id` e o `g-recaptcha-response`) são descartados antes da checagem.

> O `Form::open` da view aponta para `route('contato.enviar')` — a rota real de envio é interceptada pelo JS.

> **Continua em `12-GRAVACAO-E-RESULTADO.md`**: etapa 5 (gravação da resposta), etapa 6 (tela de resultado), diagrama do fluxo completo e checklist de equivalência funcional.

