# Ouvidoria — Ciclo de Vida do Chamado (ponta a ponta)

> Documento de referência do fluxo completo: do formulário público até o arquivamento, com todas as transições, gatilhos e efeitos colaterais.

## 1. Visão geral

```
   CIDADÃO                          CMS (operador)                     CIDADÃO
      │                                   │                                │
      │ ① POST /ouvidoria/store           │                                │
      ├──────────────────────────────────►│                                │
      │   cria ombudsman_requests         │                                │
      │   status_id = 1 (Em Tramitação)   │                                │
      │   protocol = OUV-dmY-His          │                                │
      │   token = Hash::make(token)       │                                │
      │   mail → cidadão + mail → setor   │                                │
      │◄──────────────────────────────────┤                                │
      │ ② exibe protocolo + prazo         │                                │
      │                                   │                                │
      │                        ③ GET /cms/chamado-da-ouvidoria/responder/{id}
      │                                   │◄───────────────────────────────┤
      │                                   │   lê chamado + respostas + logs │
      │                                   │                                │
      │                        ④ POST /cms/chamado-da-ouvidoria/responder
      │                                   │   type=1 → status 2 (Atendido)  │
      │                                   │   type=0 → status 3 (Indeferido) │
      │                                   │   grava ombudsman_answers       │
      │                                   │   grava mask_answer             │
      │                                   │   mail → cidadão                │
      │                                   │                                │
      │ ⑤ GET /ouvidoria?consulta=protocol│                                │
      │◄───────────────────────────────────────────────────────────────────►│
      │   status + solicitante + resposta │                                │
      │                                   │                                │
      │ ⑥ POST /ajax/new-message/ouvidoria│                                │
      │   (contestação, com token)        │                                │
      │◄───────────────────────────────────────────────────────────────────►│
      │                                   │                                │
      │              ⑦ GET /cms/chamado-da-ouvidoria/prorrogar/{id}         │
      │                                   │  status_id = 4 (Prorrogado)     │
      │                                   │  mail → cidadão                 │
      │                                   │                                │
      │              ⑧ POST /cms/chamado-da-ouvidoria/enviar              │
      │                                   │  troca assunto/setor            │
      │                                   │  grava log type='forward'       │
      │                                   │  mail → novo destinatário       │
      │                                   │                                │
      │ ⑨ GET /relatorio-estatistico-ouvidoria?ano=YYYY                   │
      │◄───────────────────────────────────────────────────────────────────►│
```

## 2. Etapa ① — Criação da manifestação

### 2.1 Entrada do cidadão

| Campo | Origem na tela | Regra |
|---|---|---|
| `modality` | select "Modalidade do chamado" | obrigatória |
| `type_id` | select "Tipo de chamado" (só os com `status = true`) | obrigatória |
| `subject_id` | select "Assunto", agrupado em `<optgroup>` por `sector_name` | obrigatório |
| `response_method_id` | select "Forma de resposta" | obrigatório |
| `origin_id` | select "Origem do chamado" | obrigatório |
| `name`, `email`, `phone` | inputs | desabilitados pelo JS se `modality = anonimo` |
| `address`, `number`, `neighborhood`, `reference_point`, `complement` | inputs | idem |
| `description` | textarea "Mensagem:" | obrigatória |
| `token` | input "Digite um token para poder contestar a resposta posteriormente" | obrigatório |
| captcha | `{!! Recaptcha::render() !!}` | exibido, **não validado** |

### 2.2 Processamento (portal)

