# 02 — Modelagem de Dados

> Fonte: schema real do PostgreSQL (`\d` em `prefeituras_v5`) conferido contra as migrations de `v6/database/migrations/`.

## 1. Diagrama relacional

```
questionnaires (soft delete)
   │ 1
   ├──────────────► ask_categories ──(parent_id, auto-FK)──► ask_categories
   │                     │ 1
   │                     └──────────────► asks ──(ask_type_id)──► ask_types
   │                                           │        (ask_style_id)──► ask_styles
   │                                           │ 1
   │                                           └──────────────► ask_files
   │ 1
   └──────────────► filled_questionnaires ──(answers JSON)──┘
                              │ 1
                              └──────────────► ask_files (filled_questionnaire_id)
```

## 2. `questionnaires`

| Coluna | Tipo | Null | Default | Observação |
|---|---|---|---|---|
| `id` | `integer` (serial) | não | `nextval('questionnaires_id_seq')` | PK |
| `identifier` | `varchar(36)` | não | — | **UNIQUE**; gerado no `creating` do model |
| `status` | `boolean` | não | `true` | Habilita o formulário no portal |
| `title` | `varchar(100)` | não | — | Título exibido no portal |
| `description` | `text` | sim | — | HTML do Summernote; `<p><br></p>` é convertido em `null` no save |
| `published_at` | `timestamp(0)` | sim | — | Mutador/acessor próprio: grava `Y-m-d h:i:s`, exibe `d/m/Y H:i` |
| `deleted_at` | `timestamp(0)` | sim | — | Soft delete |
| `created_at` / `updated_at` | `timestamp(0)` | sim | — | Laravel timestamps |
| `slug` | `varchar(100)` | sim | — | Link permanente; **unique foi removido** (ver §6) |
| `has_result` | `boolean` | não | `false` | Exibe o botão/tela de resultado |
| `submit_info` | `text` | sim | — | Aviso exibido abaixo do formulário no portal |

Geração do `identifier` (`v6/app/Http/Model/Prefecture/DynamicForms/Questionnaire.php`):

```php
static::creating(function ($model) {
    if (empty($model->identifier)) {
        $model->identifier = sprintf("%s-%s-%s", time(), rand(pow(10, 8), pow(10, 9)-1), uniqid());
    }
});
```

## 3. `ask_categories`

| Coluna | Tipo | Null | Default | Observação |
|---|---|---|---|---|
| `id` | `integer` | não | seq | PK |
| `order` | `integer` | não | `1` | Ordenação no CMS e no portal |
| `name` | `varchar(100)` | não | — | Nome da seção (`distinct` na validação) |
| `questionnaire_id` | `bigint` | sim | — | FK → `questionnaires.id` ON DELETE CASCADE; virou **nullable** em 2024-04-23 (categorias órfãs) |
| `parent_id` | `bigint` | sim | — | Auto-FK → `ask_categories.id` ON DELETE CASCADE |

## 4. `asks`

| Coluna | Tipo | Null | Default | Observação |
|---|---|---|---|---|
| `id` | `integer` | não | seq | PK |
| `identifier` | `varchar(36)` | não | — | **UNIQUE**; usado como `name` do input HTML |
| `questionnaire_id` | `bigint` | não | — | FK → `questionnaires.id` CASCADE |
| `order` | `integer` | não | `1` | Ordem dentro da categoria |
| `size` | `integer` | não | `12` | Largem Bootstrap (`col-md-{size}`) |
| `ask_type_id` | `bigint` | não | — | FK → `ask_types.id` CASCADE |
| `title` | `varchar(100)` | não | — | Rótulo da pergunta |
| `description` | `text` | sim | — | Texto de apoio |
| `required` | `boolean` | não | `true` | Obrigatoriedade (aplicada **só no cliente**) |
| `ask_style_id` | `bigint` | sim | — | FK → `ask_styles.id` ON DELETE CASCADE (nullable) |
| `value` | `varchar(255)` | sim | — | Valor padrão / valor atual |
| `config` | `json` | sim | — | Parâmetros por tipo (cast `array`) |
| `ask_category_id` | `bigint` | sim | — | FK → `ask_categories.id` CASCADE |
| `placeholder` | `varchar(100)` | sim | — | Texto do campo |

`identifier` é gerado no `creating` (`v6/app/Http/Model/Prefecture/DynamicForms/Ask.php`):

```php
static::creating(function ($model) {
    if (empty($model->identifier)) {
        $model->identifier = sprintf("%s%s%s", time(), rand(pow(10, 8), pow(10, 9)-1), uniqid());
    }
});
```

Casts do model:

```php
protected $casts = [
    'required' => 'boolean',
    'config' => 'array',
];
```

## 5. `ask_types` e `ask_styles`

| Tabela | Colunas | Observação |
|---|---|---|
| `ask_types` | `id` (PK), `name` `varchar(100)`, `description` `varchar(100)`, `icon` `varchar(30)`, timestamps | Catálogo com **10** tipos (1..10) |
| `ask_styles` | `id` (PK), `name` `varchar(100)`, `description` `varchar(100)`, `icon` `varchar(30)`, timestamps | Catálogo com **6** estilos (1..6) |

