# 03 — Rotas, Controllers, Requests e Resources

## 1. Rotas CMS (`/cms/formularios-dinamicos`)

Definidas em `v6/routes/basepro.php:471-509`, dentro do grupo `prefix => formularios-dinamicos`, `as => dynamic_forms.`, que por sua vez está dentro do grupo `cms` (prefixo `/cms`) com `before => csrf`.

### 1.1 Questionários — `dynamic_forms.questionnaire.*`

| Método | URI (relativa a `/cms/formularios-dinamicos`) | Nome da rota | Controller@ação |
|---|---|---|---|
| POST | `/questionarios/atualiza-status` | `dynamic_forms.questionnaire.update-status` | `QuestionnaireController@updateStatus` |
| POST | `/questionarios/atualiza-resultado` | `dynamic_forms.questionnaire.update-result` | `QuestionnaireController@updateResult` |
| POST | `/questionarios/categoria` | `dynamic_forms.questionnaire.store-category` | `QuestionnaireController@storeCategory` |
| PUT | `/questionarios/categoria` | `dynamic_forms.questionnaire.update-category` | `QuestionnaireController@updateCategory` |
| DELETE | `/questionarios/categoria` | `dynamic_forms.questionnaire.destroy-category` | `QuestionnaireController@destroyCategory` |
| POST | `/questionarios` | `dynamic_forms.questionnaire.store` | `QuestionnaireController@store` |
| GET | `/questionarios` | `dynamic_forms.questionnaire.index` | `QuestionnaireController@index` |
| GET | `/questionarios/criar` | `dynamic_forms.questionnaire.create` | `QuestionnaireController@create` |
| GET | `/questionarios/{questionnaire}` | `dynamic_forms.questionnaire.show` | `QuestionnaireController@show` |
| PUT | `/questionarios` | `dynamic_forms.questionnaire.update` | `QuestionnaireController@update` |
| GET | `/questionarios/{questionnaire}/edit` | `dynamic_forms.questionnaire.edit` | `QuestionnaireController@edit` |
| DELETE | `/questionarios` | `dynamic_forms.questionnaire.destroy` | `QuestionnaireController@destroy` |

### 1.2 Questionários preenchidos — `dynamic_forms.filled-questionnaire.*`

| Método | URI | Nome da rota | Controller@ação |
|---|---|---|---|
| GET | `/questionarios-preenchidos/asks` | `dynamic_forms.filled-questionnaire.get-asks` | `FilledQuestionnaireController@getAsks` |
| POST | `/questionarios-preenchidos` | `dynamic_forms.filled-questionnaire.store` | `FilledQuestionnaireController@store` |
| GET | `/questionarios-preenchidos` | `dynamic_forms.filled-questionnaire.index` | `FilledQuestionnaireController@index` |
| GET | `/questionarios-preenchidos/criar` | `dynamic_forms.filled-questionnaire.create` | `FilledQuestionnaireController@create` |
| GET | `/questionarios-preenchidos/{filled_questionnaire}` | `dynamic_forms.filled-questionnaire.show` | `FilledQuestionnaireController@show` |
| PUT | `/questionarios-preenchidos` | `dynamic_forms.filled-questionnaire.update` | `FilledQuestionnaireController@update` |
| GET | `/questionarios-preenchidos/{questionnaire}/respostas` | `dynamic_forms.filled-questionnaire.responses` | `FilledQuestionnaireController@responses` |
| GET | `/questionarios-preenchidos/{questionnaire}/respostas/show` | `dynamic_forms.filled-questionnaire.showResponses` | `FilledQuestionnaireController@showResponses` |
| GET | `/questionarios-preenchidos/{questionnaire}/respostas/individual/{index}` | `dynamic_forms.filled-questionnaire.showIndividualResponses` | `FilledQuestionnaireController@showIndividualResponses` |
| GET | `/questionarios-preenchidos/{questionnaire}/respostas/exportar-planilha` | `dynamic_forms.filled-questionnaire.exportSpreadSheet` | `FilledQuestionnaireController@exportSpreadSheet` |
| DELETE | `/questionarios-preenchidos` | `dynamic_forms.filled-questionnaire.destroy` | `FilledQuestionnaireController@destroy` |