```php
// v6/app/Http/Controllers/Portal/OmbudsmanController.php
$validated = $this->validated($request);

$ombudsman = new OmbudsmanRequest();
$ombudsman->modality            = $request->modality;
$ombudsman->type_id             = $request->type_id;
$ombudsman->subject_id          = $request->subject_id;
$ombudsman->response_method_id  = $request->response_method_id;
$ombudsman->origin_id           = $request->origin_id;
$ombudsman->protocol            = 'OUV-' . now()->format('dmY-His');
$ombudsman->anonymous           = $request->has('anonymous') ? true : false;
$ombudsman->name                = $request->name;
$ombudsman->email               = $request->email;
$ombudsman->phone               = $request->phone;
$ombudsman->description         = $request->description;
$ombudsman->mask_description    = $this->maskTextWithAi($request->description ?? '');
$ombudsman->number              = $request->number;
$ombudsman->neighborhood        = $request->neighborhood;
$ombudsman->complement          = $request->complement;
$ombudsman->address             = $request->address;
$ombudsman->reference_point     = $request->reference_point;
$ombudsman->user_option         = $request->user_option;
$ombudsman->status_id           = 1;   // Pendente
$ombudsman->ip                  = $_SERVER["REMOTE_ADDR"];
$ombudsman->browser             = $request->header('User-Agent');
$ombudsman->token               = Hash::make($request->token);
$insertOmbudsman = $ombudsman->save();
```

### 2.3 E-mails disparados

| # | Destinatário | Template | Condição |
|---|---|---|---|
| 1 | `$request->get('email')` (o cidadão) | `prefecture.emails.ombudsman-requests.email_user` | `!empty($request->get('email'))` |
| 2 | `OmbudsmanSubject::where('id', $ombudsman->subject_id)->first()->email` (o setor) | `prefecture.emails.ombudsman-requests.email_admin` | sempre (fallback: `Configuration.email`) |

Variáveis passadas: `title`, `browser`, `ip`, `protocol`, `welcome`, `copyright`, `ombudsmanRequest` (+ `token` no e-mail ao cidadão).

> **Destinatário do e-mail ao setor:** o controller resolve `$to = OmbudsmanSubject::where('id', $ombudsman->subject_id)->first()->email;`, com fallback para `Configuration.email` se o assunto não tiver e-mail. A saudação é calculada pela hora do servidor ("Bom dia" < 12h, "Boa tarde" < 18h, "Boa noite").

### 2.4 Resposta ao cidadão

View `portal.layouts.{modelo}.transparencia-fiscal.ouvidoria-solicitada` com `success`, `ombudsmanRequest`, `title`, `protocol` e `prazo_resposta` (de `configurations.response_time`).

## 3. Etapa ③ — Abertura da tela de resposta

```
GET /cms/chamado-da-ouvidoria/responder/{id}
  → OmbudsmanAnswerController@createByRequest(int $idRequest): View
      $ombudsman_request = OmbudsmanRequest::with('type','subject','responseMethod','origin','status')->find($idRequest)
      $answers           = OmbudsmanAnswer::where('ombudsman_request_id', $idRequest)->with('archive')->get()
      $request_logs      = OmbudsmanRequestLogs::where('ombudsman_request_id', $idRequest)
                            ->with('user','sectorFrom','sectorTo','subjectFrom','subjectTo')->get()
      return view('prefecture.ombudsman.request.answer.create', compact(...))
```

A tela exibe, em ordem:
1. Dados do chamado (status, modalidade, tipo, assunto, meio de resposta, origem, data, protocolo, anônimo, nome, e-mail, telefone, número, bairro, complemento, endereço, ponto de referência, descrição).
2. **Histórico de Tramitação** (se houver logs): data, usuário, setor origem/destino, assunto origem/destino, observação.
3. Respostas anteriores (com anexos).
4. Formulário: editor WYSIHTML5 (`#resposta`), menu de **Respostas Prontas**, linhas de anexo (`archive[]` com `description`), botão de envio.

> **Bug verificado:** no bloco do histórico, `{{$log->subjectFrom->title}}` e `{{$log->subjectTo->title}}` são acessados sem `optional()`, ao contrário do bloco de setor (`optional($log->sectorFrom)->title ?? '-'`). Um log com `subject_from_id` ou `subject_to_id` nulo gera **erro fatal** na renderização.

