# Lote — Avatar do portal, login (slogan/wires) e dashboard progresso (2026-05-22)

Documento para **commit** da working tree atual. Complementa [lote-ui-login-jogar-resultado-2026-05-19.md](./lote-ui-login-jogar-resultado-2026-05-19.md) e [bi-dashboard.md](./bi-dashboard.md).

**Estado:** pendente commit em `main` (ver `git status`).

---

## 1. Resumo do produto

| Área | O que entrou |
|------|----------------|
| **Foto de perfil (portal)** | Upload com câmera/galeria, **recorte quadrado** (Cropper.js), modal no primeiro acesso, edição em **Meu perfil**, avatar no menu superior |
| **Login guest** | Slogan do splash do jogo abaixo do logo; triângulos wireframe + gradiente; logo animada no mobile |
| **Dashboard BI** | Secção **Progresso por participante** no painel + exportação **Excel** com os mesmos filtros |
| **UI global** | `<x-modal>` centralizado na viewport (beneficia todos os modais) |

---

## 2. Foto de perfil no portal

### Fluxo do utilizador

1. Ao entrar no portal **sem foto** e sem ter clicado em «Agora não», abre o **modal de onboarding**.
2. **Tirar foto** (câmera frontal no mobile) ou **Escolher imagem**.
3. Abre o **recorte** (quadrado 1:1) → **Aplicar recorte** → pré-visualização atualizada.
4. **Salvar foto** (ou «Usar esta foto» no modal).
5. Em **Portal → Meu perfil** (`/portal/perfil`): mesma experiência para alterar ou **Remover foto**.
6. «**Agora não**» grava `avatar_prompt_dismissed_at` e não volta a mostrar o modal.

### Regras técnicas

| Item | Detalhe |
|------|---------|
| Armazenamento | Disco `public`, pasta `user-avatars/{company_id}/{user_id}/` |
| URL da imagem | `/storage/...` (relativa, igual ao logo do portal — evita `APP_URL` ≠ host real) |
| Validação | `required`, `file`, `image`, máx. 5 MB |
| Recorte | Export JPEG 512×512 após crop |
| Autorização | `UserPolicy::updateOwnAvatar` (próprio utilizador + `portal.access` ou contexto portal) |
| Exibição | `User::hasAvatar()` só devolve true se o ficheiro existir no disco |

### Rotas (`portal.*`)

| Método | URL | Nome |
|--------|-----|------|
| POST | `/portal/perfil/foto` | `portal.profile.avatar.store` |
| DELETE | `/portal/perfil/foto` | `portal.profile.avatar.destroy` |
| POST | `/portal/perfil/foto/pular` | `portal.profile.avatar.dismiss-prompt` |

### Pipeline (camadas)

```
PortalProfileAvatarController
  → StorePortalProfileAvatarRequest (validação)
  → UserProfileAvatarService (upload / remove / dismiss)
  → UserRepository::update
```

### Base de dados

Migration `2026_05_22_120000_add_avatar_to_users_table.php`:

- `users.avatar_path` (string, nullable)
- `users.avatar_prompt_dismissed_at` (timestamp, nullable)

### Frontend

| Ficheiro | Função |
|----------|--------|
| `resources/js/portal-avatar-crop.js` | Alpine `portalAvatarForm` + Cropper.js |
| `resources/css/portal-avatar-crop.css` | Estilos do crop e botões |
| `resources/views/portal/profile/partials/*` | Formulário, widget, crop, modal onboarding |
| `resources/views/layouts/portal.blade.php` | CSS/JS Vite, estilos avatar, erros de validação, include modal |
| `resources/views/layouts/portal-topbar.blade.php` | Imagem no avatar do menu |
| `app/View/Components/PortalLayout.php` | `showAvatarOnboardingModal` |

`resources/js/app.js` importa `./portal-avatar-crop` **antes** de `Alpine.start()`.

### Dependência npm

```bash
npm install   # cropperjs já em package.json
npm run build # ou npm run dev
```

### Deploy / ambiente

```bash
php artisan migrate
php artisan storage:link   # obrigatório para servir /storage/user-avatars/...
```

---

## 3. Login guest — slogan e refinamentos

