# 01 — Arquitetura

## 1. Visão geral

O módulo **não** usa a arquitetura de Repositories/Services que o resto da plataforma v6 possui (`v6/app/Repositories/Eloquent/Prefecture/**`). Ele conversa direto com o Eloquent a partir dos controllers, com uma convenção própria: um controller "principal" por agregado e uma pasta `Actions/` com controllers **unitários** (método estático `handle($request)`) que concentram a transação e a lógica de escrita.

```
CMS (AdminLTE)                        Portal (Bootstrap 4)
─────────────                         ───────────────────
prefecture/dynamics-forms/**          portal/layouts/dynamics-forms/**
        │                                     │
        ▼                                     ▼
QuestionnaireController                App\Http\Controllers\Portal\DynamicForms\Questionnaire\QuestionnaireController
FilledQuestionnaireController          App\Http\Controllers\Portal\DynamicForms\FilledQuestionnaire\FilledQuestionnairesController
        │  → Actions\*Controller::handle()   │  → SaveFilledQuestionnaireController::handle()
        ▼                                     ▼
Eloquent (App\Prefecture\DynamicForms\*) ◄────┘
        │
        ▼
PostgreSQL (questionnaires, ask_categories, asks, ask_types, ask_styles,
            filled_questionnaires, ask_files) + storage/app/public (anexos)
```

## 2. Fluxo da requisição

### 2.1 CMS — salvar questionário

```
POST /cms/formularios-dinamicos/questionarios
  → middleware: check.logged, check.route.permission (nome da rota x permissões)
  → Questionnaire\SaveRequest (valida o JSON do questionário inteiro)
  → QuestionnaireController@store
  → SaveQuestionnaireController::handle($request)   [DB::beginTransaction]
      → gera slug único (str_slug + sufixo numérico)
      → Questionnaire::updateOrCreate
      → apaga categorias removidas
      → apaga perguntas removidas (bloqueia se já houver respostas)
      → AskCategory::updateOrCreate por categoria
      → Ask::updateOrCreate por pergunta (config vira JSON no cast)
      → limpeza de categorias órfãs com mais de 1 dia
  → [DB::commit] → JSON {"success":{"questionnaire":"Questionário salvo com sucesso."}} (HTTP 201)
```

### 2.2 Portal — enviar respostas

```
GET /formularios/{slug}
  → App\Http\Controllers\Portal\DynamicForms\Questionnaire\QuestionnaireController@edit
  → Questionnaire::with('categories','asks')->where('status',true)->where('slug',$slug)
  → view portal.layouts.{modelo}.dynamics-forms.questionnaire.edit
      → @include portal.layouts.dynamics-forms.questionnaire.render
          → components.categories → components.asks → componente por tipo

POST /formularios/fill-questionnaire/save   (enviado por XHR pelo JS do portal)
  → Portal\DynamicForms\FilledQuestionnaire\SaveRequest
      → id? / questionnaire_id? / g-recaptcha-response? (se configurado)
  → FilledQuestionnairesController@save
  → SaveFilledQuestionnaireController::handle($request)  [DB::beginTransaction]
      → para cada ask: value = request->get($ask->identifier)
      → FilledQuestionnaire::updateOrCreate (answers = [{ask_id, value}])
      → vincula ask_files.filled_questionnaire_id
  → JSON {"success":{"message":"Questionário preenchido com sucesso!"},"filled_id":N}
```

## 3. Diagrama das classes