### 1.3 Rotas auxiliares (fora do grupo do módulo)

| Método | URI | Nome | Controller@ação | Fonte |
|---|---|---|---|---|
| POST | `/cms/formularios-dinamicos/questionarios/delete-por-lote` | `ajax.questionnaire.deleteByLote` | `QuestionnaireController@deleteByLote` | `v6/routes/basepro.php:158` |
| POST | `/cms/formularios-dinamicos/questionarios-preenchidos/delete-por-lote` | `ajax.filled-questionnaire.deleteByLote` | `FilledQuestionnaireController@deleteByLote` | `v6/routes/basepro.php:159` |
| GET | `/cms/listagem_questionarios` | (sem nome) | `Prefecture\DatatableController@get_questionnaire_list` | `v6/routes/basepro.php:457` |
| GET | `/cms/listagem_questionarios_preenchidos` | (sem nome) | `Prefecture\DatatableController@get_filled_questionnaire_list` | `v6/routes/basepro.php:458` |

## 2. Rotas Portal (públicas)

Definidas em `v6/routes/basepro.php:533-540`, **fora** do grupo `cms` (sem prefixo `/cms`), grupo `prefix => formularios`, `as => portal.formularios.`:

| Método | URI | Nome da rota | Controller@ação |
|---|---|---|---|
| GET | `/formularios/{slug}` | `portal.formularios.edit` | `Portal\DynamicForms\Questionnaire\QuestionnaireController@edit` |
| POST | `/formularios/ask-file` | `portal.formularios.ask-file.save` | `Portal\DynamicForms\AskFile\AskFileController@save` |
| POST | `/formularios/fill-questionnaire/save` | `portal.formularios.fill-questionnaire.save` | `Portal\DynamicForms\FilledQuestionnaire\FilledQuestionnairesController@save` |
| GET | `/formularios/resultado/{slug}` | `portal.formularios.result.show` | `Portal\DynamicForms\FilledQuestionnaire\FilledQuestionnairesController@show` |
| GET | `/formularios/resultado/{id}/list` | `portal.formularios.result.list` | `Portal\DynamicForms\FilledQuestionnaire\FilledQuestionnairesController@list` |

**Não existe rota equivalente em `v6/routes/web.php`** — a busca por `formulario|questionnaire|DynamicForms` em `v6/routes/web.php` não retorna nenhuma ocorrência; as rotas públicas vivem todas em `basepro.php` (que é registrado em `v6/app/Providers/RouteServiceProvider.php:88`).

Essas rotas **não têm middleware de autenticação**: o preenchimento é anônimo. O único controle é o `reCAPTCHA`, e apenas se `Configuration` tiver as chaves `recaptcha_public_key` e `recaptcha_private_key`.

## 3. Permissões

- A autorização real acontece no middleware `check.route.permission` (`v6/app/Http/Middleware/CheckRoutePermission.php`), que exige login Sentinel e casa o **nome da rota** com as permissões do papel; super-admin passa direto.
- As views do CMS usam o helper `has_access()` (`v6/app/Library/Helper/appHelpers.php:22`) com nomes como `questionarios.create` e `questionarios.delete` (ex.: `v6/resources/views/prefecture/dynamics-forms/questionnaire/index.blade.php`).
- Os recursos registrados para o módulo 52 na tabela `resources` são `questionario` (id 71) e `questionario-preenchido` (id 72).
- No portal, `GET /formularios/resultado/{slug}` usa `Sentinel::check()` apenas para decidir se mostra blocos de resultado que exigem login (`$isAuth`).

## 4. Form Requests e regras de validação

