# Correção — ranking vazio no portal (escopo N-N)

**Data:** 2026-05-27  
**Sintoma:** `/portal` e `/portal/questionario/experiencia/resultado` mostravam “Ainda não há participantes com respostas nesta bateria” mesmo com utilizadores que já tinham respondido.

---

## Causa

A query `QuestionnaireParticipantSessionRepository::topActiveRankingForBattery` aplicava filtros legados incompatíveis com o escopo **many-to-many** introduzido na migration `2026_05_22_160000_add_training_scope_many_to_many.php`:

| Filtro antigo | Problema |
|---------------|----------|
| `users.sector_id` = setor da bateria | Participantes só em `user_sectors` (sem `sector_id` na linha do user) eram excluídos |
| JOIN obrigatório em `trainee_profiles` com `training_group_id` da bateria | Participantes só em `user_training_groups` eram excluídos |
| Early return em `PortalBatteryRankingService` se `battery.sector_id` ou `battery.training_group_id` fossem `null` | Baterias com escopo **só** em `question_battery_sectors` / `question_battery_training_groups` devolviam lista vazia |

A resolução da bateria para o participante (`PortalQuestionnaireService::resolveBatteryForUser`) já suportava N-N; o ranking não.

---

## Correção (atualizada)

### Regra de negócio

O pódio do participante é **por grupo de treinamento**, **não por área/setor**:

- Viewer vê colegas do **mesmo `training_group_id`** que respondeu a mesma bateria.
- Participantes de **outro grupo** na mesma bateria **não** aparecem.
- Participantes de **outra área** no **mesmo grupo** **aparecem**.

### `PortalQuestionnaireService`

- `resolveParticipantBatteryContext()` — retorna bateria + `training_group_id` usados na home.
- `resolveTrainingGroupIdForBattery()` — resolve o grupo do viewer no ecrã de resultado.

### `PortalBatteryRankingService::topTenForBattery`

- Recebe o `training_group_id` do viewer e passa ao repositório.
- Sem grupo resolvido → lista vazia (não erro).

### `QuestionnaireParticipantSessionRepository::topActiveRankingForBattery`

Assinatura:

```php
topActiveRankingForBattery(int $companyId, int $questionBatteryId, int $trainingGroupId, int $limit = 10)
```

Critérios:

1. Mesma empresa e bateria
2. Utilizador ativo
3. **Mesmo grupo** (`user_training_groups` ou `trainee_profiles`) — **sem** filtro de área
4. Pelo menos uma resposta na sessão
5. Ordenação: pontos ↓, tempo ↑, nome ↑

### Separação de responsabilidades

| Camada | Papel |
|--------|--------|
| `resolveParticipantBatteryContext` | Qual bateria o participante **pode ver/responder** e **qual grupo** define o pódio |
| `topActiveRankingForBattery` | Quem **já pontuou** nessa bateria **no mesmo grupo** |
| Policy `respondAsParticipant` | Autorização para aceder ao questionário/ranking da bateria |

O ranking do **BI/gestão** (`/dashboard/ranking`) usa outro serviço (`TrainingDashboardService`) com filtros de área/grupo — não foi alterado nesta correção.

---

## Ficheiros alterados

| Ficheiro | Alteração |
|----------|-----------|
| `app/Services/PortalQuestionnaireService.php` | `resolveParticipantBatteryContext`, `resolveTrainingGroupIdForBattery` |
| `app/Services/PortalBatteryRankingService.php` | Ranking por grupo do viewer |
| `app/Repositories/Contracts/QuestionnaireParticipantSessionRepositoryInterface.php` | Assinatura e doc do método de ranking |
| `app/Repositories/Eloquent/QuestionnaireParticipantSessionRepository.php` | Query por bateria + empresa; PHPDoc |
| `tests/Feature/Portal/PortalHomeRankingTest.php` | Cenários N-N (participante e bateria) |

---

## Testes

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

Casos relevantes:

- `test_home_shows_top_ranking_for_active_battery` — escopo legado
- `test_home_shows_ranking_for_participants_with_many_to_many_scope_only`
- `test_home_shows_ranking_when_battery_uses_only_many_to_many_scope`
- `test_home_ranking_is_scoped_by_training_group_not_sector`

---

## Verificação manual

1. Entrar no portal com utilizador que já completou a bateria.
2. Abrir `/portal` — pódio e lista com nomes/pontos.
3. Abrir `/portal/questionario/experiencia/resultado` após concluir — mesma classificação.
4. Confirmar que utilizador sem bateria publicada no seu escopo **não** vê o bloco de ranking na home.

---

## Documentação relacionada

- [deploy-producao-questionario-experiencia.md](./deploy-producao-questionario-experiencia.md) — § Portal home e § Query de ranking
- [portal-questionario-ui-game.md](./portal-questionario-ui-game.md) — §12 Ranking partilhado