```
App\Http\Controllers\Prefecture\DynamicForms\Questionnaire\QuestionnaireController
├── index()        → view prefecture.dynamics-forms.questionnaire.index
├── create()/edit()→ AskType::get(), AskStyle::get() → view prefecture.dynamics-forms.questionnaire.create | prefecture.dynamics-forms.questionnaire.edit
├── store()/update()   → SaveRequest → SaveQuestionnaireController::handle
├── show($id)         → ShowResource (JSON)
├── updateStatus()    → UpdateStatusRequest → UpdateStatusQuestionnaireController::handle
├── updateResult()    → UpdateStatusRequest → UpdateResultQuestionnaireController::handle
├── destroy()         → DeleteRequest → DeleteQuestionnaireController::handle
├── deleteByLote()    → DeleteByLoteRequest → DeleteByLoteQuestionnaireController::handle
├── storeCategory()/updateCategory() → AskCategory\SaveRequest → SaveCategoryController::handle
└── destroyCategory() → AskCategory\DeleteRequest → DeleteCategoryController::handle

App\Http\Controllers\Prefecture\DynamicForms\FilledQuestionnaire\FilledQuestionnaireController
├── index()                    → view prefecture.dynamics-forms.filled-questionnaire.index
├── create()                   → questionnaires com status=1 e asks
├── store()/update()           → SaveRequest → SaveFilledQuestionnaireController::handle
├── show($id)                  → ShowResource (JSON)
├── responses($questionnaireId)→ view prefecture.dynamics-forms.filled-questionnaire.responses
├── showResponses($id)         → ShowResponsesResource (JSON)
├── showIndividualResponses()  → JSON montado à mão (categorias + answers)
├── exportSpreadSheet($id)     → Excel::store(new ResponsesExport($questionnaireId), $filePath)
├── destroy()/deleteByLote()   → Delete*/DeleteByLote*Controller::handle
└── getAsks()                  → Ask\ListResource::collection

App\Http\Controllers\Portal\DynamicForms\Questionnaire\QuestionnaireController
└── edit($slug) → view portal.layouts.{modelo}.dynamics-forms.questionnaire.edit

App\Http\Controllers\Portal\DynamicForms\FilledQuestionnaire\FilledQuestionnairesController
├── show($slug) → view portal.layouts.{modelo}.dynamics-forms.filled-questionnaire.show
├── list($id)   → ShowFilledQuestionnaireController::handle (JSON)
└── save()      → SaveFilledQuestionnaireController::handle

App\Http\Controllers\Portal\DynamicForms\AskFile\AskFileController
└── save()      → AskFile\SaveRequest → FileTrait::saveUpload → AskFile::create
```

## 4. Classes envolvidas (papel)

| Classe | Caminho | Papel |
|---|---|---|
| `Ask` | `v6/app/Http/Model/Prefecture/DynamicForms/Ask.php` | Pergunta. Casts `required=>bool`, `config=>array`; gera `identifier` no `creating`. |
| `AskCategory` | `v6/app/Http/Model/Prefecture/DynamicForms/AskCategory.php` | Seção; `fillable` inclui `parent_id`. |
| `AskType` | `v6/app/Http/Model/Prefecture/DynamicForms/AskType.php` | Catálogo de tipos. `fillable` está **errado** (ver `08-GAPS-E-DIVIDAS.md`). |
| `AskStyle` | `v6/app/Http/Model/Prefecture/DynamicForms/AskStyle.php` | Catálogo de estilos; mesmo `fillable` incorreto. |
| `AskFile` | `v6/app/Http/Model/Prefecture/DynamicForms/AskFile.php` | Anexo enviado; `path` relativo ao disco público. |
| `Questionnaire` | `v6/app/Http/Model/Prefecture/DynamicForms/Questionnaire.php` | Questionário; `SoftDeletes`; accessor/mutator de `published_at`. |
| `FilledQuestionnaire` | `v6/app/Http/Model/Prefecture/DynamicForms/FilledQuestionnaire.php` | Resposta; cast `answers=>array`. |
| `AskTypesEnum` | `v6/app/Enum/Prefecture/DynamicForms/AskTypesEnum.php` | Constantes 1..10 dos tipos. |
| `AskStylesEnum` | `v6/app/Enum/Prefecture/DynamicForms/AskStylesEnum.php` | Constantes 1..6 dos estilos. |
| `ResponsesExport` | `v6/app/Exports/DynamicForms/FilledQuestionnaire/ResponsesExport.php` | Exportação XLSX (maatwebsite/excel). |
| `FileTrait` | `v6/app/Http/Traits/FileTrait.php` | `saveUpload()` grava o anexo no disco e (opcionalmente) comprime imagens. |
| `AppTrait` | `v6/app/Http/Traits/AppTrait.php` | Usado por `SaveQuestionnaireController` (traits globais do CMS). |
| `getTemplateActived()` | `v6/app/Http/Controllers/Admin/Controller.php:39` e `v6/app/Http/Traits/GetInformationsPrefectureTrait.php:92` | Define o layout do portal a partir de `Configuration` key `layout`. |
| `setQuestionnaireFieldStyle()` | `v6/app/Helpers/functions.php:1065` | Converte `ask_style_id` em classe CSS de máscara (`cpf`, `cnpj`, `cep`, `date`, `cash`, `phone`). |
| `menuClicks()` | `v6/app/Helpers/functions.php:865` | Incrementa o contador do item de menu `formulario-dinamico`. |
| `has_access()` | `v6/app/Library/Helper/appHelpers.php:22` | Verifica permissão do operador no Blade. |
| `str_slug()` | `v6/vendor/laravel/framework/src/Illuminate/Support/helpers.php:982` | Helper do Laravel 5.5 (removido no 6+). |
| `flash()` | `v6/composer.json` (`laracasts/flash`) | Mensagens de sucesso/erro do CMS. |

