# Ouvidoria — Views e Experiência

> **Slug:** `Ouvidoria` · **Versão analisada:** `lotep` (Laravel 5.5.50 / PHP 7.4)
>
> Três telas (duas no CMS, uma no portal), nenhum JavaScript próprio, nenhuma exportação e
> nenhum upload. As views usam os layouts e partials compartilhados da camada `V2/layouts`.

---

## 1. Telas do CMS

| Tela | Blade | Rota | Ações disponíveis | Quem acessa |
|---|---|---|---|---|
| Lista de manifestações | `V2/Modules/Servicos/Ouvidoria/Views/cms/requests/index.blade.php` | `v2.ouvidoria.requests.index` | buscar, ordenar, paginar, abrir detalhe | `ouvidoria.read` ou `write` |
| Detalhe da manifestação | `V2/Modules/Servicos/Ouvidoria/Views/cms/requests/show.blade.php` | `v2.ouvidoria.requests.show` | alterar status, responder, voltar | `read`; as duas ações exigem `write` |

### 1.1 `index` — lista

- **Colunas** (`Views/cms/requests/index.blade.php:27-38`): Status, Protocolo, Nome, Tipo, Data,
  Ações.
- **Status** é renderizado por closure com `switch ((int) $row->status)`
  (`index.blade.php:6-17`): `1` → badge amarelo “Em tramitação”; `2` → verde “Atendido”;
  `3` → vermelho “Indeferido”; qualquer outro → cinza “Desconhecido”.
- **Nome** cai para “Anônimo” quando vazio (`$row->name ?: 'Anônimo'`,
  `index.blade.php:30-32`). Não consulta `is_anonymous`.
- **Tipo** usa `$row->type->title ?? '—'` — lazy load por linha, gerando **N+1** (o `index` não
  faz *eager load* de `type`, diferente do `show`).
- **Ações**: um único botão “Ver detalhes” com `aria-label` incluindo o protocolo
  (`index.blade.php:19-25`).
- **Filtros**: apenas `search` (`filter-card`, `showDateRange => false`,
  `index.blade.php:56-62`). O placeholder diz “Buscar por protocolo ou nome...”, mas o servidor
  também busca `email`.
- **Ordenação**: colunas `status`, `protocol` e `created_at` marcadas `sortable`
  (`index.blade.php:28-36`); o partial reescreve os links com `sort`/`dir`.
- **Paginação**: 20 por página, com `appends($r->query())` no controller.
- **Estado vazio**: ícone `bi-chat-dots`, título “Nenhuma manifestação encontrada”; a mensagem
  muda quando há busca (`index.blade.php:73-79`).
- **Estado de erro**: não há tratamento próprio; erros de banco sobem como 500.

### 1.2 `show` — detalhe

Estrutura em duas colunas (`col-lg-8` + `col-lg-4`):

- **Dados da manifestação** (`show.blade.php:23-39`): Tipo, Assunto, Origem, Forma de resposta,
  Nome (ou “Anônimo”), E-mail, Telefone, Endereço, Recebido em (`d/m/Y H:i:s`) e **IP** em
  `<code>`.
- **Descrição** (`show.blade.php:41-46`): parágrafo com `white-space: pre-line` para preservar
  quebras de linha do texto puro.
- **Respostas** (`show.blade.php:48-60`): `@forelse($request->answers)` na ordem de inserção,
  sem ordenação explícita; mostra texto e data.
- **Status** (`show.blade.php:63-89`): badge + formulário `POST` para
  `v2.ouvidoria.requests.status` com três opções (1/2/3), exibido só para `write`.
- **Responder** (`show.blade.php:91-106`): `textarea` obrigatório, `POST` para
  `v2.ouvidoria.requests.answer`, exibido só para `write`.
- **Voltar**: botão no slot `page-actions` (`show.blade.php:11-16`).

O detalhe não permite editar dados do manifestante, excluir a manifestação, anexar arquivo nem
escolher o desfecho da resposta — o único caminho de resposta grava `status = 2`.

---

## 2. Telas do Portal

| Tela | Blade | Rota | Público? | Layout |
|---|---|---|---|---|
| Canal de Ouvidoria | `V2/Modules/Servicos/Ouvidoria/Views/portal/index.blade.php` | `v2.ouvidoria.portal.index` | sim | `v2-portal::app-layout` |

A view declara o layout com as flags de estrutura:

```blade
{{-- V2/Modules/Servicos/Ouvidoria/Views/portal/index.blade.php:1-12 --}}
@extends('v2-portal::app-layout', [
    'pageTitle' => 'Ouvidoria',
    'pageDescription' => 'Registre elogios, reclamações, sugestões e denúncias no canal oficial de Ouvidoria.',
    'showAccessibilityBar' => true,
    'stickyAccessibilityBar' => true,
    'showHeader' => true,
    'showMainHeader' => true,
    'showMainFooter' => true,
    'showFooter' => true,
    'showMainLeftArea' => false,
    'showMainRightArea' => false
])
```

Conteúdo:

1. Título `h2` “Canal Direto de Ouvidoria” com ícone e **estilo inline**
   (`style="border-bottom: 2px solid var(--bs-border-color);"`, `index.blade.php:17-20`).
2. Parágrafo institucional (`index.blade.php:22-26`).
3. Alerta de sucesso vindo de `session('success')` e bloco de erros de validação com
   `role="alert"` (`index.blade.php:28-47`).
4. Formulário `POST /ouvidoria/store` com `novalidate` e `csrf_field()`
   (`index.blade.php:51-202`).
5. Caixa informativa fixa com e-mail `ouvidoria@lotep.pb.gov.br` e horário
   “Segunda a Sexta, das 08:00 às 14:00” (`index.blade.php:204-215`). Os valores são
   *hardcoded* na view, não vêm de configuração.

---

## 3. Formulários

| Formulário | Blade | Action | Campos | Validação no cliente |
|---|---|---|---|---|
| Manifestação | `Views/portal/index.blade.php:51` | `v2.ouvidoria.portal.store` | tipo, assunto, origem, forma de resposta, nome, e-mail, telefone, endereço, descrição, consentimento | `required` em tipo e descrição; `novalidate` desliga a validação nativa do navegador |
| Alterar status | `Views/cms/requests/show.blade.php:75` | `v2.ouvidoria.requests.status` | `status` (1/2/3) | nenhuma |
| Responder | `Views/cms/requests/show.blade.php:95` | `v2.ouvidoria.requests.answer` | `answer` | `required` no `textarea` |

Campos do portal e o que o servidor realmente exige:

| Campo | `name` | Obrigatório no HTML | Obrigatório no servidor | Persistido |
|---|---|---|---|---|
| Tipo | `v2_type_id` | sim (`required`, `aria-required`) | não (`nullable`) | sim |
| Assunto | `v2_subject_id` | não | não | sim |
| Origem | `v2_origin_id` | não | não | sim |
| Forma de resposta | `v2_response_method_id` | não | não | sim |
| Nome | `name` | não | não | sim |
| E-mail | `email` | não | não (formato validado) | sim |
| Telefone | `phone` | não | não | sim |
| Endereço | `address` | não | **nem formato nem tamanho** | sim (limite do banco: 500) |
| Descrição | `description` | sim | **sim** | sim |
| Consentimento | `user_option` | não | **não** | **não** |

O descompasso relevante: o campo “Tipo” é marcado como obrigatório na tela e aceito vazio pelo
servidor; o consentimento LGPD é apresentado como condição (“Autorizo o uso dos meus dados...”,
`index.blade.php:184-193`) mas não é exigido, não é validado e não tem coluna no banco.

Erros são exibidos no topo (resumo com `$errors->all()`) **e** abaixo de cada campo via
`is-invalid`/`invalid-feedback`. O layout do portal usa o mesmo `$errors` bag do Laravel 5.5.
O formulário usa `old()` nos selects, nos campos de texto e no checkbox de consentimento.

---

## 4. Layouts

| Layout | Caminho | Usado por | Como se escolhe |
|---|---|---|---|
| CMS | `V2/layouts/cms/layouts/app.blade.php` | `Views/cms/requests/index.blade.php`, `show.blade.php` | `@extends('v2-cms::layouts.app', [...])` |
| Portal | `V2/layouts/portal/app-layout.blade.php` | `Views/portal/index.blade.php` | `@extends('v2-portal::app-layout', [...])` |

Os namespaces `v2-cms` e `v2-portal` são registrados no boot:

```php
// V2/Core/Providers/V2ServiceProvider.php:36-42
$this->loadViewsFrom(base_path('V2/layouts/cms'), 'v2-cms');
$this->loadViewsFrom(base_path('V2/layouts/portal'), 'v2-portal');
```