| Funcionalidade | Descrição |
|----------------|-----------|
| **Slogan** | Mesmo texto do splash do jogo (`QuestionnaireGameTheme::splashTaglineHtml()`), abaixo do logo quando há branding game |
| **Fonte** | Cinzel (Google Fonts) só nesse modo |
| **Wireframe** | Triângulos alinhados à splash; mobile com tamanhos/opacidades ajustados |
| **Logo mobile** | Animação flutuante + glow (`guest-auth__logo-wrap--animated`) |

Ficheiros principais: `resources/views/layouts/guest.blade.php`, `resources/css/guest-auth-wires.css`, `resources/views/auth/login.blade.php`, `tests/Feature/Auth/AuthenticationTest.php`.

---

## 4. Dashboard — progresso por participante + Excel

### Produto

- Nova secção no final de `/dashboard`: tabela de participantes (inclui quem não iniciou).
- Sessão mais recente no âmbito dos filtros (área, grupo, período).
- Botão **Baixar Excel** → `GET /dashboard/participantes/exportar-excel` (mesmos query params do filtro).
- **Atualização (mesmo lote ou commit seguinte):** busca, ordenação por coluna e paginação na tabela — ver [bi-dashboard.md](./bi-dashboard.md) e [lote-usuarios-import-fila-dashboard-2026-05-20.md](./lote-usuarios-import-fila-dashboard-2026-05-20.md#5-dashboard--progresso-por-participante-busca-ordenação-paginação).

### Camadas

```
DashboardController::participants / exportParticipantsProgress
  → TrainingDashboardService::participantsWithProgress / exportParticipantsProgressExcel
  → TrainingDashboardRepository::participantsWithProgress
  → TrainingDashboardExportService (PhpSpreadsheet)
```

### Views / testes

- `resources/views/dashboard/_participants-progress.blade.php`
- `resources/views/dashboard.blade.php` (include da partial)
- `tests/Feature/Dashboard/TrainingDashboardTest.php` (novos casos)

Documentação de rotas atualizada em [bi-dashboard.md](./bi-dashboard.md).

---

## 5. Componente modal global

`resources/views/components/modal.blade.php`:

- Painel **centrado** (`flex min-h-full items-center justify-center`)
- Backdrop com `bg-gray-500/75`
- Usado pelo modal de avatar e pelos modais existentes do sistema

---

## 6. Inventário de ficheiros (working tree)

### Novos

```
app/Http/Controllers/Portal/PortalProfileAvatarController.php
app/Http/Requests/Portal/StorePortalProfileAvatarRequest.php
app/Services/UserProfileAvatarService.php
app/Services/TrainingDashboardExportService.php
database/migrations/2026_05_22_120000_add_avatar_to_users_table.php
resources/css/guest-auth-wires.css
resources/css/portal-avatar-crop.css
resources/js/portal-avatar-crop.js
resources/views/dashboard/_participants-progress.blade.php
resources/views/portal/profile/partials/avatar-crop-panel.blade.php
resources/views/portal/profile/partials/avatar-form.blade.php
resources/views/portal/profile/partials/avatar-onboarding-modal.blade.php
resources/views/portal/profile/partials/avatar-settings-card.blade.php
resources/views/portal/profile/partials/avatar-upload-widget.blade.php
tests/Feature/Portal/PortalProfileAvatarTest.php
tests/Feature/Portal/PortalProfileAvatarAdminAccessTest.php
```

### Alterados

```
app/Http/Controllers/DashboardController.php
app/Models/User.php
app/Policies/UserPolicy.php
app/Repositories/Contracts/TrainingDashboardRepositoryInterface.php
app/Repositories/Eloquent/TrainingDashboardRepository.php
app/Services/TrainingDashboardService.php
app/View/Components/PortalLayout.php
docs/bi-dashboard.md
docs/lote-ui-login-jogar-resultado-2026-05-19.md
package.json
package-lock.json
resources/js/app.js
resources/views/auth/login.blade.php
resources/views/components/modal.blade.php
resources/views/dashboard.blade.php
resources/views/layouts/guest.blade.php
resources/views/layouts/portal-topbar.blade.php
resources/views/layouts/portal.blade.php
resources/views/portal/profile/edit.blade.php
routes/web.php
tests/Feature/Auth/AuthenticationTest.php
tests/Feature/Dashboard/TrainingDashboardTest.php
tests/Feature/Profile/ProfileLayoutContextTest.php
vite.config.js
```

---

## 7. Testes

```bash
php artisan test --filter=PortalProfileAvatar
php artisan test --filter=ProfileLayoutContext
php artisan test --filter=AuthenticationTest::test_login_uses_portal_appearance
php artisan test --filter=TrainingDashboard
```

---

## 8. Checklist antes do commit

- [ ] `php artisan migrate` (local/staging/produção)
- [ ] `php artisan storage:link` em cada ambiente
- [ ] `npm run build` (ou assets commitados após build em CI)
- [ ] Testar fluxo: login portal → modal foto → crop → salvar → avatar no menu
- [ ] Testar `/portal/perfil` alterar/remover foto
- [ ] Testar dashboard: tabela progresso + download Excel com filtros
- [ ] Confirmar que não há ficheiros sensíveis (`.env`) no stage

---

## 9. Mensagens de commit sugeridas

### Opção A — um único commit

```
feat(portal): avatar com crop, login slogan e dashboard progresso Excel

- Foto de perfil no portal (modal onboarding, Meu perfil, Cropper.js)
- URLs de avatar relativas a /storage; erros de validação no layout portal
- Login guest: slogan do game, wires e CSS dedicado
- Dashboard: progresso por participante e exportação XLSX
- Modal Blade centralizado na tela
```

### Opção B — commits separados (revisão mais fácil)

1. `feat(portal): foto de perfil com recorte e modal de onboarding`
2. `feat(dashboard): progresso por participante e exportação Excel`
3. `feat(auth): slogan e wires no login guest`
4. `fix(ui): centralizar componente modal`

---

## 10. Erro 419 (página expirada) no login

### Causa habitual

O Laravel grava o token CSRF na **sessão** (cookie). O 419 aparece quando o POST do login **não envia o mesmo cookie** da página que exibiu o formulário.

Cenário típico em desenvolvimento:

| Você abre no browser | `route('login')` gerava (APP_URL) | Resultado |
|----------------------|-----------------------------------|-----------|
| `http://127.0.0.1:8000/login` | `http://localhost/login` | POST em outro host → sessão nova → **419** |

### Correções aplicadas

- Formulário de login com `action="/login"` (URL relativa ao host atual).
- `AppServiceProvider::alignGeneratedUrlsWithCurrentRequest()` — `URL::forceRootUrl()` com o host da requisição HTTP.

### O que conferir no `.env`

- `APP_URL` deve usar o **mesmo host e porta** que você digita no browser (ex.: `http://127.0.0.1:8000`, não `http://localhost` se acessa por 127.0.0.1).
- `SESSION_DOMAIN` vazio/null em local (não force `.localhost` se usa IP).
- `SESSION_SECURE_COOKIE=true` só com **HTTPS**; em HTTP local deixe `false` ou omitido.
- `SESSION_DRIVER=database` → tabela `sessions` migrada e DB acessível.

```bash
php artisan migrate
php artisan config:clear
```

---

## 11. Notas para produção

1. **Limite de upload PHP:** `upload_max_filesize` / `post_max_size` ≥ 6M recomendado.
2. **Permissões:** `storage/app/public` gravável pelo PHP-FPM.
3. **HEIC (iPhone):** validação aceita `image/*` genérico; após crop o envio é sempre JPEG.
4. Registos antigos com `avatar_path` sem ficheiro: `hasAvatar()` trata como sem foto até novo upload.

---

## 11. Relacionado (outros lotes na mesma árvore)

- [pendente-commit-perfil-auth-provider-2026-05-20.md](./pendente-commit-perfil-auth-provider-2026-05-20.md) — perfil PT + `auth_provider` Microsoft (**outro lote**, confirmar se já foi commitado à parte).
- [lote-ui-login-jogar-resultado-2026-05-19.md](./lote-ui-login-jogar-resultado-2026-05-19.md) — UI game login/jogar/resultado (base do slogan/wires).
