# BI — Painel de gestão (`/dashboard`)

Documento de controle do BI de treinamento/questionário.

**Rota:** `GET /dashboard`  
**Participante:** `User` com `trainee_profiles` ou quem possui sessão de questionário na empresa.

**Arquitetura:** `DashboardController` → `TrainingDashboardService` → `TrainingDashboardRepository` (interface + binding em `AppServiceProvider`). Validação: `DashboardRequest`. Filtros tipados: `TrainingDashboardPeriodFilter`, `TrainingDashboardParticipantStatusFilter`.

**Último login:** `App\Listeners\RecordUserLastLoginAt` no evento `Login` atualiza `users.last_login_at` — usado no KPI e listagem “sem login”.

## Indicadores

| # | Indicador | Status |
|---|-----------|--------|
| 1 | Total de participantes | ✅ |
| 2 | Participantes sem login | ✅ |
| 2b | **KPI:** Começou e não finalizou (total) | ✅ |
| 3 | Ranking por time | ✅ |
| 4 | Perguntas com mais acerto | ✅ (+ gráfico barras horizontais) |
| 5 | Perguntas com mais erros | ✅ (+ gráfico barras horizontais) |
| 6 | Erros mais frequentes por pergunta | ✅ |
| 7 | Começou e não finalizou | ✅ |
| 8 | **Baterias de perguntas** (progresso por bateria) | ✅ |
| 8b | **Export Excel** — resumo por bateria | ✅ |

## Filtros

Formulário único no topo. Datas em #1–#2 filtram cadastro; em #3–#7 filtram atividade da sessão (`qps.updated_at`).

## Detalhes (#6 e #7)

- **#6:** alternativa incorreta mais escolhida, por pergunta (letra da opção + texto).
- **#7:** sessões `in_progress` com progresso (respostas / total de perguntas da bateria).

## Rotas (KPIs e relatório)

| Destino | URL |
|---------|-----|
| Painel | `/dashboard` |
| **Relatório** (visão completa + imprimir) | `/dashboard/relatorio` |
| **Ranking / Pódio** (nomes, geral / área / grupo) | `/dashboard/ranking` |
| Lista — total de participantes | `/dashboard/participantes` |
| Lista — sem login | `/dashboard/sem-login` |
| Lista — não finalizaram | `/dashboard/nao-finalizaram` |
| **Progresso por participante** (final do painel) | `/dashboard` (secção + tabela) |
| **Exportar Excel** (progresso por participante) | `/dashboard/participantes/exportar-excel` |
| **Exportar Excel** (baterias) | `/dashboard/baterias/exportar-excel` |

Os totais no painel são links (quando &gt; 0) e mantêm os filtros na query string. Os botões **Baixar Excel** (participantes e baterias) repetem os mesmos filtros da query string.

### Baterias de perguntas (#8)

- Secção após os três KPIs, antes do ranking por time (`dashboard/_batteries-progress`).
- Uma card por bateria (layout grande, 2 colunas em `lg`): progresso de **início** (% / barra / iniciaram / pendentes / elegíveis), status, total de perguntas, área e grupo.
- Lista limitada às baterias da empresa; filtros de **área** e **grupo** do painel restringem quais baterias entram.
- Link para o detalhe da bateria se o utilizador tiver permissão `menu.training.question_batteries`.

**Export Excel baterias** (`GET /dashboard/baterias/exportar-excel`):

| Coluna | Origem |
|--------|--------|
| Total de perguntas | `withCount('batteryQuestions')` |
| Participantes elegíveis / Iniciaram / Pendentes / Progresso (%) | `QuestionBatteryRepository::progressStatsByBatteryIds` |
| Participantes com respostas | Distintos com resposta (`answerStatsByBatteryIds`) |
| Total acertos / Total erros | Soma de `questionnaire_participant_answers` (filtros de período e status do painel aplicados) |

Camada: `TrainingDashboardService::batteriesForExport` → `TrainingDashboardExportService::downloadBatteriesProgressXlsxResponse`.