| Request | Caminho | Regras |
|---|---|---|
| `Questionnaire\SaveRequest` | `v6/app/Http/Requests/Prefecture/DynamicForms/Questionnaire/SaveRequest.php` | `id` nullable/exists; `status` required boolean; `has_result` nullable boolean; `title` required string max 100; `description`, `submit_info` nullable string; `published_at` required `date_format:d/m/Y H:i`; `slug` nullable max 100 unique (ignorando o próprio id, `whereNull('deleted_at')`); `categories` required array; `categories.*.id` nullable exists; `categories.*.parent_id` nullable exists; `categories.*.order` required numeric; `categories.*.name` required string max 100 **distinct**; `categories.*.asks` required array; `categories.*.asks.*.id` nullable exists; `categories.*.asks.*.order` required numeric; `categories.*.asks.*.size` required numeric; `categories.*.asks.*.ask_type_id` required exists; `categories.*.asks.*.title` required max 100; `categories.*.asks.*.placeholder` nullable max 100; `categories.*.asks.*.description` nullable; `categories.*.asks.*.required` required boolean; `categories.*.asks.*.ask_style_id` nullable exists; `categories.*.asks.*.value` nullable string; `categories.*.asks.*.config` nullable array. |
| `Questionnaire\UpdateStatusRequest` | `v6/app/Http/Requests/Prefecture/DynamicForms/Questionnaire/UpdateStatusRequest.php` | `contents_id` required exists `questionnaires,id` |
| `Questionnaire\DeleteRequest` | `v6/app/Http/Requests/Prefecture/DynamicForms/Questionnaire/DeleteRequest.php` | `id` required exists `questionnaires,id` |
| `Questionnaire\DeleteByLoteRequest` | `v6/app/Http/Requests/Prefecture/DynamicForms/Questionnaire/DeleteByLoteRequest.php` | `toDelete` required array; `toDelete.*` exists `questionnaires,id` |
| `AskCategory\SaveRequest` | `v6/app/Http/Requests/Prefecture/DynamicForms/AskCategory/SaveRequest.php` | `id` nullable exists; `name` required string max 100; `order` nullable numeric; `parentId` nullable exists |
| `AskCategory\DeleteRequest` | `v6/app/Http/Requests/Prefecture/DynamicForms/AskCategory/DeleteRequest.php` | `id` required exists `ask_categories,id` |
| `FilledQuestionnaire\SaveRequest` (CMS) | `v6/app/Http/Requests/Prefecture/DynamicForms/FilledQuestionnaire/SaveRequest.php` | `id` nullable exists; `questionnaire_id` required exists |
| `FilledQuestionnaire\GetAsksRequest` | `v6/app/Http/Requests/Prefecture/DynamicForms/FilledQuestionnaire/GetAsksRequest.php` | `questionnaire_id` required exists |
| `FilledQuestionnaire\DeleteRequest` | `v6/app/Http/Requests/Prefecture/DynamicForms/FilledQuestionnaire/DeleteRequest.php` | `id` required exists `filled_questionnaires,id` |
| `FilledQuestionnaire\DeleteByLoteRequest` | `v6/app/Http/Requests/Prefecture/DynamicForms/FilledQuestionnaire/DeleteByLoteRequest.php` | `toDelete` required array; `toDelete.*` exists `filled_questionnaires,id` |
| `Portal\FilledQuestionnaire\SaveRequest` | `v6/app/Http/Requests/Portal/DynamicForms/FilledQuestionnaire/SaveRequest.php` | `id` nullable exists; `questionnaire_id` required exists; `g-recaptcha-response => recaptcha` **somente se** houver as chaves de reCAPTCHA em `Configuration` |
| `Portal\AskFile\SaveRequest` | `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) |

### 4.1 Bug real nas regras de `config`

Em `Questionnaire\SaveRequest` as regras condicionais referenciam um campo que **não existe** no payload (`style` em vez de `ask_type_id`):

```php
'categories.*.asks.*.config.min' => ["required_if:categories.*.asks.*.style,$range", 'numeric'],
'categories.*.asks.*.config.options' => ["required_if:categories.*.asks.*.style,$select", 'array'],
```

Como o `required_if` nunca casa, `config.min`, `config.max` e `config.options` **nunca são obrigatórios** no servidor. A obrigatoriedade real vem só do JS do CMS (`structure.js`, `specialConfig()`), que marca `min`/`max` como required quando o tipo é 8 (range) e exige a lista de opções para 3/4/7. Detalhes em `08-GAPS-E-DIVIDAS.md`.

### 4.2 Divisão de responsabilidades (padrão "Actions")

Todos os `FormRequest` têm `authorize()` retornando `true` — a autorização fica no middleware. Os controllers "Actions" são unitários com método estático `handle()`:

| Action | O que faz |
|---|---|
| `SaveQuestionnaireController` | Salva questionário + categorias + perguntas numa única transação; gera slug único; apaga removidos; bloqueia remoção de pergunta com resposta. |
| `SaveCategoryController` | `AskCategory::updateOrCreate` (usado pelo CRUD rápido de categoria). |
| `DeleteCategoryController` | Recusa exclusão se a categoria tiver perguntas (422). |
| `DeleteQuestionnaireController` | Soft delete. |
| `DeleteByLoteQuestionnaireController` | Exclusão em lote + log de atividade. |
| `UpdateStatusQuestionnaireController` | Alterna `status`. |
| `UpdateResultQuestionnaireController` | Alterna `has_result`. |
| `SaveFilledQuestionnaireController` | Grava `filled_questionnaires.answers` e vincula os arquivos. |
| `ShowFilledQuestionnaireController` | Devolve JSON com categorias, perguntas e respostas. |
| `DeleteFilledQuestionnaireController` / `DeleteByLoteFilledQuestionnaireController` | Exclusão individual / em lote (com log de atividade). |

## 5. API Resources — formato JSON

| Resource | Caminho | Campos |
|---|---|---|
| `Ask\ListResource` | `v6/app/Http/Resources/Prefecture/DynamicForms/Ask/ListResource.php` | `id`, `ask_category_id`, `identifier`, `order`, `size`, `ask_type_id`, `title`, `placeholder`, `description`, `required`, `ask_style_id`, `value`, `config` |
| `Ask\ListWithFilesResource` | `v6/app/Http/Resources/Prefecture/DynamicForms/Ask/ListWithFilesResource.php` | os mesmos + `files` (coleção de `AskFile\ListResource`: `id`, `title`, `path`) |
| `AskCategory\ListResource` | `v6/app/Http/Resources/Prefecture/DynamicForms/AskCategory/ListResource.php` | `id`, `order`, `name`, `parent_id`, `asks` |
| `AskCategory\ListWithoutAsksResource` | `v6/app/Http/Resources/Prefecture/DynamicForms/AskCategory/ListWithoutAsksResource.php` | `id`, `order`, `name`, `parent_id` |
| `AskFile\ListResource` | `v6/app/Http/Resources/Prefecture/DynamicForms/AskFile/ListResource.php` | `id`, `title`, `path` |
| `Questionnaire\ListResource` | `v6/app/Http/Resources/Prefecture/DynamicForms/Questionnaire/ListResource.php` | `id`, `title`, `asks_count`, `published_at`, `created_at` (formatado `d/m/Y`) |
| `Questionnaire\ShowResource` | `v6/app/Http/Resources/Prefecture/DynamicForms/Questionnaire/ShowResource.php` | `id`, `status`, `has_result`, `slug`, `title`, `description`, `submit_info`, `published_at`, `categories`, `asks` |
| `Questionnaire\ShowWithFilesResource` | `v6/app/Http/Resources/Prefecture/DynamicForms/Questionnaire/ShowWithFilesResource.php` | `id`, `status`, `title`, `description`, `published_at`, `categories`, `asks` (com `files`) |
| `FilledQuestionnaire\ShowResource` | `v6/app/Http/Resources/Prefecture/DynamicForms/FilledQuestionnaire/ShowResource.php` | `id`, `answers`, `questionnaire` (ShowWithFiles) |
| `FilledQuestionnaire\ShowResponsesResource` | `v6/app/Http/Resources/Prefecture/DynamicForms/FilledQuestionnaire/ShowResponsesResource.php` | `id`, `categories`, `asks` (com arquivos), `responses` |
| `FilledQuestionnaire\ShowWithoutQuestionnaireResource` | `v6/app/Http/Resources/Prefecture/DynamicForms/FilledQuestionnaire/ShowWithoutQuestionnaireResource.php` | `id`, `answers` |

Envelope padrão do Laravel 5.5: a chave `data` embrulha o Resource. Exemplo real de `Questionnaire\ShowResource` (rota `GET /cms/formularios-dinamicos/questionarios/{id}`):

```json
{"data":{"id":1,"status":true,"has_result":false,"slug":"avaliacao","title":"Avaliação de serviços","description":"<p>Conte sua experiência</p>","submit_info":"Obrigado pela participação!","published_at":"01/03/2025 08:00","categories":[{"id":1,"order":1,"name":"Dados","parent_id":null,"asks":[{"id":1,"ask_category_id":1,"identifier":"1740780000-123456789-65f1c2d3e4f5","order":1,"size":12,"ask_type_id":1,"title":"Nome completo","placeholder":"Digite seu nome","description":null,"required":true,"ask_style_id":null,"value":null,"config":{}}]}],"asks":[]}}
```

É esse envelope que o `edit.js` do CMS consome, em `v6/public/assets/js/custom/contents/formularios-dinamicos/questionarios/edit.js`.

## 6. Respostas JSON montadas à mão

`FilledQuestionnaireController@showIndividualResponses` **não** usa Resource: monta o array na mão, agrupando as respostas por categoria e resolvendo arquivos:

```php
$answer = collect($filledQuestionnaires->answers)->map(function ($item) use ($asks) {
    $ask = $asks->where('id', $item['ask_id'])->first();
    $type = $ask->ask_type_id;
    $config = $ask->config;
    if ($type == AskTypesEnum::FILE) {
        $file = $ask->askFiles->where('id', $item['value'])->first();
        $item['ask_file_path'] = '/storage' . data_get($file, 'path');
        $item['ask_file_title'] = data_get($file, 'title');
    }
    if ($type == AskTypesEnum::MULTI_FILE) {
        $files = [];
        foreach ($item['value'] as $fileId) {
            $file = $ask->askFiles->where('id', $fileId)->first();
            if ($file) {
                $files[] = [
                    'ask_file_path' => '/storage' . data_get($file, 'path'),
                    'ask_file_title' => data_get($file, 'title')
                ];
            }
        }
        $item['ask_files'] = $files;
    }

    if ($ask) {
        $item['ask_title'] = $ask->title;
        $item['ask_order'] = $ask->order;
        $item['ask_category_id'] = $ask->ask_category_id;
        $item['ask_style'] = $ask->ask_style_id;
        $item['ask_type'] = $type;
        $item['ask_config'] = $config;
        return $item;
    }
})->filter(function ($item) {
    return !is_null($item);
})->sortBy('ask_order');
```

Formato devolvido (exemplo real, com os campos montados pelo controller):

```json
{"data":{
  "categories":[{"id":1,"name":"Dados","order":1,"parent_id":null,"answers":[{"ask_id":1,"value":"Maria Silva","ask_title":"Nome completo","ask_order":1,"ask_category_id":1,"ask_style":null,"ask_type":1,"ask_config":{}}]}],
  "answers":[{"ask_id":1,"value":"Maria Silva","ask_title":"Nome completo","ask_order":1,"ask_category_id":1,"ask_style":null,"ask_type":1,"ask_config":{}}],
  "allResponses":12,
  "currentResponse":1
}}
```