> **Bug verificado:** o `$answers` carrega `->with('archive')` — relação `hasOne` por `archives_id`. Como o model usa `belongsToMany` comentado e a tabela pivô não existe, os anexos **não** aparecem.

## 4. Etapa ④ — Resposta ou indeferimento

### 4.1 Contrato do POST

`POST /cms/chamado-da-ouvidoria/responder` (`enviar_resposta`), validado por `OmbudsmanAnswerRequest`:

| Campo | `type=1` (atender) | `type=0` (indeferir) | Outro |
|---|---|---|---|
| `ombudsman_request_id` | required | required | required |
| `type` | required\|boolean | required\|boolean | required\|boolean |
| `answer` | **required** | — | — |
| `rejected` | — | **required** | — |
| `archive` | — | — | — (não validado) |

### 4.2 Processamento

```
DB::transaction:
  ① dataForm = { ombudsman_request_id }
  ② se status==2: dataForm += { answer: $request->answer }   ; emailAnswer=true, emailRejected=false
     se status==3: dataForm += { answer: $request->rejected } ; emailRejected=true, emailAnswer=false
  ③ dataForm += { mask_answer: $this->maskTextWithAi($dataForm['answer']) }
  ④ $answer = OmbudsmanAnswer::create($dataForm)
  ⑤ para cada $request->archive:
       - sanitiza o nome (tira extensão, troca espaços por "-", remove acentos, mantém [a-zA-Z0-9_ -])
       - Archive::create([name, extension, type, description])
       - $insertArchive->path = url("storage/answer/{answer_id}/{name}")
       - $archive[0]->storeAs("answer/{answer_id}/", $name)
       - $answer->archives()->attach($insertArchive->id)     ← FALHA (método comentado + pivô inexistente)
  ⑥ $ombudsmanRequest->status_id = $status; save()
  ⑦ activity()->performedOn($ombudsmanRequest)->log('Chamado de Ouvidoria respondida')
  ⑧ Mail::send(view, [...]) → conforme §4.3
```

### 4.3 Escolha do template de e-mail

| Condição | Template |
|---|---|
| `$emailAttach && $emailAnswer` | `prefecture.emails.ombudsman-answer.email_answer_attach` |
| `!$emailAttach && $emailAnswer` | `prefecture.emails.ombudsman-answer.email_answer_no_attach` |
| `$emailRejected` | `prefecture.emails.ombudsman-answer.email_rejected` |

### 4.4 Estados possíveis

| `type` | `status_id` | `answer` recebe | E-mail |
|---|---|---|---|
| 1 | 2 (Atendido) | `$request->answer` | `email_answer_attach` ou `email_answer_no_attach` |
| 0 | 3 (Indeferido) | `$request->rejected` | `email_rejected` |

> **Observação:** a coluna `answer` da resposta guarda **o mesmo texto** conforme o tipo — o campo `rejected` do request é gravado em `answer`. Não há distinção no banco entre "resposta" e "justificativa de indeferimento"; o distinction está apenas em `status_id`.

## 5. Etapa ⑤ — Consulta pelo cidadão

```
POST /cms/ajax/get/ombudsman/by/protocol   (nome: ajax.get.ombudsman.by.protocol)
  → OmbudsmanRequest::where('protocol', $request->get('protocol'))->first()
  → 200: JSON do model COMPLETO
  → 200: {"message": "Protocolo não encontrado."} (se não achou)

POST /cms/ajax/get/answer/by/ombudsman      (nome: ajax.get.answer.by.ombudsman)
  → $ombudsmanRequest = OmbudsmanRequest::where('protocol', '=', ...)->first()
  → $answers = OmbudsmanAnswer::where('ombudsman_request_id', $ombudsmanRequest->id)->get()
  → 200: [{answer: {id, mask_answer, created_at}}]
  → 404: [] (se não há respostas)
```

A view `consulta-solicitacao-ouvidoria.blade.php` usa esses dois endpoints e monta a tabela:

| Linha | Origem |
|---|---|
| Status (com cor) | `response.status_id` → `OmbudsmanStatus` (1/2/3/4) |
| Protocolo | `response.protocol` (em claro) |
| Solicitante | `response.name` (em claro) |
| Data do pedido | `response.created_at` |
| Descrição do pedido | `response.description` (em claro) |
| Respostas | `ajax.get.answer.by.ombudsman` → `mask_answer` |
| Contestações | `response.answer_contestation` e `answer_contestation_2` |

> **Risco de LGPD:** `name`, `description` e `protocol` em claro para quem tiver o protocolo. O relatório público, em contraste, usa `masked_protocol` e `masked_name`.

## 6. Etapa ⑥ — Contestação

```
POST /ajax/new-message/ouvidoria   (nome: new.message.ombudsman)
  → $ombudsmanRequest = OmbudsmanRequest::where('protocol', $request->protocol)->first()
  → if (!$request->has('contestation_token')
        || !Hash::check($request->contestation_token, $ombudsmanRequest->token))
        return 403 {"error": "Token inválido."}
  → if ($ombudsmanRequest->answer_contestation) {
        answer_contestation_2 = $request->message;  answer_contestation_2_date = now();
    } else {
        answer_contestation = $request->message;   answer_contestation_date = now();
    }
  → save() → 200 {"success": "Mensagem salva!"}
```

Regras:
- O token é **do próprio cidadão** (digitado livremente no formulário e hasheado no ato do envio).
- A 1ª contestação preenche `answer_contestation`; a 2ª, `answer_contestation_2`.
- Uma 3ª contestação **sobrescreve** a `answer_contestation` (o `if` testa apenas se a 1ª está preenchida).

## 7. Etapa ⑦ — Prorrogação

```
GET /cms/chamado-da-ouvidoria/prorrogar/{id}   (nome: prorrogar_chamado)
  → $status = 4
  → DB::transaction:
      $ombudsmanRequest->status_id = 4; save()
      activity()->log('Chamado da ouvidoria respondido')   ← descrição imprecisa
      se !empty($to = $ombudsmanRequest->email):
          Mail::send('prefecture.emails.ombudsman-answer.email_to_extend', [...])
  → flash success → redirect()->back()
```

- O destinatário é o e-mail do **cidadão** (`$ombudsmanRequest->email`).
- Se o chamado for anônimo/sigiloso sem e-mail, **nenhum** e-mail é enviado (guarda `if (!empty($to))`), mas o status é alterado normalmente.
- A tela de listagem mostra o botão "Prorrogar" com o ícone `fa fa-clock-o` (azul).

## 8. Etapa ⑧ — Encaminhamento

```
POST /cms/chamado-da-ouvidoria/enviar   (nome: enviar_chamado)
  Body: { ombudsman_request_id, new_email, title_subject }
  → $ombudsmanRequest = OmbudsmanRequest::with('type','subject','responseMethod','origin','status')
                              ->where('id', $request->ombudsman_request_id)->first()
  → $old_sector_id  = $ombudsmanRequest->subject->sector_id
  → $old_subject_id = $ombudsmanRequest->subject_id
  → $new_subject    = OmbudsmanSubject::where('title', $request->title_subject)->first()
  → se $new_subject:
        $ombudsmanRequest->subject_id = $new_subject->id; save()
  → OmbudsmanRequestLogs::create([
        ombudsman_request_id, user_id,
        sector_from_id  => $old_sector_id,
        sector_to_id    => $ombudsmanRequest->subject->sector_id,
        subject_from_id => $old_subject_id,
        subject_to_id   => $ombudsmanRequest->subject_id,
        type            => 'forward',
        observation     => 'Alteração de assunto e setor no chamado da Ouvidoria.'
    ])
  → Mail::send('prefecture.emails.ombudsman-send-requests.email_admin', [...]) → $request->new_email
  → 200 {"success": true, "message": "E-mail enviado com sucesso."}
```

