# Lote 2026-05-25/26 — Áreas, baterias, exclusão em lote e performance (BI / alto volume)

Documentação do conjunto de entregas preparado para **subir em `main`**: funcionalidades de treinamento (áreas e baterias) e otimizações pensadas para **crescimento de dados** (respostas, sessões, utilizadores).

**Commits de referência (já em `main` local):**

| Commit | Resumo |
|--------|--------|
| `02f112a` | Exclusão com confirmação (áreas/baterias), exclusão em lote de áreas, exclusão em lote de utilizadores multi-área |
| `4c24a53` | Métricas na listagem de áreas; total de perguntas na listagem e detalhe da bateria |

**Pendente de commit (staged):** migration de índices + refatoração de queries no BI e no detalhe da bateria.

**Índice:** [registro-entregas.md](./registro-entregas.md) · [plataforma-treinamento.md](./plataforma-treinamento.md)

---

## 1. Visão do produto

### 1.1 Áreas (setores) — Treinamento (`/training/areas`)

| Funcionalidade | Descrição |
|----------------|-----------|
| Exclusão unitária | Página de confirmação (`confirm-destroy`) com preview de grupos, utilizadores afetados e bloqueios (equipamentos / transporte). |
| Exclusão em lote | Checkboxes na listagem → `GET training/areas/bulk/confirm-destroy` → `DELETE training/areas/bulk`. |
| Métricas na listagem | Colunas **Utilizadores** (ativos em `users.sector_id` + `user_sectors`), **Perguntas** (soma nas baterias da área) e **Progresso** (participantes elegíveis / que iniciaram baterias da área, mesmo indicador visual das baterias). |

**Rotas:**

| Método | Rota | Nome |
|--------|------|------|
| GET | `/training/areas/bulk/confirm-destroy` | `training.areas.bulk.confirm-destroy` |
| DELETE | `/training/areas/bulk` | `training.areas.bulk.destroy` |
| GET | `/training/areas/{sector}/confirm-destroy` | `training.areas.confirm-destroy` |

**Fluxo:** `TrainingAreaController` → `SectorService` → `SectorRepository` / `QuestionBatteryRepository` (progresso por área).

**Views partilhadas:** `resources/views/sectors/confirm-destroy.blade.php`, `confirm-bulk-destroy.blade.php`.

### 1.2 Inventário → Setores

Mesma tabela `sectors` e fluxo de **confirmação** de exclusão (`inventory.sectors.confirm-destroy`), com mensagens adaptadas a inventário.

### 1.3 Baterias de perguntas

| Funcionalidade | Descrição |
|----------------|-----------|
| Exclusão com confirmação | `training/question-batteries/{id}/confirmar-exclusao` — resume perguntas, alternativas e sessões. |
| Total de perguntas | Coluna **Perguntas** na listagem (`withCount` + `questions_count` no service). |
| Destaque no detalhe | Bloco no topo da página show com o total de perguntas da bateria. |
| Progresso na listagem | Já documentado em [lote-baterias-import-progresso-2026-05-22.md](./lote-baterias-import-progresso-2026-05-22.md). |

### 1.4 Utilizadores — exclusão em lote (reforço)

Seleção de **várias áreas** de uma vez (checkboxes + “Selecionar todas”), alinhado ao escopo N-N de `user_sectors`. Ver [lote-usuarios-import-fila-dashboard-2026-05-20.md](./lote-usuarios-import-fila-dashboard-2026-05-20.md).

### 1.5 BI — Baterias no painel e export Excel (`/dashboard`)

| Funcionalidade | Descrição |
|----------------|-----------|
| Bloco **Baterias de perguntas** | Cards grandes (2 colunas no desktop) abaixo dos KPIs: título, status, total de perguntas, área/grupo, barra de progresso (% iniciaram), iniciaram / pendentes / elegíveis. |
| Filtros do painel | Área e grupo limitam **quais baterias** aparecem (`QuestionBatteryRepository::listForDashboard`). |
| Progresso por bateria | Mesma regra da listagem de baterias (`progressStatsByBatteryIds` — escopo área+grupo da bateria). |
| **Baixar Excel** | Exporta todas as baterias do bloco com os filtros do dashboard. |

**Rota de export:**

| Método | URL | Nome |
|--------|-----|------|
| GET | `/dashboard/baterias/exportar-excel` | `dashboard.batteries.export` |