### KPIs com breakdown (total, sem login, não finalizou)

Os três cards do topo usam o mesmo layout (`_kpi-metric-card`): faixa de cor, total clicável e duas colunas **Por área** / **Por grupo**.

| Card | Breakdown |
|------|-----------|
| Total de participantes | Todos os trainees (filtros de cadastro/situação) |
| Sem login registrado | Trainees com `last_login_at` nulo |
| Começou e não finalizou | Utilizadores com sessão `in_progress` (filtros de atividade da sessão) |

Regras comuns:

- Contagens respeitam situação e período do filtro do painel; o filtro de **área** não limita o breakdown por área; o de **grupo** não limita o breakdown por grupo.
- Cada linha é clicável e aplica o filtro no painel.
- **Sem área** / **Sem grupo** quando o vínculo não existe.

### Progresso por participante (#8)

- Lista participantes (trainee) **paginada** na própria secção do painel, incluindo quem **não iniciou** o questionário.
- Para quem já jogou: mostra a sessão **mais recente** no escopo dos filtros (área, grupo, período de atividade).
- Colunas: participante (com **foto de perfil** quando existir em `users.avatar_path`), área, grupo, bateria, status (Não iniciou / Em andamento / Concluído), respostas, acertos, pontos, tempo, última atividade.
- **Busca** na secção: query `search` (nome ou e-mail).
- **Ordenação**: cabeçalhos clicáveis; query `sort` + `direction` (`asc`/`desc`). Valores em `TrainingDashboardParticipantProgressSort`: `name`, `email`, `sector`, `team`, `battery`, `status`, `points`, `last_activity` (padrão: `name`).
- **Paginação**: `per_page` (10, 20, 50, 100; padrão 20), `page`.
- Contador «Exibindo X–Y de Z participante(s)» e links de página Laravel abaixo da tabela.
- Os filtros do topo do dashboard (área, grupo, período, etc.) continuam aplicados; a busca/ordenação da tabela preserva esses parâmetros via query string.
- **Exportação XLSX** (`/dashboard/participantes/exportar-excel`): exporta **todos** os participantes que passam nos filtros do dashboard — **sem** limite da paginação da tabela (`participantsWithProgress`, não o paginador da UI).
- Camada: `TrainingDashboardService::paginateParticipantsWithProgress` → `TrainingDashboardRepository::paginateParticipantsWithProgress`.
- Validação extra em `DashboardRequest`: `search`, `sort`, `direction`, `per_page`, `page`.

### Ranking / Pódio (gestão)

- Rota: `GET /dashboard/ranking` (`dashboard.ranking`). Botão no cabeçalho do painel e do relatório.
- Abas: **Geral** | **Por área** | **Por grupo** (`ranking_scope` na query; enum `TrainingDashboardRankingScope`).
- Regra: melhor sessão por participante (maior `total_points`; desempate: menor tempo, depois `MIN(session id)`). Só quem tem pelo menos uma resposta.
- Filtros do painel (área, grupo, período de atividade, situação) aplicam-se à consulta; com área ou grupo selecionado na aba correspondente, exibe um único pódio; sem seleção, mostra um bloco por área/grupo (top 10 cada).
- UI: componente `x-portal.battery-ranking` (`variant="management"`), CSS `resources/css/dashboard-ranking.css` (importa `battery-ranking.css`; entrada no `vite.config.js`).
- Camada: `TrainingDashboardRepository::participantRanking` → `TrainingDashboardService::buildRankingPresentation`.
- **MySQL:** query sem `ROW_NUMBER()` (compatível com MySQL 5.7 / MariaDB antigos em produção).

**Pós-deploy (front):**

```bash
npm run build
```

Documentação do lote: [lote-usuarios-import-fila-dashboard-2026-05-20.md](./lote-usuarios-import-fila-dashboard-2026-05-20.md).

```bash
php artisan test --filter=TrainingDashboard
```