`description` e `icon` foram adicionados em `v6/database/migrations/2024_04_20_105049_add_description_and_icon_column_to_ask_types_and_ask_styles_table.php`.

## 6. `filled_questionnaires`

| Coluna | Tipo | Null | Default | Observação |
|---|---|---|---|---|
| `id` | `integer` | não | seq | PK |
| `ip` | `inet` | não | — | IP do preenchimento (`$request->ip()`) |
| `questionnaire_id` | `bigint` | não | — | FK → `questionnaires.id` CASCADE |
| `answers` | `json` | não | — | Lista `[{ask_id, value}]` (cast `array`) |
| `created_at` / `updated_at` | `timestamp(0)` | sim | — | — |

## 7. `ask_files`

| Coluna | Tipo | Null | Default | Observação |
|---|---|---|---|---|
| `id` | `integer` | não | seq | PK; é o valor gravado na resposta |
| `ask_id` | `bigint` | não | — | FK → `asks.id` CASCADE |
| `filled_questionnaire_id` | `bigint` | sim | — | FK → `filled_questionnaires.id` CASCADE; `null` enquanto o arquivo está "pendente" |
| `title` | `varchar(100)` | sim | — | Nome original do arquivo |
| `path` | `varchar(255)` | não | — | Caminho relativo, ex.: `/content/formularios-dinamicos/{questionnaire_identifier}/arquivos/{ask_identifier}/{timestamp}-{rand}-{uniqid}.{ext}` |

## 8. Views SQL

### `listagem_questionarios` (definida em `v6/database/migrations/2024_04_02_003245_create_view_listagem_questionarios.php`, alterada depois)

Definição **ativa no banco**:

```sql
SELECT row_number() OVER (ORDER BY q.created_at DESC) AS value,
    q.title,
    q.slug,
    (SELECT count(sr.id) FROM asks sr WHERE sr.questionnaire_id = q.id) AS asks_count,
    to_char(q.published_at::timestamp with time zone, 'DD/MM/YYYY'::text) AS published_at,
    to_char(q.created_at::timestamp with time zone, 'DD/MM/YYYY'::text) AS created_at,
    q.id,
    q.status,
    q.has_result
   FROM questionnaires q
  WHERE q.deleted_at IS NULL
  ORDER BY q.created_at DESC;
```

Consumidor: `Prefecture\DatatableController@get_questionnaire_list` (`v6/app/Http/Controllers/Prefecture/DatatableController.php:452`).

### `listagem_questionarios_preenchidos` (migration `2024_04_11_171349_create_view_listagem_questionnaires_preenchidos.php`, em `v6/database/migrations/`)

Definição **ativa no banco**:

```sql
SELECT row_number() OVER (ORDER BY q.created_at DESC) AS value,
    q.title,
    (SELECT count(sr.id) FROM asks sr WHERE sr.questionnaire_id = q.id) AS asks_count,
    to_char(q.published_at::timestamp with time zone, 'DD/MM/YYYY'::text) AS published_at,
    count(fq.*) AS responses,
    q.id
   FROM filled_questionnaires fq
     JOIN questionnaires q ON q.id = fq.questionnaire_id
  WHERE q.deleted_at IS NULL
  GROUP BY q.id, q.title, q.published_at, q.created_at
  ORDER BY q.created_at DESC;
```

Consumidor: `Prefecture\DatatableController@get_filled_questionnaire_list` (`v6/app/Http/Controllers/Prefecture/DatatableController.php:458`).

> Existem ainda migrations `2024_04_27_003245_alter_view_listagem_questionarios_one.php` (lida e citada acima) e, na mesma série, arquivos `alter_view_listagem_questionarios_two` / `_three` e `alter_view_listagem_questionnaires_preenchidos` cujo conteúdo não pôde ser lido neste ambiente — o estado efetivo foi conferido direto no PostgreSQL.

## 9. Cronologia de migrations