Observações:
- O `status_id` **não** muda (o chamado continua "Em Tramitação").
- O assunto é buscado **por título** — títulos duplicados são um risco (§2.9 de `08-GAPS-E-DIVIDAS.md`).
- O log é do tipo `forward` — os outros 4 valores do enum (`status_change`, `closed`, `reopened`, `comment`) nunca são gravados.

## 9. Etapa ⑨ — Relatório estatístico público

```
GET /relatorio-estatistico-ouvidoria?ano=YYYY[&type=xls|csv|pdf|txt]
  → Portal\OmbudsmanController@relStatitics
      ├─ $content = StaticPage::where('slug', 'relatorio-estatistico-ouvidoria')->first()
      │    ├─ se existe: $archives, $view_pdf, $title, incrementa $content->clicks
      │    └─ senão:    $archives=null, $view_pdf=null, $title='Página não encontrada', $null=true
      ├─ $ombudsmanRequests = OmbudsmanRequest::with('status')->byDateBetweenMain($ano)->get()
      ├─ se $request->filled('type') → $this->export($ombudsmanRequests, $type)
      ├─ 8 contadores (§9.1)
      ├─ $datas = anos distintos com chamados (para o filtro de ano)
      ├─ $fullListRequestsDisabled = (int) Configuration('full_list_requests_disabled') === 1
      └─ view relatorio-estatistico-ouvidoria.index
```

### 9.1 Os 8 indicadores

| Indicador | Escopo | Cálculo |
|---|---|---|
| `emTramitacao_apos_prazo` | status 1 | `created_at` → mais de 20 dias até agora |
| `emTramitacao_no_prazo` | status 1 | `created_at` → até 20 dias |
| `prorrogado_apos_prazo` | status 4 | `updated_at` → mais de 20 dias |
| `prorrogado_no_prazo` | status 4 | `updated_at` → até 20 dias |
| `indeferido_apos_prazo` | status 3 | `answer.created_at - request.created_at` > 20 dias |
| `indeferido_no_prazo` | status 3 | ≤ 20 dias |
| `atendido_apos_prazo` | status 2 | > 20 dias |
| `atendido_no_prazo` | status 2 | ≤ 20 dias |

Implementação dos escopos (no model):

```php
public function scopeGetByBetweenDate($query, $year, $statusId, $comparationForm)
{
    $values = $this->checkParameters($statusId, $comparationForm);
    if (!empty($year) && isset($year)) {
        $query->whereBetween($values['fieldBetween'], [
            Carbon::create($year)->startOfYear(),
            Carbon::create($year)->endOfYear(),
        ]);
    }
    return $query->where('status_id', $statusId)
        ->whereRaw('EXTRACT(DAY FROM now() - '.$values['fieldExtract'].') '.$values['comparation'].' 20')
        ->groupBy('status_id');
}

public function scopeGetByBetweenDateWithAnswer($query, $year, $statusIdRequest, $comparationFormAnswer)
{
    $values = $this->checkParameters($statusIdRequest, $comparationFormAnswer);
    if (!empty($year) && isset($year)) {
        return DB::table('ombudsman_answers')
            ->join('ombudsman_requests', 'ombudsman_answers.ombudsman_request_id', '=', 'ombudsman_requests.id')
            ->where('ombudsman_requests.status_id', $statusIdRequest)
            ->whereRaw('EXTRACT(DAY FROM ombudsman_answers.'.$values['fieldExtract'].' - ombudsman_requests.'.$values['fieldExtract'].') '.$values['comparation'].' 20')
            ->whereBetween('ombudsman_requests.'.$values['fieldBetween'], [ ... ]);
    }
    // ... (sem filtro de ano)
}
```

`checkParameters` decide o campo de data:

| Status | `fieldExtract` (data comparada) | `fieldBetween` (filtro de ano) |
|---|---|---|
| 1, 2, 3 | `created_at` | `created_at` |
| 4 (prorrogado) | **`updated_at`** | `created_at` |

### 9.2 Exportações