**Colunas do XLSX:** ID, título, status, área, grupo, total de perguntas, participantes elegíveis, iniciaram, pendentes, progresso (%), participantes com respostas, total de acertos, total de erros.

**Fluxo:** `DashboardController::exportBatteriesProgress` → `TrainingDashboardService::batteriesForExport` → `QuestionBatteryRepository` (progresso) + `TrainingDashboardRepository::answerStatsByBatteryIds` (acertos/erros; respeita período e status do participante nos filtros do BI).

**UI:** `resources/views/dashboard/_batteries-progress.blade.php`, componente `x-ui.battery-progress-block` (`size="lg"`).

Detalhe do BI: [bi-dashboard.md](./bi-dashboard.md).

---

## 2. Tratativa de longo prazo (código e dados)

### 2.1 Camada de domínio e repositórios

- **Controller fino** → **Service** (regra de negócio, transações) → **Repository** (SQL agregado).
- **DTOs** de preview: `SectorDeletePreview`, `SectorBulkDeletePreview`, `SectorDeletePreviewGroup` — a UI não calcula impacto de exclusão.
- **Form requests** dedicados: `ConfirmDestroySectorsBulkRequest`, `DestroySectorsBulkRequest`, `DestroySectorRequest`.
- **Estatísticas em lote**, não por linha na listagem:
  - `SectorRepository::activeUserCountsBySectorIds`, `questionCountsBySectorIds`
  - `QuestionBatteryRepository::progressStatsBySectorIds`, `progressStatsByBatteryIds`
  - `SectorService::listForTrainingAreasIndex()` enriquece a coleção numa passagem.
- **Resolução lazy** de `QuestionBatteryRepository` dentro de `SectorService::listForTrainingAreasIndex` (via `app()`) para **evitar dependência circular** com `QuestionBatteryScopeService` → `SectorService`.

### 2.2 Escopo N-N nas consultas de BI

`TrainingDashboardRepository::applyQuestionnaireActivityFilters` passa a filtrar baterias por área/grupo considerando:

- coluna legada (`question_batteries.sector_id` / `training_group_id`);
- pivôs `question_battery_sectors` e `question_battery_training_groups`.

Assim o dashboard continua correto quando a bateria tem várias áreas/grupos (regra de negócio de 2026-05-22).

### 2.3 Padrão de agregação (evitar subconsultas correlacionadas)

Antes, várias listagens do BI e o resumo de participantes na bateria usavam subqueries do tipo:

```sql
(SELECT COUNT(*) FROM questionnaire_participant_answers WHERE session_id = qps.id)
```

Com **centenas de milhares / milhões** de linhas em `questionnaire_participant_answers`, isso executa **uma subquery por linha** do resultado — degradação típica O(n×m).

**Abordagem adoptada (staged):**

1. **Subquery agregada** (`GROUP BY session_id` ou `battery_id`) com `JOIN` / `leftJoinSub`.
2. Filtros de empresa, bateria, status ou `user_id IN (...)` **dentro** da agregação, para limitar o volume agregado.
3. **Índices** alinhados aos `JOIN` e `WHERE` (ver secção 3).

Ficheiros afectados:

- `TrainingDashboardRepository::inProgressSessions`
- `TrainingDashboardRepository::latestSessionsProgressByUserIds`
- `QuestionnaireParticipantSessionRepository::listParticipantSummariesForBattery`

---

## 3. Performance de base de dados (migration `2026_05_26_100000_add_performance_indexes`)

**Objectivo:** suportar analytics e listagens com volume alto (ex.: ~1M respostas) sem full table scan nos caminhos críticos.

| Tabela | Índice | Uso principal |
|--------|--------|----------------|
| `questionnaire_participant_answers` | `qpa_battery_question_idx` (`battery_question_id`) | JOINs e agregações por pergunta |
| `questionnaire_participant_answers` | `qpa_question_correct_idx` (`battery_question_id`, `is_correct`) | Contagem de acertos por pergunta / sessão |
| `questionnaire_participant_sessions` | `qps_company_battery_status_idx` (`company_id`, `question_battery_id`, `status`) | Filtro in_progress por empresa e bateria (coexiste com `qps_company_battery_idx`, exigido por FK) |
| `questionnaire_participant_sessions` | `qps_updated_at_idx` (`updated_at`) | Ordenação e filtros por última actividade |
| `trainee_profiles` | `tp_training_group_idx` (`training_group_id`) | `whereHas` / JOIN por grupo no dashboard |
| `users` | `users_company_active_idx` (`company_id`, `is_active`) | Participantes activos por empresa |
| `users` | `users_last_login_idx` (`last_login_at`) | Métricas “sem login” |