| Data | Migration | Efeito |
|---|---|---|
| 2024-03-27 190858 | `2024_03_27_190858_create_questionnaires_table.php` | Cria `questionnaires` (identifier unique, status, title, description, published_at, softDeletes). |
| 2024-03-27 190859 | `2024_03_27_190859_create_ask_types_table.php` | Cria `ask_types` (id, name). |
| 2024-03-27 190860 | `2024_03_27_190860_create_ask_styles_table.php` | Cria `ask_styles` (id, name). |
| 2024-03-27 190914 | `2024_03_27_190914_create_asks_table.php` | Cria `asks` com `identifier` unique, `order` default 1, `size` default 12, `required` default true, `config` json e FKs CASCADE. |
| 2024-03-27 190936 | `2024_03_27_190936_create_filled_questionnaires_table.php` | Cria `filled_questionnaires` (ip inet, answers json). |
| 2024-03-27 191004 | `2024_03_27_191004_create_ask_files_table.php` | Cria `ask_files` (ask_id, filled_questionnaire_id, title, path). |
| 2024-04-02 000718 | `2024_04_02_000718_add_questionnaires_to_menu_order_and_resource_tables.php` | Registra o módulo no menu e em `resources`. |
| 2024-04-02 003245 | `2024_04_02_003245_create_view_listagem_questionarios.php` | Cria a view de listagem de questionários. |
| 2024-04-11 163017 | `2024_04_11_163017_add_filled_questionnaires_to_menu_order_and_resource_tables.php` | Registra o segundo recurso do módulo. |
| 2024-04-11 171349 | `2024_04_11_171349_create_view_listagem_questionnaires_preenchidos.php` | Cria a view de listagem de questionários preenchidos. |
| 2024-04-19 093859 | `2024_04_19_093859_create_ask_categories_table.php` | Cria `ask_categories` (order, name, questionnaire_id). |
| 2024-04-19 094604 | `2024_04_19_094604_add_ask_category_id_column_to_asks_table.php` | Liga `asks.ask_category_id`. |
| 2024-04-19 095049 | `2024_04_19_095049_add_slug_column_to_questionnaires_table.php` | Adiciona `slug`. |
| 2024-04-20 105049 | `2024_04_20_105049_add_description_and_icon_column_to_ask_types_and_ask_styles_table.php` | Adiciona `description` e `icon` aos catálogos. |
| 2024-04-23 175049 | `2024_04_23_175049_add_parent_id_column_to_ask_categories_table.php` | Hierarquia de categorias + `questionnaire_id` nullable. |
| 2024-04-24 115049 | `2024_04_24_115049_add_placeholder_column_to_asks_table.php` | Adiciona `placeholder`. |
| 2024-04-27 003245 | `2024_04_27_003245_alter_view_listagem_questionnaires_one.php` | Recria `listagem_questionarios` já com `status`. |
| 2024-07-19 122159 | `2024_07_19_122159_add_has_result_column_to_questionnaires_table.php` | Adiciona `has_result`. |
| 2024-12-02 052824 | `2024_12_02_052824_drop_unique_on_slug_column_questionnaires_table.php` | **Remove** o índice único de `slug` (permite slugs repetidos em perguntas diferentes). |
| 2025-03-25 104718 | `2025_03_25_104718_add_submit_info_column_to_questionnaires_table.php` | Adiciona `submit_info` (texto). |

> Migrations antigas `2022_05_17_090509_create_question_types_table.php`, `2022_05_17_092101_create_question_categories_table.php`, `2022_05_18_084825_create_questions_table.php` e `2023_03_28_142219_alter_order_to_questions_categories.php` pertencem a outro módulo (pesquisas/enquetes) e **não** são usadas por este.

## 10. Seeds

| Seed | Caminho | O que popula |
|---|---|---|
| `AskTypeSeeder` | `v6/database/seeds/DynamicForms/AskTypeSeeder.php` | 9 tipos (1..9) em `ask_types` via `updateOrInsert` |
| `AskStyleSeeder` | `v6/database/seeds/DynamicForms/AskStyleSeeder.php` | 6 estilos em `ask_styles` via `updateOrInsert` |
| `AddSatisfationLevelOptionOnAskType` | `v6/database/seeds/AddSatisfationLevelOptionOnAskType.php` | Insere o tipo 10 `SATISFACTION_LEVEL` com `updateOrCreate` |

Exemplo real (`v6/database/seeds/DynamicForms/AskTypeSeeder.php`):

```php
DB::table('ask_types')->updateOrInsert(['id' => AskTypesEnum::SELECT], ['name' => 'SELECT', 'description' => 'Única Escolha', 'icon' => 'fa-dot-circle-o']);
DB::table('ask_types')->updateOrInsert(['id' => AskTypesEnum::MULTI_SELECT], ['name' => 'MULTI_SELECT', 'description' => 'Escolhas', 'icon' => 'fa-list-ul']);
```

Estilos (`v6/database/seeds/DynamicForms/AskStyleSeeder.php`):

```php
DB::table('ask_styles')->updateOrInsert(['id' => AskStylesEnum::CPF], ['name' => 'CPF', 'description' => 'CPF', 'icon' => 'fa-id-card-o']);
DB::table('ask_styles')->updateOrInsert(['id' => AskStylesEnum::DATE], ['name' => 'DATE', 'description' => 'Data', 'icon' => 'fa-calendar-o']);
```

## 11. Dados de apoio fora do módulo

| Tabela | Registro | Origem |
|---|---|---|
| `modulos` | id 52, `formularios-dinamicos`, "Formulários Dinâmicos", `status = f` | `v6/database/migrations/2024_11_01_150206_create_modulos_table.php` |
| `resources` | id 71 `questionario` ("Formulários Dinâmicos"), id 72 `questionario-preenchido` ("Questionários Preenchidos") | linhas gravadas pela migration `2024_04_02_000718_*` e `2024_04_11_163017_*` |
| `menu_orders` | id 1, slug `formulario-dinamico`, `modulo_id = 52` | migration `2024_04_02_000718_*` |

O item de menu `formulario-dinamico` é o que `menuClicks('formulario-dinamico')` incrementa ao abrir as duas listagens do CMS.