| `type` | Resultado |
|---|---|
| `xls` | `Excel::download(new OmbudsmanRequestsExport($ombudsmanRequests), 'relatorio-estatistico-ouvidoria.xlsx')` |
| `csv` | idem com `.csv` |
| `pdf` | `PDF::loadView('exports.ombudsman-requests', ['ombudsmanRequests' => …])->download('relatorio-estatistico-ouvidoria.pdf')` |
| outro | `exportTxt()` — texto com cabeçalho "Ouvidoria - Relatório Estatístico" e colunas Status \| Protocolo \| Solicitante \| Data do pedido, usando `masked_protocol` e `masked_name` |

> **Nota sobre a v5:** a v5 tinha uma rota **separada** `/relatorio-estatistico-ouvidoria/exportacao` (`exportRelStatitics`) e um `exportOmbudsmanRequestsTxt` com cabeçalho `"Ouvidoria - Relatório Estatístico - {app.name}"` e filtro de ano. A v6 unificou tudo em `relStatitics` + `export()`, e o TXT perdeu o nome da aplicação e a linha "Ano de Gestão".

## 10. Tabela-resumo do ciclo

| # | Etapa | Status | Tabela(s) alterada(s) | E-mail | Log |
|---|---|---|---|---|---|
| ① | Protocolar | 1 | `ombudsman_requests` (INSERT) | cidadão + setor | — |
| ② | Exibir protocolo | 1 | `static_pages.clicks` | — | — |
| ③ | Abrir tela de resposta | 1 | — | — | — |
| ④ | Responder | 2 | `ombudsman_answers` (INSERT), `ombudsman_requests.status_id`, `archives` | cidadão | activity |
| ④' | Indeferir | 3 | idem | cidadão | activity |
| ⑤ | Consultar | — | — | — | — |
| ⑥ | Contestar | — | `ombudsman_requests.answer_contestation*` | — | — |
| ⑦ | Prorrogar | 4 | `ombudsman_requests.status_id` | cidadão | activity |
| ⑧ | Encaminhar | (inalterado) | `ombudsman_requests.subject_id`, `ombudsman_request_logs` (INSERT) | novo destinatário | — |
| ⑨ | Relatório | — | `static_pages.clicks` | — | — |
| — | Visualizar no CMS | — | — | — | activity (listagem) |

## 11. Onde o ciclo quebra

| Ponto | Quebra | Evidência |
|---|---|---|
| ① | Manifestação anônima não é marcada como `anonymous` | `if ($ombudsman->modality == 'Anônimo')` nunca casa (§2.6 dos gaps) |
| ① | Colisão de protocolo no mesmo segundo | `'OUV-' . now()->format('dmY-His')` (§2.7 dos gaps) |
| ① | Captcha aceito sem validação | regra comentada (§2.5 dos gaps) |
| ② | O prazo exibido (`response_time`) pode divergir do prazo usado no relatório (20 hardcoded) | I1 dos gaps |
| ③ | Log com `subject_from_id` nulo → erro fatal na tela | `{{$log->subjectFrom->title}}` sem `optional()` |
| ④ | Resposta **com anexo** falha e reverte | `archives()` comentado + pivô inexistente |
| ④ | Falha na resposta → `dd($e)` expõe internals | — |
| ⑤ | Consulta expõe `name`, `description`, `ip`, `browser` em claro | LGPD |
| ⑥ | 3ª contestação sobrescreve a 1ª | — |
| ⑦ | Descrição do log diz "respondido" em vez de "prorrogado" | — |
| ⑧ | `sector_from_id` gravado com id de `ombudsman_sectors` em coluna com FK para `contents` | funciona só por coincidência do seed |
| ⑨ | Registro com `deleted_at` preenchido entra na contagem | escopos do model não filtram `deleted_at` |
| ⑨ | Status 4 medido por `updated_at` | qualquer edição recalcula o prazo |
| ⑨ | Relatório **institucional** (PDF anual) não existe mais | regressão da v5 (§1.1 dos gaps) |