Partials do CMS usados pelo módulo:

| Partial | Caminho | Papel |
|---|---|---|
| `breadcrumb` | `V2/layouts/cms/partials/breadcrumb.blade.php` | trilha “Serviços > Ouvidoria” |
| `filter-card` | `V2/layouts/cms/partials/filter-card.blade.php` | busca e (opcional) período |
| `table` | `V2/layouts/cms/partials/table.blade.php` | tabela, ordenação, paginação e vazio |
| `pagination` | `V2/layouts/cms/partials/pagination.blade.php` | usada internamente pelo `table` |

O `table` renderiza a saída das closures com `{!! ... !!}` (sem escape,
`V2/layouts/cms/partials/table.blade.php:141`). Hoje as closures escapam com `e()` os dados
variáveis e devolvem HTML fixo nos badges, então não há XSS nos caminhos atuais; a construção
de HTML dentro do PHP, porém, tira a garantia estrutural da Blade.

---

## 5. JavaScript e Ajax

| Arquivo | Endpoints chamados | Eventos | Tratamento de erro |
|---|---|---|---|
| — | — | — | — |

Não há JS específico do módulo, `fetch`, jQuery, polling, máscara, contador de caracteres nem
preview. As páginas herdam o bundle do layout (Bootstrap). O CMS usa ícones Bootstrap Icons
(`bi-*`) e o portal também; não há dependência de CDN declarada no módulo.

Consequência de UX: o envio é um *full page reload*; não há feedback de progresso nem proteção
contra duplo clique. O antiabuso é só o `throttle:10,1`.

---

## 6. Exportações

Nenhuma. Não há Excel, CSV, PDF, impressão dedicada nem endpoint de download. A listagem só
existe na tela paginada. A V1 também não tinha exportação de ouvidoria documentada.

---

## 7. Upload de arquivos

| Campo | Tipo | Validação | Onde é gravado | Nome gerado |
|---|---|---|---|---|
| — | — | — | — | — |

Não há upload no portal nem na resposta. A coluna `file_url` de `v2_ombudsman_answers` e o
`fillable` correspondente existem, mas nenhum action a preenche. Respostas com anexo existiam
na V1 (`archives_id` em `ombudsman_answers`) e são importadas para a galeria pelo
`OuvidoriaLegacyImporter`, não por este módulo.

---

## 8. Acessibilidade e responsividade

Pontos corretos:

- todos os controles têm `<label for>`; os `select` têm opção vazia “Selecione...”;
- ícones decorativos usam `aria-hidden="true"`; o botão de detalhe tem `aria-label`;
- o resumo de erros tem `role="alert"` e o alerta de sucesso `role="status"`;
- a tabela recebe `ariaLabel` (“Manifestações da ouvidoria”) no partial;
- grid responsivo `col-md-6`, `col-lg-8`/`col-lg-4`.

Lacunas:

- `novalidate` no formulário do portal desliga a validação nativa sem substituí-la por feedback
  imediato (a validação só aparece após o *submit*);
- `aria-required` está apenas em tipo e descrição; os demais campos obrigatórios do servidor
  (nenhum além da descrição) não têm marcação;
- os `invalid-feedback` não são ligados por `aria-describedby` ao campo;
- a tabela não expõe `aria-sort` nos cabeçalhos ordenáveis;
- o título da página do portal é `h2` sem `h1` visível no conteúdo (o layout pode fornecer o
  `h1`; a view não depende dele);
- mensagens de erro são genéricas do Laravel 5.5, sem tradução para o domínio.

---

## 9. Internacionalização e formatação

| Dado | Formatação | Locale/regra |
|---|---|---|
| Datas no CMS | `d/m/Y H:i:s` | pt-BR fixo, via `format()` do Carbon |
| Datas no portal | não exibidas | — |
| Rótulos, mensagens e *placeholders* | português fixo no código | sem arquivos de lang |
| Protocolo | `OUV-{Ymd}-{8}` | dígitos e hífen, sem localização |
| Status | rótulos fixos “Em tramitação”, “Atendido”, “Indeferido” | duplicados em `index.blade.php` e `show.blade.php` |

Não há seletor de idioma, `__()` nem arquivo de tradução no módulo. A camada V7 exige i18n
(ADR-012), o que implica extrair rótulos e mensagens para `Lang/{locale}`.