## 5. Diagrama Mermaid — preenchimento no portal

```mermaid
sequenceDiagram
    participant U as Cidadão
    participant B as Blade (edit.blade.php)
    participant J as form-utils.js / script.js
    participant A as Portal AskFileController
    participant F as FilledQuestionnairesController
    participant S as SaveFilledQuestionnaireController
    participant DB as PostgreSQL

    U->>B: GET /formularios/{slug}
    B->>DB: Questionnaire::with(categories,asks)
    B-->>U: HTML com inputs name=ask.identifier
    U->>J: escolhe arquivo
    J->>A: POST /formularios/ask-file (multipart)
    A->>DB: ask_files (filled_questionnaire_id = null)
    A-->>J: {ask_id: N}
    U->>J: clica em Salvar
    J->>J: validate() (obrigatoriedade + CPF/CNPJ/data)
    J->>F: POST /formularios/fill-questionnaire/save
    F->>S: handle($request)
    S->>DB: filled_questionnaires.answers = [{ask_id, value}]
    S->>DB: ask_files.filled_questionnaire_id = filled.id
    S-->>U: {success:{message:"Questionário preenchido com sucesso!"}, filled_id}
```

## 6. Dependências externas (composer)

| Pacote | Versão (v6) | Uso no módulo |
|---|---|---|
| `maatwebsite/excel` | `^3.1` | `Excel::store(new ResponsesExport($questionnaireId), $filePath)` na exportação de respostas. |
| `spatie/laravel-activitylog` | `^2.8` | `activity()->performedOn(new Questionnaire)->causedBy(Sentinel::getUser())` nas exclusões em lote. |
| `laracasts/flash` | `3.0.*` | `flash('Questionário salvo com sucesso.')->success()` / `->error()` no CMS. |
| `cartalyst/sentinel` | (não versionado no recorte) | Usuário do CMS (log de atividade) e verificação de login no portal (resultados). |
| `yajra/laravel-datatables` | (usado pela plataforma) | `DataTables::of($this->struct_views($questionnairesView, 'questionarios'))->make(true)` nas listagens do CMS. |
| `greggilbert/recaptcha` | `^2.2` | `Recaptcha::render()` no formulário do portal e regra `recaptcha` no Request. |
| `intervention/image` | `^2.7` | Compressão de imagens anexadas (via `FileTrait`). |