**Nota de deploy:** **não remove** `qps_company_battery_idx` (InnoDB usa esse índice na FK de `company_id`). Apenas **adiciona** `qps_company_battery_status_idx` e `qps_updated_at_idx`. Em tabelas grandes, preferir janela de manutenção.

**Reexecução segura:** a migration verifica `information_schema` antes de criar/remover cada índice (idempotente). Se uma execução anterior falhou a meio (ex.: `Duplicate key name qpa_battery_question_idx`), **não** usar `migrate:fresh` — fazer deploy da versão corrigida e rodar `php artisan migrate` de novo.

---

## 4. Comandos pós-deploy

```bash
php artisan migrate
php artisan test --filter='SectorDeleteTest|QuestionBatteryImportTest|UserBulkDeleteTest|TrainingDashboardTest'
```

Se usar filas para importação de utilizadores, manter `php artisan queue:work` (inalterado neste lote).

---

## 5. Testes automatizados

| Ficheiro | Cobertura |
|----------|-----------|
| `tests/Feature/Training/SectorDeleteTest.php` | Confirmação, exclusão com grupos, bulk confirm/destroy, métricas no index de áreas |
| `tests/Feature/Training/QuestionBatteryImportTest.php` | Progresso e total de perguntas (index + show) |
| `tests/Feature/Users/UserBulkDeleteTest.php` | Exclusão em lote multi-área |
| `tests/Feature/Dashboard/TrainingDashboardTest.php` | Bloco baterias no BI + export Excel (`test_dashboard_shows_batteries_progress_block`, `test_dashboard_exports_batteries_progress_excel`) |

---

## 6. Inventário principal de ficheiros

### Novos

```
app/Data/SectorBulkDeletePreview.php
app/Data/SectorDeletePreview.php
app/Data/SectorDeletePreviewGroup.php
app/Http/Requests/Inventory/ConfirmDestroySectorsBulkRequest.php
app/Http/Requests/Inventory/DestroySectorsBulkRequest.php
database/migrations/2026_05_26_100000_add_performance_indexes.php
resources/views/sectors/confirm-destroy.blade.php
resources/views/sectors/confirm-bulk-destroy.blade.php
resources/views/components/partials/sector-delete-action.blade.php
resources/views/training/question-batteries/confirm-destroy.blade.php
resources/views/training/question-batteries/_index-actions.blade.php
resources/views/dashboard/_batteries-progress.blade.php
resources/views/components/ui/battery-progress-block.blade.php
docs/lote-treinamento-areas-performance-2026-05-26.md
```

### Alterados (resumo)

```
app/Services/SectorService.php
app/Services/QuestionBatteryService.php
app/Services/TrainingDashboardService.php
app/Services/TrainingDashboardExportService.php
app/Repositories/Eloquent/SectorRepository.php
app/Repositories/Eloquent/QuestionBatteryRepository.php
app/Repositories/Eloquent/TrainingDashboardRepository.php
app/Repositories/Eloquent/QuestionnaireParticipantSessionRepository.php
app/Http/Controllers/Training/TrainingAreaController.php
app/Http/Controllers/Inventory/SectorController.php
app/Http/Controllers/Training/QuestionBatteryController.php
app/Http/Controllers/DashboardController.php
resources/views/dashboard.blade.php
resources/views/training/areas/index.blade.php
resources/views/training/question-batteries/index.blade.php
resources/views/training/question-batteries/show.blade.php
resources/views/components/list/datatable.blade.php
routes/web.php
```

---

## 7. O que monitorizar após subir

- Tempo de resposta de `/dashboard` (tabelas de progresso e “em andamento”) com filtros por área/grupo.
- Tempo de `/training/areas` com muitas áreas e baterias vinculadas.
- Detalhe da bateria — lista de participantes com contagem de respostas (`listParticipantSummariesForBattery`).
- Plano de disco: novos índices aumentam ligeiramente o espaço em `questionnaire_participant_answers` e `questionnaire_participant_sessions`.

Para evolução futura (fora deste lote): cache de métricas por área com TTL curto, ou materialized view / tabela de resumo diário se o volume ultrapassar confortavelmente o milhão de respostas por empresa.
