# Deploy em produção — Questionário (experiência game)

Documento de referência para subir em **produção** o lote que inclui: experiência de jogo no portal, **uma pergunta por vez**, cronómetro por pergunta, pausa, persistência de respostas, branding global (nome do projeto + favicon), normalização de logos, **modal animado ao acertar** e **ranking top 10 na home do portal**.

**Público:** equipa de deploy, QA e suporte.  
**Última atualização:** 2026-05-27 (regra de query do ranking alinhada ao escopo N-N; ver [correcao-ranking-portal-escopo-2026-05-27.md](./correcao-ranking-portal-escopo-2026-05-27.md)).

Documentação do lote UI complementar: [lote-ui-login-jogar-resultado-2026-05-19.md](./lote-ui-login-jogar-resultado-2026-05-19.md).

---

## Índice

1. [Resumo do que mudou (produto)](#1-resumo-do-que-mudou-produto)
2. [Checklist de deploy](#2-checklist-de-deploy)
3. [Migrations (obrigatório)](#3-migrations-obrigatório)
4. [Modelo de dados](#4-modelo-de-dados)
5. [Fluxo do participante](#5-fluxo-do-participante)
6. [Rotas e API](#6-rotas-e-api)
7. [Regras de negócio](#7-regras-de-negócio)
8. [Branding e aparência](#8-branding-e-aparência)
9. [Logos (fundo transparente)](#9-logos-fundo-transparente)
10. [Arquitetura técnica](#10-arquitetura-técnica)
11. [Inventário de arquivos](#11-inventário-de-arquivos)
12. [Testes automatizados](#12-testes-automatizados)
13. [Verificação manual pós-deploy](#13-verificação-manual-pós-deploy)
14. [Rollback e riscos](#14-rollback-e-riscos)
15. [Limitações conhecidas](#15-limitações-conhecidas)
16. [Ranking na home do portal](#16-ranking-na-home-do-portal)
17. [O que ainda não está no produto / backlog](#17-o-que-ainda-não-está-no-produto--backlog)

---

## 1. Resumo do que mudou (produto)

### Portal — experiência “game” do questionário

| Antes (comportamento antigo) | Agora |
|------------------------------|--------|
| Várias perguntas na mesma página | **Uma pergunta por ecrã** |
| Progresso pouco ou não persistido ao voltar | **Sessão guardada**; retoma na pergunta seguinte à última respondida |
| Cronómetro global ou inexistente | **Cronómetro por pergunta** (reinicia em cada nova pergunta) |
| — | **Pausar**: embaça/oculta a pergunta e **para o tempo** (só no browser) |
| — | **Sair**: guarda sessão `in_progress` sem gravar resposta da pergunta atual |
| Envio em lote | Cada **“Confirmar resposta”** grava **uma linha** na base |
| — | Ao **acertar**: partículas + **modal “Parabéns!”** com pontos; botão **Avançar** (ou **Ver resultado** na última) |
| — | Ao **errar**: **modal “Não foi desta vez”**; botão **Continuar** (ou **Ver resultado** na última) |
| — | **Barra de progresso** + “Pergunta N de M”; ícone de relógio no HUD |
| — | Botão **Confirmar resposta** no estilo 3D (`qg-btn-iniciar`) |

### Login (`/login`)

| Funcionalidade | Descrição |
|----------------|-----------|
| Logo / favicon | Do cadastro em Configurações → Aparência (e logo do questionário game se definida) |
| Fundo | Gradiente **game** (cores do questionário) quando há um único tenant ou slug na URL |
| Botão Entrar | Cor primária do portal |

Serviço: `PortalAppearanceSettingService::themeForGuestLogin()`.

### Gestão — configurações

- Campo **Nome do projeto** em Configurações → Aparência do portal (título do browser/abas em gestão e portal).
- **Favicon** e **logo** aplicados de forma consistente nas layouts (incluindo experiência game).

### Dados gravados por pergunta

Cada resposta confirmada regista:

- alternativa escolhida (`selected_position`);
- se acertou (`is_correct`);
- tempo em segundos naquela pergunta (`elapsed_seconds`);
- pontos ganhos na pergunta (`points_earned` = peso da pergunta se correto, senão `0`).

### Portal — home (`/portal`) — ranking da bateria ativa

| Funcionalidade | Descrição |
|----------------|-----------|
| Bateria de referência | A mesma bateria **publicada** do setor/grupo do participante (`PortalQuestionnaireService::resolveBatteryForUser`) |
| Quem entra no ranking | Utilizadores **ativos** da mesma **empresa**, no **mesmo grupo de treinamento** do participante logado (`user_training_groups` ou `trainee_profiles`), com sessão nesta **bateria** e **≥ 1 resposta** em `questionnaire_participant_answers`. **Não** filtra por área/setor. |
| Ordenação | `total_points` DESC → `elapsed_seconds` ASC → nome |
| Limite | **Top 10** |
| UI | **Pódio** (🥇🥈🥉) no topo + **lista completa** dos 10 abaixo (“Classificação completa”) |
| Destaque | Linha do utilizador logado com badge **“Você”** |
| Sem bateria | Bloco de ranking **não aparece** (ex.: perfil sem setor/grupo ou sem bateria publicada) |

**Nota de UI:** o card de boas-vindas (“Bem-vindo ao portal…”) está **comentado** em `portal/home.blade.php`; a home mostra sobretudo o ranking quando há bateria.

### Ecrã de resultado (`/portal/questionario/experiencia/resultado`)

| Antes | Agora |
|-------|--------|
| Lista de **todas** as perguntas com acerto/erro | **Resumo**: acertos X/Y, pontos, tempo total |
| — | **Pódio + ranking top 10** da mesma bateria (componente `battery-ranking`, variant `game`) |
| — | Botão **Voltar à listagem** (3D) |

Backend: `PortalBatteryRankingService::topTenForBattery($user, $battery)`.

**Motivo:** baterias com 30+ perguntas tornam a lista ilegível.

### Ajustes de UI (questionário)

- **Jogar:** barra de progresso, relógio com ícone, confirmar 3D, modais acerto/erro.
- **Resultado:** hero de resumo + ranking; CSS em `battery-ranking.css` (importado por `questionnaire-game.css`).
- **Acerto / erro:** modais; envio via **AJAX** (`Accept: application/json`); avanço só no botão.

---

## 2. Checklist de deploy

Executar na ordem (ajustar paths ao servidor):

```bash
# 1. Código
git pull origin <branch-de-release>

# 2. Dependências PHP
composer install --no-dev --optimize-autoloader

# 3. Migrations (ver secção 3)
php artisan migrate --force

# 4. Front-end (CSS/JS do questionário game)
npm ci
npm run build

# 5. Cache Laravel (produção)
php artisan config:cache
php artisan route:cache
php artisan view:cache

# 6. Storage (se ainda não existir link público)
php artisan storage:link

# 7. Permissões (exemplo — adaptar user/group do servidor)
# chown -R www-data:www-data storage bootstrap/cache
# chmod -R ug+rwx storage bootstrap/cache
```

**Variáveis de ambiente:** não há novas variáveis obrigatórias só para este lote. Manter `APP_URL`, base de dados e disco `public` (ou S3) como já configurado.

**Fila / workers:** este lote **não** adiciona jobs obrigatórios.

---

## 3. Migrations (obrigatório)

Três migrations na pasta `database/migrations/` (prefixo `2026_05_22_*`):

| Arquivo | O que faz |
|---------|-----------|
| `2026_05_22_100000_create_questionnaire_participant_sessions_table.php` | Tabela `questionnaire_participant_sessions` |
| `2026_05_22_120000_add_project_name_to_portal_appearance_settings_table.php` | Coluna `project_name` em `portal_appearance_settings` |
| `2026_05_22_140000_create_questionnaire_participant_answers_table.php` | Tabela `questionnaire_participant_answers` + coluna `total_points` em sessions |

**Nota MySQL:** a migration de respostas usa nomes curtos de foreign key (`qpa_session_fk`, `qpa_question_fk`) por limite de 64 caracteres do identificador.

**Se uma migration falhar a meio** (tabela criada sem FK): remover tabela órfã `questionnaire_participant_answers` e voltar a correr `php artisan migrate --force`.

---

## 4. Modelo de dados

### `questionnaire_participant_sessions`

Uma sessão por **utilizador + bateria** (unique `user_id` + `question_battery_id`).

| Coluna | Descrição |
|--------|-----------|
| `company_id` | Tenant (isolamento multi-empresa) |
| `user_id` | Participante |
| `question_battery_id` | Bateria em curso |
| `status` | `in_progress` ou `completed` |
| `answers` | JSON legado/mapa `{ question_id => posição }` (atualizado ao responder) |
| `elapsed_seconds` | Soma dos tempos de todas as respostas gravadas |
| `total_points` | Soma de `points_earned` das respostas |
| `result` | JSON com resumo ao concluir (`buildResultPayload`) |
| `completed_at` | Timestamp de conclusão |

### `questionnaire_participant_answers`

Uma linha por pergunta respondida (unique sessão + pergunta).

| Coluna | Descrição |
|--------|-----------|
| `questionnaire_participant_session_id` | FK sessão |
| `battery_question_id` | FK pergunta |
| `selected_position` | 1–4 |
| `is_correct` | boolean |
| `points_earned` | decimal(8,2) |
| `elapsed_seconds` | tempo **desta** pergunta |
| `answered_at` | momento da resposta |

### `portal_appearance_settings.project_name`

- Até 120 caracteres, opcional.
- Usado no `<title>` e branding via `PortalAppearanceTheme::displayName()`.

---

## 5. Fluxo do participante

```
/portal/questionario
    → lista / boas-vindas
/portal/questionario/experiencia          (splash — INICIAR)
/portal/questionario/experiencia/jogar    (1 pergunta, timer, pausar, confirmar)
    → POST /pergunta (cada resposta)
/portal/questionario/experiencia/resultado (após última pergunta ou sessão já completed)
```

**Retomar:** se existir sessão `in_progress`, o splash/play redireciona para continuar na **próxima pergunta sem resposta** (ordem `sort_order`).

**Sair:** `POST .../sair` mantém sessão; **não** cria resposta para a pergunta em aberto.

**Pausar:** apenas UI (overlay + blur); **não** persiste no servidor.

**Acerto:** pedido AJAX com `Accept: application/json` → modal com pontos → utilizador clica **Avançar** → redirect para jogar ou resultado.

**Erro:** alternativa marcada a vermelho → **modal de erro** → utilizador clica **Continuar** → redirect para jogar ou resultado.

---

## 6. Rotas e API

Prefixo portal: middleware `auth`, `portal.context`, `verified`.  
Nomes de rota Laravel: `portal.questionnaire.*`.

| Método | URL | Nome | Ação |
|--------|-----|------|------|
| GET | `/portal/questionario/experiencia` | `experience` | Splash |
| GET | `/portal/questionario/experiencia/jogar` | `experience.play` | Jogar (1 pergunta) |
| GET | `/portal/questionario/experiencia/resultado` | `experience.result` | Resultado |
| POST | `/portal/questionario/experiencia/pergunta` | `experience.answer` | Registar resposta |
| POST | `/portal/questionario/experiencia/sair` | `experience.exit` | Sair (guardar sessão) |

### POST `.../pergunta` — corpo (form ou JSON)

| Campo | Tipo | Regras |
|-------|------|--------|
| `question_battery_id` | int | Bateria da empresa do user |
| `battery_question_id` | int | Pergunta da bateria |
| `selected_position` | int | 1–4 |
| `elapsed_seconds` | int | 0–86400 |

**Resposta JSON** (quando `Accept: application/json` ou pedido AJAX):

```json
{
  "is_correct": true,
  "points_earned": 2.5,
  "completed": false,
  "redirect_url": "https://<host>/portal/questionario/experiencia/jogar"
}
```

**Resposta HTML:** redirect 302 para `redirect_url` equivalente (comportamento fallback sem JS).

**Rota removida / não usar:** `POST .../experiencia/progresso` (fluxo antigo em lote) — já não existe.

---

## 7. Regras de negócio

- **Pontuação:** classe `App\Support\QuestionnaireAnswerGrade` — se a posição escolhida coincide com a alternativa `is_correct`, `points_earned = weight` da pergunta; senão `0`.
- **Duplicar resposta** na mesma pergunta: exceção `InvalidArgumentException` (não deve ocorrer na UI normal).
- **Conclusão:** quando o número de respostas = total de perguntas da bateria → `status = completed`, `result` preenchido, redirect para resultado.
- **Autorização:** `StorePortalQuestionnaireAnswerRequest` + policy `respondAsParticipant` na bateria.
- **Escopo:** sempre filtrado por `company_id` do utilizador autenticado.

---

## 8. Branding e aparência

### Middleware `SharePortalBranding`

Registado em `bootstrap/app.php` no grupo `web`.  
Partilha em **todas** as views: variável `$portalTheme` (`PortalAppearanceTheme`).

Resolve `company_id` por:

1. Utilizador autenticado (`user->company_id`), ou  
2. Slug do portal público na rota / query `?portal=`.

### Onde o nome e favicon aparecem

- Layouts: `admin`, `portal`, `guest`, `questionnaire-game`, `game/app`
- Sidebars gestão/portal
- `<title>`: `displayName()` → `project_name` se definido, senão `config('app.name')`

### Configuração (gestão)

**Configurações → Aparência do portal**

- **Nome do projeto** (`project_name`) — abas e título
- Favicon, logo, fundo (como antes)

**Configurações → Questionário (game)**

- Logo e imagem de fundo da experiência splash/jogo

---

## 9. Logos, favicon e upload de imagens (GD)

Processamento automático para PNG/JPEG com fundo claro nas bordas:

| Classe | Função |
|--------|--------|
| `GdImageFactory` | Carrega raster via GD; suprime avisos benignos libpng (**iCCP**) |
| `PortalFaviconNormalizer` | Favicon → PNG 32×32 (usa `GdImageFactory`) |
| `RasterLogoBackgroundNormalizer` | Flood-fill a partir das bordas; gera PNG com transparência |
| `StoredRasterLogoProcessor` | No upload e ao servir logo antiga (normalização lazy) |

Integrado em:

- `QuestionnaireGameSettingService` (logo do game)
- `PortalAppearanceSettingService` (logo, favicon e fundo do portal)

**Requisito servidor:** extensão PHP **GD** com suporte a PNG. Sem GD, o upload mantém o ficheiro original.

### Erro 500 em produção — `iCCP: known incorrect sRGB profile`

| Item | Detalhe |
|------|---------|
| **Sintoma** | 500 ao `PUT /settings/portal-appearance` ao enviar PNG (favicon/logo) |
| **Causa** | Warning do libpng promovido a exceção pelo Laravel em produção |
| **Correção** | `GdImageFactory` envolve `imagecreatefrompng` (e demais formatos) |
| **Doc completa** | [correcao-upload-imagens-gd-iccp.md](./correcao-upload-imagens-gd-iccp.md) |

---

## 10. Arquitetura técnica

Pipeline (padrão do projeto):

```
Controller → Service → Repository → Model
         ↘ FormRequest (validação)
         ↘ Policy (autorização)
```

| Camada | Responsabilidade neste módulo |
|--------|-------------------------------|
| `PortalQuestionnaireController` | Splash, play, submit, exit, result |
| `PortalHomeController` | Home do portal + ranking |
| `PortalBatteryRankingService` | Top 10 da bateria ativa do user |
| `QuestionnaireParticipantSessionService` | Sessão, próxima pergunta, `recordAnswer`, resultado |
| `QuestionnaireParticipantSessionRepository` | Persistência sessão + query `topActiveRankingForBattery` |
| `QuestionnaireParticipantAnswerRepository` | Persistência respostas |
| Views `experience-*.blade.php` | UI game + JS timer/modal |
| `components/portal/battery-ranking.blade.php` | Pódio + lista top 10 (Tailwind via layout portal) |
| `resources/css/questionnaire-game.css` | Tema visual, HUD, modal sucesso, pausa, resultado |

Bindings em `AppServiceProvider`:

- `QuestionnaireParticipantSessionRepositoryInterface`
- `QuestionnaireParticipantAnswerRepositoryInterface`

Policy: `QuestionnaireParticipantSessionPolicy` (registo em `AuthServiceProvider`).

---

## 11. Inventário de arquivos

### Criados

**Base de dados**

- `database/migrations/2026_05_22_100000_create_questionnaire_participant_sessions_table.php`
- `database/migrations/2026_05_22_120000_add_project_name_to_portal_appearance_settings_table.php`
- `database/migrations/2026_05_22_140000_create_questionnaire_participant_answers_table.php`

**Domínio / aplicação**

- `app/Enums/QuestionnaireParticipantSessionStatus.php`
- `app/Models/QuestionnaireParticipantSession.php`
- `app/Models/QuestionnaireParticipantAnswer.php`
- `app/Policies/QuestionnaireParticipantSessionPolicy.php`
- `app/Repositories/Contracts/QuestionnaireParticipantSessionRepositoryInterface.php`
- `app/Repositories/Eloquent/QuestionnaireParticipantSessionRepository.php`
- `app/Repositories/Contracts/QuestionnaireParticipantAnswerRepositoryInterface.php`
- `app/Repositories/Eloquent/QuestionnaireParticipantAnswerRepository.php`
- `app/Services/QuestionnaireParticipantSessionService.php`
- `app/Support/QuestionnaireAnswerGrade.php`
- `app/Support/GdImageFactory.php`
- `app/Support/RasterLogoBackgroundNormalizer.php`
- `app/Support/StoredRasterLogoProcessor.php`
- `app/Http/Middleware/SharePortalBranding.php`
- `app/Http/Requests/Portal/StorePortalQuestionnaireAnswerRequest.php`
- `app/Http/Requests/Portal/ExitPortalQuestionnaireExperienceRequest.php`
- `app/Services/PortalBatteryRankingService.php`

**Views / assets**

- `resources/views/portal/questionnaire/experience-splash.blade.php`
- `resources/views/portal/questionnaire/experience-play.blade.php`
- `resources/views/portal/questionnaire/experience-result.blade.php`
- `resources/views/portal/questionnaire/_experience-success-modal.blade.php`
- `resources/views/portal/questionnaire/_experience-error-modal.blade.php`
- `resources/css/battery-ranking.css`
- `resources/css/questionnaire-game.css` (ou extensão significativa se já existia)
- `resources/views/layouts/questionnaire-game.blade.php` (layout dedicado)
- `resources/views/components/portal/battery-ranking.blade.php`

**Testes**

- `tests/Feature/Portal/QuestionnaireParticipantSessionTest.php`
- `tests/Feature/Portal/PortalHomeRankingTest.php`
- `tests/Unit/Support/PortalAppearanceThemeTest.php` (se incluído no lote)
- `tests/Unit/Support/GdImageFactoryTest.php`

### Alterados (principais)

- `routes/web.php` — rotas experiência + answer + exit
- `bootstrap/app.php` — `SharePortalBranding` no grupo `web`
- `app/Http/Controllers/Portal/PortalQuestionnaireController.php` — fluxo experiência
- `app/Providers/AppServiceProvider.php` — bindings repositories
- `app/Providers/AuthServiceProvider.php` — policy sessão
- `app/Services/PortalAppearanceSettingService.php` — logo + tema
- `app/Services/QuestionnaireGameSettingService.php` — logo game
- `app/Support/PortalAppearanceTheme.php` — `displayName()`, `project_name`
- `resources/views/settings/portal-appearance/_portal-tab.blade.php` — campo nome projeto
- `resources/views/portal/questionnaire/welcome.blade.php` — links continuar/resultado
- `app/Http/Controllers/Portal/PortalHomeController.php` — injeta ranking na home
- `app/Repositories/Eloquent/QuestionnaireParticipantSessionRepository.php` — método ranking
- `app/Repositories/Contracts/QuestionnaireParticipantSessionRepositoryInterface.php`
- `app/Http/Controllers/Portal/PortalQuestionnaireController.php` — resposta JSON em `submitAnswer`
- `resources/views/portal/home.blade.php` — exibe `<x-portal.battery-ranking>`
- `resources/views/portal/questionnaire/experience-play.blade.php` — AJAX + modal sucesso
- `resources/views/portal/questionnaire/experience-result.blade.php` — botão com `.qg-result__actions`
- Layouts `admin`, `portal`, `guest`, sidebars — título/favicon via `$portalTheme`

### Removidos / obsoletos

- `app/Http/Requests/Portal/SavePortalQuestionnaireProgressRequest.php` (fluxo antigo em lote)

---

## 12. Testes automatizados

```bash
php artisan test --filter=QuestionnaireParticipantSessionTest
php artisan test --filter=PortalHomeRankingTest
php artisan test --filter=PortalQuestionnaireTest
php artisan test --filter=PortalAppearanceThemeTest
```

Cenários em `PortalHomeRankingTest`:

- Home mostra ranking com ordem correta de pontos
- Utilizador logado vê “(você)” / badge na sua linha
- Sem bateria ativa → ranking oculto

Cenários cobertos em `QuestionnaireParticipantSessionTest`:

- Play mostra **uma** pergunta e progresso “X de Y”
- POST JSON grava resposta e avança
- Duas respostas completam sessão e redirecionam para resultado
- Sair não grava respostas

---

## 13. Verificação manual pós-deploy

1. **Gestão:** definir Nome do projeto, favicon e logo; guardar.
2. **Portal:** abrir questionário → experiência → INICIAR.
3. **Timer:** confirmar que incrementa na pergunta atual.
4. **Pausar:** pergunta embaçada, tempo parado; Continuar retoma.
5. **Responder errado:** modal “Não foi desta vez”; Continuar.
6. **Responder certo:** partículas + modal “Parabéns!” + pontos; Avançar.
7. **Sair** a meio: voltar depois — deve retomar na pergunta não respondida.
8. **Concluir bateria:** resumo (acertos, pontos, tempo) + pódio/ranking; **sem** lista longa por pergunta.
9. **Login** (`/login`): logo e fundo escuro alinhados ao cadastro.
10. **Barra de progresso** avança a cada pergunta.
11. **Base de dados:** linhas em `questionnaire_participant_answers` com `elapsed_seconds` e `points_earned` coerentes.
12. **Logo splash:** fundo escuro do jogo sem “caixa branca” à volta do logo (se GD ativo).
13. **Home portal:** pódio + lista com até 10 nomes; sua posição destacada.
14. **Ranking vazio:** mensagem incentivando a ser o primeiro a pontuar.
15. **Resultado questionário:** botão “Voltar à listagem” com margem adequada.
16. **Upload aparência:** favicon/logo PNG que antes gerava 500 (perfil iCCP) grava sem erro.

---

## 14. Rollback e riscos

**Rollback de código:** deploy da versão anterior + `php artisan migrate:rollback --step=3` **apenas** se não houver dados de produção que devam ser preservados.

**Atenção:** rollback apaga tabelas/colunas de sessões e respostas — **perda de progresso e pontuações** dos participantes.

**Risco baixo:** rotas novas não alteram módulos de suprimentos/eventos; isolamento por `company_id` mantido.

**Build front-end:** sem `npm run build`, o modal e estilos novos podem não aparecer (CSS em Vite).

---

## 15. Limitações conhecidas

| Tópico | Comportamento atual |
|--------|---------------------|
| Pausa | Só no cliente; recarregar a página **não** restaura estado “pausado” |
| Tempo | Enviado pelo browser (`elapsed_seconds`); confiança no participante (validado 0–86400) |
| Modal de erro | Modal dedicado (vermelho); utilizador confirma **Continuar** antes de avançar |
| Resultado sem detalhe por pergunta | Por desenho — só resumo + ranking; ver detalhe no BI/gestão se necessário |
| Fluxo legado `questionario/responder` | Mantém-se separado da “experiência”; não grava em `questionnaire_participant_answers` |
| GD ausente | Logos não são normalizados automaticamente |
| Ranking | Só participantes que **já responderam** pelo menos 1 pergunta; quem só abriu e saiu não aparece |
| Ranking duplicado visual | Top 3 aparecem no pódio **e** na lista (intencional) |
| CSS portal vs game | Home: Tailwind + `battery-ranking.css`; jogo: `questionnaire-game.css` (+ mesmo CSS de ranking no resultado) |

---

## 16. Ranking na home do portal

### Rota

| Método | URL | Controller |
|--------|-----|------------|
| GET | `/portal` | `PortalHomeController@index` |

### Fluxo

```
User autenticado (portal)
  → PortalBatteryRankingService::topTenForUserActiveBattery()
  → resolve bateria publicada (setor + grupo)
  → QuestionnaireParticipantSessionRepository::topActiveRankingForBattery()
  → view portal.home com rankingPayload
  → Policy respondAsParticipant na bateria (autorização)
```

### Query de ranking (regra)

Método: `QuestionnaireParticipantSessionRepository::topActiveRankingForBattery($companyId, $questionBatteryId, $trainingGroupId, $limit)`.

O `training_group_id` vem de `PortalQuestionnaireService::resolveParticipantBatteryContext` (home) ou `resolveTrainingGroupIdForBattery` (resultado da experiência).

| Critério | Detalhe |
|----------|---------|
| Tenant | `questionnaire_participant_sessions.company_id` e `users.company_id` |
| Bateria | `question_battery_id` = bateria resolvida para o participante |
| Grupo | Mesmo `training_group_id` do viewer (pivô `user_training_groups` **ou** `trainee_profiles.training_group_id`) |
| Área | **Não filtra** — participantes de outras áreas no mesmo grupo entram no pódio |
| Ativo | `users.is_active` = true |
| Resposta | EXISTS em `questionnaire_participant_answers` ligado à sessão |
| Ordenação | `total_points` DESC → `elapsed_seconds` ASC → `users.name` ASC |
| Limite | 10 (default) |

**Importante:** o ranking é **por grupo**, não por área. A elegibilidade para **responder** continua em `PortalQuestionnaireService::resolveBatteryForUser` (combinação área + grupo) e na policy `respondAsParticipant`.

**Histórico:** versões anteriores filtravam por `users.sector_id` ou omitiam grupo — ver [correcao-ranking-portal-escopo-2026-05-27.md](./correcao-ranking-portal-escopo-2026-05-27.md).

### Componente Blade

`resources/views/components/portal/battery-ranking.blade.php`  
Tag: `<x-portal.battery-ranking :battery="..." :entries="..." />`

Estrutura:

1. Cabeçalho escuro (título da bateria + link “Ir ao questionário”)
2. Grelha pódio (2º | 1º | 3º) com emojis 🥇🥈🥉
3. Lista “Classificação completa” com **todos** os entries (até 10)

---

## 17. O que ainda não está no produto / backlog

Itens **não implementados** (não esperar em produção após este deploy):

| Item | Estado |
|------|--------|
| Ranking na gestão / export CSV | Não feito |
| Pausa persistida no servidor | Não feito |
| Modal dedicado ao **errar** pergunta | Não feito (só shake + avanço automático) |
| Ranking em tempo real (WebSocket/polling) | Não feito — recarrega a página |
| Incluir participantes sem nenhuma resposta no ranking | Não feito — só quem já pontuou |
| Painel admin da bateria com leaderboard | Não feito |

### Documentação ainda por alinhar (fora deste ficheiro)

| Documento | Situação |
|-----------|----------|
| [registro-entregas.md](./registro-entregas.md) | Não atualizado com lote ranking + modal (auditoria por data) |
| [plataforma-treinamento.md](./plataforma-treinamento.md) | Inventário geral pode estar desatualizado |
| Testes CI | Confirmar `php artisan test --filter=Portal` verde antes do deploy |

---

## Documentos relacionados

- [correcao-upload-imagens-gd-iccp.md](./correcao-upload-imagens-gd-iccp.md) — erro 500 upload favicon/logo (libpng iCCP)
- [plataforma-treinamento.md](./plataforma-treinamento.md) — visão geral da plataforma
- [registro-entregas.md](./registro-entregas.md) — histórico de entregas e correção SCAN
- Regras de código: `.cursor/rules/sistema-diego.mdc`

---

*Em caso de divergência entre este documento e o código em produção, prevalece o código deployado (`git` + migrations aplicadas).*
