# EventFlow — Estudo Técnico Completo da Versão Atual (JSON / PHP)

> **Documento técnico para reescrita.** Este arquivo descreve, com profundidade, todo o sistema EventFlow na sua versão atual (PHP 8.2 + persistência em arquivos JSON), servindo de base para a nova versão em **Laravel 13 + PostgreSQL 16 (compatível com Aurora)**.
>
> Origem analisada: `/var/www/EventFlow/eventflow_versao_json/` (ZIP `EventFlow.zip`, ~14,5 MB).
> Branding atual: **"Melhores do Ano Valença"** (instância concreta — o sistema é genérico para premiações empresariais).

---

## Sumário

1. [Visão geral do produto](#1-visão-geral-do-produto)
2. [Arquitetura atual](#2-arquitetura-atual)
3. [Estrutura de diretórios](#3-estrutura-de-diretórios)
4. [Modelo de dados (entidades JSON)](#4-modelo-de-dados-entidades-json)
5. [Configuração e constantes globais](#5-configuração-e-constantes-globais)
6. [Roteamento](#6-roteamento)
7. [Autenticação e sessões](#7-autenticação-e-sessões)
8. [Camada de persistência (`JsonHandler`)](#8-camada-de-persistência-jsonhandler)
9. [Helpers globais](#9-helpers-globais)
10. [Painel administrativo — módulos detalhados](#10-painel-administrativo--módulos-detalhados)
11. [Telas operacionais (uso durante o evento)](#11-telas-operacionais-uso-durante-o-evento)
12. [Tela do convidado (voucher)](#12-tela-do-convidado-voucher)
13. [API HTTP](#13-api-http)
14. [Sistema de uploads e mídia](#14-sistema-de-uploads-e-mídia)
15. [Voucher e QR Code](#15-voucher-e-qr-code)
16. [Pesquisa de satisfação](#16-pesquisa-de-satisfação)
17. [Totalização de presenças e reordenamento manual (planejado v2)](#17-totalização-de-presenças-e-reordenamento-manual-planejado-v2)
18. [Backup e restauração](#18-backup-e-restauração)
19. [Importação de dados](#19-importação-de-dados)
20. [Identidade visual e UX](#20-identidade-visual-e-ux)
21. [Scripts CLI utilitários](#21-scripts-cli-utilitários)
22. [Áreas auxiliares (`comercial/`)](#22-áreas-auxiliares-comercial)
23. [Limites técnicos e operacionais conhecidos](#23-limites-técnicos-e-operacionais-conhecidos)
24. [Mapeamento para a nova arquitetura (Laravel 13 + PostgreSQL 16)](#24-mapeamento-para-a-nova-arquitetura-laravel-13--postgresql-16)
25. [Funcionalidade futura: modo offline](#25-funcionalidade-futura-modo-offline)
26. [Checklist de paridade funcional](#26-checklist-de-paridade-funcional)

---

## 1. Visão geral do produto

**EventFlow** é um sistema de gestão e operação de cerimônias de premiação empresarial (modelo "Melhores do Ano"), pensado para uso em três momentos distintos:

- **Pré-evento** — cadastro de categorias, empresas, fotos/vídeos de logomarca, geração de vouchers com QR Code, configuração de mesas e cotas.
- **Durante o evento (operação ao vivo)** — recepção (check-in por confirmação manual), tela dos apresentadores (ordem inteligente de chamada), controle do telão (operador escolhe o que projetar), telão (projeção principal com transições cinematográficas), tela do convidado (acompanhamento via voucher).
- **Pós-evento** — pesquisa de satisfação por critérios (1–5 estrelas), relatórios de chegada e ganhadores, backup/restauração.

### Atores (perfis de uso)

| Perfil | Tela usada | Acesso |
|---|---|---|
| Administrador (organizador) | `/admin/*` | E-mail + senha |
| Recepcionista | `/recepcao` | Aberto (operacional, kiosk) |
| Apresentador / cerimonialista | `/apresentadores` | Aberto (kiosk) |
| Operador de telão | `/telao/controle` | Aberto (kiosk) |
| Telão (projeção) | `/telao` | Aberto (sem cursor) |
| Convidado | `/convidado` | Voucher de 6 caracteres ou QR Code |

### Principais conceitos de domínio

- **Evento** — registro singleton (1 evento ativo: nome, cidade, ano, logo, regra de exibição).
- **Categoria** — eixo de premiação (ex.: "Padaria", "Loja de Calçados"). Numerada e codificada (`CAT0001`).
- **Empresa** — entidade premiada/convidada. Pode estar em N categorias e ter ganhado em M (M ⊆ N). Possui logo, vídeo opcional, mesa, cota, voucher.
- **Cota** — patrocínio (ouro / prata / bronze) ou tipo especial (convidado / autoridade / "ganhador padrão" sem cota).
- **Slide especial** — mídia avulsa (imagem ou vídeo) projetável fora do fluxo de empresas.
- **Estado do telão** — ponteiro singleton para o que está sendo projetado agora (empresa, slide, boas-vindas ou nada).
- **Voucher** — código alfanumérico de 6 caracteres (sem caracteres ambíguos) que dá acesso à tela do convidado.
- **Avaliação** — voto 1–5 por critério, agrupado por empresa (mesa).
- **Check-in (planejado v2 — ver seção 17)** — registro individual de cada pessoa que chega escaneando o QR. A empresa é marcada como confirmada no primeiro check-in; os demais incrementam o contador `X/Y`.
- **Fila de boas-vindas (planejado v2 — ver seção 17)** — fila com tempo de deslocamento (atraso após o check-in) e tempo de exibição configuráveis. Dispara apenas no primeiro check-in da empresa.
- **Reordenamento manual (planejado v2 — ver seção 17)** — override da ordem natural da chamada, aplicado pelo cerimonial backstage com reconfirmação de senha.

---

## 2. Arquitetura atual

| Camada | Tecnologia |
|---|---|
| Servidor web | Apache 2.4 + `mod_rewrite` (vhost dedicado, HTTPS via Let's Encrypt) |
| Linguagem | PHP 8.2+ (usa `match`, named arguments, `never`, `readonly` indireto) |
| Persistência | **Sistema de arquivos** (JSON) — sem banco de dados relacional |
| Cache | Cache em memória de leitura (`JsonHandler::$cache`), válido apenas no escopo da requisição |
| Frontend | Server-side rendered (PHP) + Tailwind CSS via CDN + Lucide Icons via CDN + JS vanilla |
| Bibliotecas externas | `qrcodejs` (QR), `crypto-js` (AES das credenciais "lembrar") |
| Tipografia | Playfair Display, Cinzel, Outfit (Google Fonts) |
| Tempo real | **Long polling** simples — frontend faz `fetch` a cada 2s/3s/4s/5s dependendo da tela |
| Sessão | PHP nativo (`session_*`), nome `MELHORES_SESSION`, timeout 8h |

### Padrão de comunicação

Não há WebSocket nem SSE. Todo "tempo real" é polling HTTP:

| Tela | Endpoint polled | Intervalo | Backoff |
|---|---|---|---|
| Telão (projeção) | `GET /api/telao` | 2s | — |
| Controle do telão | `GET /api/empresas` + `GET /api/telao` | 3s | — |
| Apresentadores | `GET /api/empresas` | 4s | até 15s em erro |
| Recepção | `GET /api/empresas` | 5s | — |
| Convidado (dashboard) | `GET /api/empresas` | 5s | — |

### Arquitetura lógica de uma requisição

```
URL → .htaccess → index.php → Router::dispatch($route)
  ├─ rota pública  → pages/<rota>.php
  ├─ rota admin   → admin/<rota>.php  (requer Auth::requireLogin())
  └─ rota api     → api/<rota>.php    (responde JSON)
```

Cada arquivo de rota é autocontido — inclui `head.php` (HTML/CSS) ou apenas processa e retorna JSON.

---

## 3. Estrutura de diretórios

```
eventflow_versao_json/
├── .htaccess                     # rewrite + bloqueia .json + mod_php upload limits
├── .user.ini                     # ajustes PHP por diretório
├── README.md                     # README curto (operacional)
├── config.php                    # constantes globais e bootstrap
├── index.php                     # ponto de entrada (Router::dispatch)
├── iniciar_claude.sh             # script auxiliar (não relacionado ao app)
├── vincular_logos.php            # utilitário pontual: associa imagens da pasta logos a empresas pelo nome
│
├── app/                          # núcleo
│   ├── auth.php                  # Auth (multi-usuário, JSON em data/usuarios.json)
│   ├── helpers.php               # slugify, normalizeSearch, generateCode, generateVoucher, redirect, e(), flash, badges
│   ├── json-handler.php          # JsonHandler (CRUD JSON; getAll/save/delete; arquivo-por-empresa)
│   └── router.php                # Router::dispatch
│
├── admin/                        # painel administrativo (login obrigatório)
│   ├── index.php                 # redirect → /admin/dashboard
│   ├── login.php                 # form de login com "lembrar" (AES no localStorage)
│   ├── logout.php
│   ├── dashboard.php             # stats + ações rápidas + últimas chegadas
│   ├── empresas.php              # listagem (mobile/desktop) + filtros por cota + busca
│   ├── empresa-form.php          # cadastro/edição (com upload logo + vídeo, validações)
│   ├── empresa-deletar.php       # GET — apaga empresa
│   ├── empresa-voucher.php       # tela do voucher imprimível com QR
│   ├── categorias.php            # listagem
│   ├── categoria-form.php        # cadastro/edição
│   ├── categoria-deletar.php     # GET — apaga categoria
│   ├── slides.php                # CRUD de slides especiais (imagem/vídeo)
│   ├── avaliacoes.php            # painel de pesquisa (médias por critério, por empresa)
│   ├── importar.php              # importação JSON (categorias e empresas)
│   ├── relatorio.php             # tabela imprimível de ganhadores OU chegada
│   ├── backup.php                # criar/restaurar/baixar/deletar backups
│   └── configuracoes.php         # dados do evento, alterar senha, resetar (chegadas/chamados/telão/tudo)
│
├── api/                          # endpoints JSON
│   ├── empresas.php              # GET — lista enriquecida (categorias + filtros)
│   ├── presenca.php              # POST/GET — confirmar/cancelar presença + lista
│   ├── chamado.php               # POST — chamar/resetar empresa
│   ├── telao.php                 # GET (estado) + POST (exibir/exibir_slide/boas_vindas/ocultar)
│   └── avaliacao.php             # POST (gravar voto) + GET (listar — admin)
│
├── pages/                        # páginas operacionais (não-admin)
│   ├── home.php                  # redireciona para /admin
│   ├── 404.php                   # página de erro
│   ├── apresentadores.php        # tela dos apresentadores
│   ├── recepcao.php              # check-in da recepção
│   ├── telao.php                 # projeção principal (1318 linhas: 5 telas internas)
│   ├── telao-controle.php        # interface do operador
│   ├── convidado.php             # login por voucher + dashboard do convidado
│   ├── convidado-pesquisa.php    # formulário de avaliação (estrelas)
│   └── documentacao.php          # documentação interna (renderizada em HTML)
│
├── includes/                     # parciais
│   ├── head.php                  # DOCTYPE, fonts, Tailwind, paleta CSS, badges
│   ├── sidebar-admin.php         # menu lateral do painel
│   ├── admin-layout-start.php    # abre layout (require head + sidebar + flash)
│   └── admin-layout-end.php      # fecha layout
│
├── scripts/                      # CLI (executados via `php scripts/...`)
│   ├── gerar-vouchers.php        # garante voucher único para empresas sem voucher_codigo
│   └── importar-ganhadores.php   # importa data/csv/ganhadores.csv
│
├── comercial/                    # site/landing comercial estático (HTML + imagens)
│   ├── index.html
│   └── img/...                   # 6 imagens geradas por IA
│
├── assets/
│   └── uploads/
│       ├── logos/                # logos das empresas (após upload, convertidas para WebP)
│       ├── slides/               # mídia dos slides especiais
│       ├── videos/               # vídeos de apresentação das empresas
│       └── evento/               # logo do evento (PNG transparente)
│
└── data/                         # persistência (protegida por .htaccess: deny .json)
    ├── evento.json               # singleton do evento
    ├── categorias.json           # array de categorias (222 registros na base atual)
    ├── usuarios.json             # admins (multi-usuário)
    ├── pesquisa-criterios.json   # critérios da pesquisa (6 fixos)
    ├── slides.json               # array de slides especiais
    ├── telao-state.json          # estado atual do telão {ativo, tipo, ts}
    ├── empresas/                 # 1 arquivo por empresa: EMP<8hex>.json
    │   └── EMP<HEX>.json         # 10 registros na base atual
    ├── avaliacoes/               # 1 arquivo por empresa avaliada
    │   └── <EMP>.json
    ├── csv/
    │   └── ganhadores.csv        # CSV de origem para importação massiva
    └── (backups/ — criada em runtime, fora de data/, em ../backups)
```

---

## 4. Modelo de dados (entidades JSON)

### 4.1 Evento (`data/evento.json`) — singleton

```json
{
  "nome": "Melhores do Ano",
  "cidade": "Valença",
  "ano": "2025",
  "logo": "logo-evento.png",
  "chamado_exibe_telao": false
}
```

| Campo | Tipo | Descrição |
|---|---|---|
| `nome` | string | Nome do evento (display em headers, voucher, telão) |
| `cidade` | string | Cidade (compõe o "nome completo": `nome + ' ' + cidade`) |
| `ano` | string | Ano de edição (4 dígitos) |
| `logo` | string | Nome do arquivo PNG em `assets/uploads/evento/` (sempre `logo-evento.png` na prática) |
| `chamado_exibe_telao` | bool | Se `true`, ao apresentador chamar a empresa, ela é projetada automaticamente no telão. Se `false` (padrão), o operador do telão decide manualmente. |

### 4.2 Categoria (`data/categorias.json`) — array

```json
{
  "codigo": "CAT0001",
  "nome": "Academia de Ginástica/Studio",
  "numero": 1,
  "criado_em": "2025-01-01T00:00:00-03:00",
  "atualizado_em": "2025-01-01T00:00:00-03:00"
}
```

| Campo | Tipo | Descrição |
|---|---|---|
| `codigo` | string | PK no formato `CAT` + 4 dígitos zero-padded (`CAT0001`). Pode também ser hash random (`CAT` + 8 hex) quando criado pelo form. |
| `nome` | string | Nome da categoria |
| `numero` | int | Número de exibição (1..N), define a ordenação na listagem |
| `criado_em` | string | ISO 8601 com timezone |
| `atualizado_em` | string | ISO 8601 com timezone |

**Exemplo de escala:** 222 categorias na base atual (`CAT0001`–`CAT0223`, com gaps possíveis).

### 4.3 Empresa (`data/empresas/<codigo>.json`) — arquivo por entidade

```json
{
  "codigo": "EMP028B33DA",
  "nome": "Murilo Buffet",
  "nome_popular": "",
  "link": "",
  "mesa": "54",
  "qtd_pessoas": 8,
  "cota": "",
  "categorias": ["CAT0070"],
  "categorias_ganhou": ["CAT0070"],
  "observacao": "",
  "logo": "Murilo Buffet.jpeg",
  "video": "",
  "ativo": true,
  "logo_formato": "quadrada",
  "foto_telao": true,
  "video_telao": false,
  "confirmado": false,
  "confirmado_em": "",
  "chamado": false,
  "chamado_em": "",
  "criado_em": "2026-03-20T23:15:44-03:00",
  "atualizado_em": "2026-03-27T22:47:36-03:00",
  "voucher_codigo": "BRTRMP"
}
```

| Campo | Tipo | Descrição | Default |
|---|---|---|---|
| `codigo` | string | PK no formato `EMP` + 8 hex (gerado por `generateCode('EMP')`) | gerado |
| `nome` | string | Razão social / nome principal | obrig. |
| `nome_popular` | string | Apelido (exibido em segundo plano em algumas telas) | `""` |
| `link` | string | URL externa (site/rede social) | `""` |
| `mesa` | string | Número da mesa (string para aceitar formatos não-numéricos) | `""` |
| `qtd_pessoas` | int | Capacidade da mesa (limite de avaliações da pesquisa) | `0` |
| `cota` | enum | `""` (ganhador padrão) / `"ouro"` / `"prata"` / `"bronze"` / `"convidado"` / `"autoridade"` | `""` |
| `categorias` | string[] | Códigos das categorias em que está inscrita | `[]` |
| `categorias_ganhou` | string[] | Subconjunto de `categorias` em que ganhou (1º lugar) | `[]` |
| `observacao` | string | Texto livre — exibido como descrição expansível na tela dos apresentadores | `""` |
| `logo` | string | Nome do arquivo em `assets/uploads/logos/` | `""` |
| `video` | string | Nome do arquivo em `assets/uploads/videos/` | `""` |
| `ativo` | bool | Se `false`, oculta de todas as telas operacionais (recepção/apresentador/telão) | `true` |
| `logo_formato` | enum | `"quadrada"` (logo ao lado do texto) ou `"retangular"` (logo em destaque, layout estilo vídeo) | `"quadrada"` |
| `foto_telao` | bool | Exibe a foto/logo no telão (mutuamente exclusivo com `video_telao`) | `true` |
| `video_telao` | bool | Exibe o vídeo no telão (requer `video` preenchido) | `false` |
| `confirmado` | bool | Marcado como presente (recepção) | `false` |
| `confirmado_em` | string | ISO 8601 do momento da confirmação | `""` |
| `chamado` | bool | Marcado como chamado pelo apresentador | `false` |
| `chamado_em` | string | ISO 8601 do momento da chamada | `""` |
| `voucher_codigo` | string | 6 chars de `[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]` (sem ambíguos: I, O, 0, 1) | gerado |
| `criado_em` | string | ISO 8601 | gerado |
| `atualizado_em` | string | ISO 8601 | gerado |

**Invariantes:**
- `categorias_ganhou ⊆ categorias` (a UI marca esse vínculo automaticamente).
- `foto_telao` e `video_telao` são mutuamente exclusivos (lógica no JS do form).
- Se `video` está vazio, `video_telao` deve ser `false`.

> **Nota v2 (ver seção 17):** na v2, `qtd_pessoas` é renomeado para `seats` (capacidade comprada) e o boolean `confirmado` é substituído por **contagem real de pessoas chegadas** (`headcount_cached`) derivada da nova entidade `PresenceCheckin`. A empresa também ganha `display_order_override` (reordenamento manual) e `welcome_shown` (flag da fila de boas-vindas).

### 4.4 Slide especial (`data/slides.json`) — array

```json
{
  "codigo": "SLDA1B2C3D4",
  "titulo": "Patrocinador Oficial",
  "midia": "SLDA1B2C3D4.mp4",
  "midia_tipo": "video",
  "ordem": 1,
  "criado_em": "...",
  "atualizado_em": "..."
}
```

| Campo | Tipo | Descrição |
|---|---|---|
| `codigo` | string | PK `SLD` + 8 hex |
| `titulo` | string | Texto exibido sob a mídia no telão |
| `midia` | string | Nome do arquivo em `assets/uploads/slides/` |
| `midia_tipo` | enum | `"image"` ou `"video"` (detectado pelo MIME / extensão) |
| `ordem` | int | Ordenação na listagem do controle do telão |

### 4.5 Estado do telão (`data/telao-state.json`) — singleton

```json
{
  "ativo": "EMP028B33DA",
  "tipo": "empresa",
  "ts": 1730000000
}
```

| Campo | Tipo | Descrição |
|---|---|---|
| `ativo` | string | Código atualmente projetado (`EMP*` ou `SLD*`); vazio = espera |
| `tipo` | enum | `""` / `"empresa"` / `"slide"` / `"boas_vindas"` |
| `ts` | int | Unix timestamp da última mudança |

### 4.6 Usuários admin (`data/usuarios.json`)

```json
{
  "usuarios": [
    {
      "nome": "Miqueias Reale",
      "email": "miqueias@realetech.com.br",
      "senha_hash": "$2y$10$...",
      "criado_em": "2026-03-20T16:58:20-03:00"
    }
  ]
}
```

- Hash via `password_hash(..., PASSWORD_DEFAULT)` (bcrypt).
- Email é único (case-insensitive) e usado como identificador.
- Auto-criado com 2 usuários padrão (Miqueias Reale e Clayton) no primeiro acesso.

### 4.7 Pesquisa de critérios (`data/pesquisa-criterios.json`) — fixo

```json
[
  {"codigo": "organizacao",        "nome": "Organização do Evento"},
  {"codigo": "buffet",             "nome": "Qualidade do Buffet"},
  {"codigo": "atendimento",        "nome": "Atendimento da Equipe"},
  {"codigo": "entretenimento",     "nome": "Entretenimento / Música"},
  {"codigo": "infraestrutura",     "nome": "Infraestrutura do Local"},
  {"codigo": "experiencia_geral",  "nome": "Experiência Geral"}
]
```

### 4.8 Avaliações (`data/avaliacoes/<EMP>.json`) — arquivo por empresa avaliada

```json
{
  "empresa_codigo": "EMP028B33DA",
  "avaliacoes": [
    {
      "id": "AVLA1B2C3",
      "criterios": {
        "organizacao": 5,
        "buffet": 4,
        "atendimento": 5,
        "entretenimento": 5,
        "infraestrutura": 4,
        "experiencia_geral": 5
      },
      "criado_em": "2026-03-20T23:30:00-03:00"
    }
  ]
}
```

- Limite por empresa: `qtd_pessoas` da empresa; ao atingir, a API retorna erro e a UI bloqueia.
- Cada avaliação tem ID `AVL` + 6 hex.
- Valores aceitos: `1..5` por critério (validado server-side e client-side).

---

## 5. Configuração e constantes globais

`config.php` define as constantes que se propagam para todo o sistema:

### Caminhos
- `BASE_PATH` — raiz do projeto.
- `DATA_PATH` — `BASE_PATH/data`.
- `PAGES_PATH`, `ADMIN_PATH`, `INCLUDES_PATH`, `ASSETS_PATH`.
- `UPLOADS_PATH` = `assets/uploads`.
- `LOGOS_PATH`, `SLIDES_PATH`, `VIDEOS_PATH`, `EVENTO_LOGO_PATH`.

### URLs
- `BASE_URL` — em produção é hard-coded para `https://melhoresdoanovalenca.com.br`. Em CLI: `http://localhost:8085`. Em desenvolvimento: derivada de `$_SERVER['REQUEST_SCHEME']` + `$_SERVER['HTTP_HOST']`.
- `ASSETS_URL`, `LOGOS_URL`, `SLIDES_URL`, `VIDEOS_URL`, `EVENTO_LOGO_URL`.

### Identidade dinâmica do evento (lidas do `data/evento.json` no bootstrap)
- `SITE_NOME`, `SITE_CIDADE`, `SITE_ANO`, `SITE_NOME_COMPLETO` (= `SITE_NOME + ' ' + SITE_CIDADE`), `EVENTO_LOGO`.

### Cores (constantes)
- `COR_OURO` `#C9A84C`, `COR_PRATA` `#A8A9AD`, `COR_BRONZE` `#8C5A2E`, `COR_DARK` `#0D0D1A`, `COR_CARD` `#13132A`.

### Sessão e uploads
- `SESSION_NAME` = `MELHORES_SESSION`.
- `SESSION_TIMEOUT` = 28800 s (8 horas).
- `UPLOAD_MAX_SIZE` = 500 MB.

### Branding (rodapé)
- `DEV_NOME` = `Reale Tech Soluções em TI`.
- `DEV_SITE` = `https://realetech.com.br`.

### Ambiente
- `APP_ENV` = `'development'` (hardcoded — ainda não é por env var).
- `APP_DEBUG` ligado em dev → `display_errors = 1` e `error_reporting(E_ALL)`.
- Timezone: `America/Bahia`.

### Bootstrap também faz
- `session_name` + `session_start` (exceto em CLI).
- Inclui `app/helpers.php`, `app/json-handler.php`, `app/auth.php`, `app/router.php`.

---

## 6. Roteamento

O `index.php` é minimalista:

```php
$route = isset($_GET['route']) ? trim($_GET['route'], '/') : '';
Router::dispatch($route);
```

O `.htaccess` reescreve qualquer request que não seja arquivo/diretório real para `index.php?route=$1`.

`Router::dispatch` consulta três mapas estáticos:

### Rotas públicas

| Rota | Arquivo |
|---|---|
| `''` (raiz) | `pages/home.php` (redireciona para `/admin`) |
| `apresentadores` | `pages/apresentadores.php` |
| `recepcao` | `pages/recepcao.php` |
| `telao` | `pages/telao.php` |
| `telao/controle` | `pages/telao-controle.php` |
| `documentacao` | `pages/documentacao.php` |
| `convidado` | `pages/convidado.php` |
| `convidado/pesquisa` | `pages/convidado-pesquisa.php` |

### Rotas admin

| Rota | Arquivo |
|---|---|
| `admin` | `admin/dashboard.php` |
| `admin/login` | `admin/login.php` |
| `admin/logout` | `admin/logout.php` |
| `admin/empresas` | `admin/empresas.php` |
| `admin/empresas/nova` | `admin/empresa-form.php` |
| `admin/empresas/editar` | `admin/empresa-form.php` |
| `admin/empresas/deletar` | `admin/empresa-deletar.php` |
| `admin/empresas/voucher` | `admin/empresa-voucher.php` |
| `admin/categorias` | `admin/categorias.php` |
| `admin/categorias/nova` | `admin/categoria-form.php` |
| `admin/categorias/editar` | `admin/categoria-form.php` |
| `admin/categorias/deletar` | `admin/categoria-deletar.php` |
| `admin/importar` | `admin/importar.php` |
| `admin/configuracoes` | `admin/configuracoes.php` |
| `admin/relatorio` | `admin/relatorio.php` |
| `admin/slides` | `admin/slides.php` |
| `admin/backup` | `admin/backup.php` |
| `admin/avaliacoes` | `admin/avaliacoes.php` |

### Rotas API

| Rota | Arquivo | Método aceito |
|---|---|---|
| `api/empresas` | `api/empresas.php` | `GET` |
| `api/presenca` | `api/presenca.php` | `GET`, `POST` |
| `api/chamado` | `api/chamado.php` | `POST` |
| `api/telao` | `api/telao.php` | `GET`, `POST` |
| `api/avaliacao` | `api/avaliacao.php` | `GET` (admin), `POST` (convidado) |
| `api/upload-logo` | (declarado mas não implementado) | — |
| `api/export-pdf` | (declarado mas não implementado) | — |

> Rotas API declaradas mas inexistentes (`upload-logo`, `export-pdf`) caem no 404 — não são chamadas pelo frontend atual.

### Comportamento no 404
`http_response_code(404)` + render de `pages/404.php`.

### Limitação arquitetural relevante
Todos os redirects, links e chamadas usam URLs absolutas (`/admin`, `/api/...`, `/convidado`). O sistema **só funciona se rodar na raiz do domínio** — não suporta rodar em subdiretório (`/eventflow/`).

---

## 7. Autenticação e sessões

Implementação na classe `Auth` (`app/auth.php`).

### Login
1. Usuário envia `email` + `senha` para `/admin/login`.
2. `Auth::login()` carrega `data/usuarios.json`, faz lookup case-insensitive por email, e valida com `password_verify`.
3. Em sucesso, popula:
   - `$_SESSION['admin_logged'] = true`
   - `$_SESSION['admin_last_activity'] = time()`
   - `$_SESSION['admin_email']`, `$_SESSION['admin_nome']`.
4. Redireciona para `/admin/dashboard.php`.

### Sessão
- `isLogged()` valida que `admin_logged` é true E que `time() - admin_last_activity ≤ SESSION_TIMEOUT (8h)`. Se inválido, faz logout.
- A cada chamada, atualiza `admin_last_activity`.
- `requireLogin()` redireciona para `/admin/login` se não logado.

### Logout
`/admin/logout` → `Auth::logout()` (apaga as chaves de sessão) → redirect.

### Operações de usuário
- `Auth::changePassword(email, novaSenha)` — usado em `/admin/configuracoes`.
- `Auth::addUser(nome, email, senha)` — existe na classe, mas **sem UI** para chamar (apenas via código/script).
- `Auth::criarUsuariosPadrao()` — chamado se o arquivo `usuarios.json` não existe; cria os 2 usuários default com senhas hardcoded.

### "Lembrar credenciais" (cliente)
- No `/admin/login`, há um checkbox "Lembrar credenciais".
- Implementado totalmente no client com `crypto-js`:
  - Encripta `{ e, s }` (email + senha) com AES e chave hardcoded `M3lh0r3s#V@l3nc@2025!`.
  - Persiste em `localStorage['melhores_cred']`.
  - Ao abrir a tela de login, descripta e preenche os campos.
- **Risco de segurança:** chave AES está em texto plano no JS — qualquer um inspecionando consegue descriptografar. Em uma reescrita, isso deve ir embora ou ser substituído por **passkeys / cookies HttpOnly + refresh tokens**.

### Sessão do convidado
Gerenciada nas próprias páginas `convidado.php` (não há classe). Chaves:
- `$_SESSION['convidado_voucher']` — código do voucher.
- `$_SESSION['convidado_empresa']` — código da empresa associada.

Login por:
- Query string `?v=<voucher>` (vindo do QR).
- POST com campo `voucher`.

Logout: `/convidado?sair=1` apaga as chaves.

---

## 8. Camada de persistência (`JsonHandler`)

Arquivo `app/json-handler.php`, classe `JsonHandler`.

### Conceito
- Cache estático em memória (`self::$cache[$file]`) durante a requisição.
- Leitura: `read($file)` retorna array vazio se inexistente.
- Escrita: `write($file, $data)` cria diretórios necessários, escreve com flags `JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES` e `LOCK_EX`.
- Cache é invalidado automaticamente em escrita.

### API específica de domínio

| Método | Comportamento |
|---|---|
| `getAllEmpresas()` | Lê todos os `data/empresas/*.json`, ordena por `nome` ascendente |
| `getEmpresa($codigo)` | Lê `data/empresas/{codigo}.json` ou `null` |
| `saveEmpresa($empresa)` | Escreve em `data/empresas/{codigo}.json` |
| `deleteEmpresa($codigo)` | `unlink` do arquivo |
| `getEmpresaByVoucher($voucher)` | Itera todas as empresas comparando `voucher_codigo` (case-insensitive, trimmed) |
| `getAllCategorias()` | Lê `data/categorias.json`, ordena por `numero` |
| `saveCategoria($cat)` | Atualiza em-place ou anexa no array; reescreve o arquivo todo |
| `deleteCategoria($codigo)` | Remove do array e reescreve |
| `getCategoria($codigo)` | Busca linear pelo `codigo` |

### Helpers globais (alias)
```php
function json_read($file): array;     // lê DATA_PATH/$file
function json_write($file, $data): bool;
```

### Limitações conhecidas
- Sem transações: uma escrita em meio a um crash pode deixar JSON corrompido (mitigado parcialmente pelo `LOCK_EX`).
- Sem índices: `getEmpresaByVoucher` faz scan linear de todos os arquivos.
- Sem paginação: `getAllEmpresas` carrega tudo.
- Sem relacionamentos formais: integridade referencial é responsabilidade do código de aplicação.

---

## 9. Helpers globais

`app/helpers.php`:

| Função | Descrição |
|---|---|
| `slugify(str)` | Lowercase + transliteração ASCII + apenas `[a-z0-9-]` |
| `normalizeSearch(str)` | Lowercase + remove acentos (mapa manual) — usado para buscas client/server |
| `generateCode(prefix='')` | `prefix` + 8 hex (uppercase MD5 truncado) |
| `generateVoucher(length=6)` | Random com alfabeto sem ambíguos: `ABCDEFGHJKLMNPQRSTUVWXYZ23456789` |
| `formatBytes(bytes, precision=1)` | Formata em B/KB/MB/GB |
| `formatDate(date)` | ISO 8601 → `d/m/Y H:i` |
| `redirect(url)` | `header('Location: ' . BASE_URL . $url); exit;` |
| `e(str)` | `htmlspecialchars` com `ENT_QUOTES`, `UTF-8` |
| `flash(type, message)` | Salva em `$_SESSION['flash']` |
| `getFlash()` | Lê e remove (one-shot) |
| `cotaBadge(cota)` | HTML do badge colorido com ícone Lucide |
| `cotaLabel(cota)` | String "Cota Ouro" / "Cota Prata" / etc., ou "Ganhador" se vazio |

---

## 10. Painel administrativo — módulos detalhados

### 10.1 Layout e navegação
- Layout shell em `includes/admin-layout-start.php` + `admin-layout-end.php`.
- Sidebar fixa em `includes/sidebar-admin.php` (260 px no desktop; drawer mobile com overlay e botão hamburger).
- Toda página admin chama `Auth::requireLogin()` no topo.

**Estrutura do menu:**

```
PAINEL
  Dashboard
  Empresas
  Categorias

EVENTO  (links abrem em nova aba)
  Tela Apresentadores
  Tela Recepção
  Controle do Telão
  Telão (Projeção)
  Tela Convidado

FERRAMENTAS
  Slides Especiais
  Pesquisa Satisfação
  Importar JSON
  Relatório PDF
  Backup
  Configurações
  Documentação  (nova aba)

[ Sair ]
```

### 10.2 Dashboard (`/admin`)

**Hero:** título + nome completo do evento + ano. Botões rápidos: Telão / Recepção / Apresentadores.

**Cards de estatísticas (4 colunas):**
- Empresas Cadastradas (count total).
- Confirmaram Presença (`confirmado=true`).
- Ganhadores (com `categorias_ganhou` não vazio).
- Convidados/Autoridades (`cota` em `convidado` ou `autoridade`).

**Painéis inferiores:**
- *Ações rápidas* — links para "Nova Empresa", "Nova Categoria", "Exportar Relatório PDF".
- *Últimas chegadas* — últimas 5 confirmações (ordenadas por `confirmado_em`), com logo, nome, mesa e horário.

### 10.3 Empresas (`/admin/empresas`)

**Listagem dual** (tabela em desktop, cards em mobile), com:
- Busca client-side em `nome` e `nome_popular` (normalizada por `normalizeSearch`).
- Filtros por cota: Todas / Ouro / Prata / Bronze / Convidado / Autoridade (botões redondos).
- Empresas inativas (`ativo=false`) aparecem com `opacity:.45` e label "INATIVA".

**Por linha:**
- Logo (ou placeholder com inicial sobre gradiente dourado).
- Nome + nome popular + código (mono).
- Mesa (em destaque dourado).
- Badge da cota.
- Categorias (até 2 visíveis, "+N" para o resto). Categorias ganhas vêm com badge "premiado".
- Status de presença: "Confirmado" + horário ou "Aguardando".
- Ações: **Editar** / **Voucher** (abre em nova aba) / **Deletar** (com `confirm()`).

### 10.4 Empresa form (`/admin/empresas/nova` | `/editar?codigo=...`)

Formulário multipart com seções:

**1. Dados da empresa** (8 campos):
- Nome \* (obrigatório), nome popular, mesa, qtd_pessoas, link, cota (select), observação (textarea), checkbox "Empresa ativa".

**2. Logomarca:**
- Preview da logo atual (ou placeholder).
- Upload (qualquer formato de imagem) — convertido server-side para WebP via GD (redimensiona max 800x800, qualidade 85). Fallback: `move_uploaded_file` se GD não estiver disponível.
- Radio: formato no telão — **quadrada** (logo ao lado das informações) | **retangular** (em destaque, layout estilo vídeo).

**3. Exibição no telão:**
- Toggle "Exibir foto no Telão".
- Toggle "Exibir vídeo no Telão" (mutuamente exclusivo com foto).
- Upload de vídeo (`mp4, webm, ogg, m4v, mov`) — guarda como `<EMP>.{ext}` em `assets/uploads/videos/`.
- Se já há vídeo, mostra preview + checkbox "Remover".
- JS impõe que apenas um esteja ativo de cada vez. Se vídeo está sendo removido, desabilita o toggle de vídeo.

**4. Categorias:**
- Campo de busca (filtra client-side).
- Lista scrollável (max-height 256 px) — cada categoria tem checkbox "selecionar" e checkbox "Ganhou" (só habilitado se selecionada).

**Validações server-side:**
- Detecta `post_max_size` excedido (POST vazio com `CONTENT_LENGTH > 0`) — exibe mensagem clara antes de zerar dados.
- Verifica códigos individuais de erro de upload (`UPLOAD_ERR_*`).
- Valida `size > UPLOAD_MAX_SIZE` (500 MB).
- Valida MIME/extensão de vídeo.

**Validação client-side (pré-envio):**
- Tamanho de cada arquivo selecionado.

**Pós-save:**
- `flash('success', '... cadastrada/atualizada!')`.
- Redirect para `/admin/empresas`.

**Geração de código:**
- Empresa nova: `EMP` + 8 hex (`generateCode`).
- Voucher: gerado se ausente.

### 10.5 Categorias (`/admin/categorias`)

- Listagem com busca normalizada (mesmo padrão).
- Tabela: número da categoria (badge dourada), nome, código (mono), ações Editar/Deletar.
- Form (`/admin/categorias/nova` | `/editar?codigo=...`) — apenas `numero` (int) e `nome` (string).
- Código gerado: `generateCode('CAT')` → `CAT` + 8 hex (na importação é `CAT0001` zero-padded por número).

### 10.6 Configurações (`/admin/configuracoes`)

**4 blocos:**

1. **Dados do evento** — formulário multipart com `evento_nome`, `evento_ano`, `evento_cidade`, `evento_logo` (upload PNG transparente, validado por MIME `image/png`, sempre salvo como `logo-evento.png`), e checkbox `chamado_exibe_telao`.
2. **Alterar senha** — nova_senha + confirmar (mínimo 6 caracteres). Atualiza no usuário logado (lookup pelo `admin_email` da sessão).
3. **Controles do evento** (botões com `confirm` JS):
   - **Resetar Chegadas** — `confirmado` e `confirmado_em` limpos em todas as empresas. Mostra contador atual.
   - **Resetar Apresentação** — `chamado` e `chamado_em` limpos.
   - **Resetar Telão** — escreve `{ativo:'', ts: time()}` em `telao-state.json`.
   - **Resetar Tudo** — combina os três acima (preserva empresas e categorias).
4. **Informações do sistema** — versão PHP, GD/WebP disponível, permissões em `data/` e `logos/`, branding "Reale Tech".

### 10.7 Slides especiais (`/admin/slides`)

- CRUD único na mesma página (form em cima + lista em baixo).
- Suporta imagem (`jpg, jpeg, png, webp, gif, svg`) e vídeo (`mp4, webm, ogg, m4v, mov`).
- Detecta `midia_tipo` por extensão **e** MIME (validação dupla).
- Upload sobrescreve mídia anterior (faz `unlink`).
- Listagem mostra thumb (imagem) ou ícone "play-circle" (vídeo).
- Ações: Editar / Deletar (com `confirm`).

### 10.8 Avaliações (`/admin/avaliacoes`)

- Lê `data/avaliacoes/*.json`.
- Calcula totais e contagens por critério.
- **Card de médias** (1 por critério): estrelas (`Math.round`), valor numérico (1 casa), número de votos, barra de progresso.
- **Tabela por empresa**: nome, qtd avaliações, capacidade da mesa, status (`Completo` se atingiu `qtd_pessoas`).

### 10.9 Importação (`/admin/importar`)

Aceita upload de arquivo OU texto colado. Dois modos:

**Modo 1 — Categorias:**
Espera JSON com formato `{"categorias": [{"numero": N, "categoria": "Nome"}]}`. Cria categorias com código `CAT` + numero zero-padded. Não duplica.

**Modo 2 — Empresas Ganhadores:**
Mesmo JSON, lendo `categorias[].primeiro_lugar.nome`. Cria empresa com código `EMP` + 8 hex do MD5(nome). Vincula à categoria como participante e ganhadora. Se já existe, adiciona a nova categoria (sem duplicar).

### 10.10 Relatório (`/admin/relatorio`)

Duas abas: **Ganhadores** (default) | **Ordem de Chegada**.

**Ordenação:**
- Ganhadores: ordem por cota (`autoridade > convidado > ouro > prata > bronze > sem_cota`), depois por mesa (natural).
- Chegada: filtra `confirmado=true`, ordena por `confirmado_em`.

**Tabela:** #, Empresa (nome + popular + observação), Mesa, Cota/Tipo, Categorias Ganhas (badges), Presença (ou Chegou em).

**Print CSS:** botão "Exportar / Imprimir PDF" dispara `window.print()`. CSS especializado de print:
- Esconde sidebar, navegação, botões, ícones Lucide.
- Cores ajustadas para fundo branco (cota Ouro/Prata/Bronze viram bordas suaves).
- Adiciona cabeçalho em negrito antes da tabela com nome do evento.
- `tr { page-break-inside: avoid; }`.

### 10.11 Backup (`/admin/backup`)

- Lista a pasta `../backups/` (FORA de `data/`, atenção: caminho relativo).
- **Criar:** copia recursivamente `data/` para `backups/backup_YYYY-MM-DD_HH-MM-SS/`.
- **Restaurar:** apaga TODOS os `data/empresas/*.json` e copia o backup por cima. Limpa cache.
- **Deletar:** remove a pasta de backup recursivamente.
- **Download ZIP:** se a extensão `zip` está habilitada, monta zip em `sys_get_temp_dir()` e serve via `readfile`.

---

## 11. Telas operacionais (uso durante o evento)

### 11.1 Recepção (`/recepcao`)

Tela kiosk para uso da equipe na entrada do evento.

**Estrutura:**
- Header sticky com logo do evento, título, relógio em tempo real e duas abas:
  - **Não Confirmados** (default) — empresas sem `confirmado=true`, busca em `nome`/`nome_popular`.
  - **Confirmados** — ordenados por `confirmado_em` desc.
- Cada card mostra: logo (ou placeholder), nome, popular, mesa, badge da cota.
- Ação no card pendente: botão dourado **"Chegou"** → `POST /api/presenca {acao:'confirmar'}`.
- Ação no card confirmado: botão vermelho **"Desfazer"** → `POST /api/presenca {acao:'cancelar'}`.

**Comportamento:**
- Polling a cada 5 s; merge inteligente que preserva confirmações locais recentes (otimismo de UI).
- Foco automático na busca ao trocar de aba.
- Auto-renderização ao confirmar (sem esperar polling).

**Side effect importante:** ao confirmar presença, a API automaticamente seta `telao-state.json` para `{tipo:'boas_vindas', ativo: <codigo>}`. **Boas-vindas é disparada na chegada.**

> **Nota v2 (ver seção 17):** o disparo deixa de ser instantâneo. Passa a ser **enfileirado** (`WelcomeQueueEntry`) com tempo de deslocamento + tempo de exibição configuráveis, e dispara apenas no primeiro check-in da empresa. A recepção ganha contador `X/Y` por mesa.

### 11.2 Tela dos apresentadores (`/apresentadores`)

Tela para o cerimonialista no palco.

**Lista visível (apenas confirmados):**
- Agrupada por seção:
  1. **Convidados & Autoridades**
  2. **Patrocinadores & Cotas** (ouro/prata/bronze)
  3. **Ganhadores**
- Dentro de cada grupo: ordenação por cota e por `confirmado_em` (chegada).
- Empresas chamadas ficam com `opacity:.35` + grayscale (visualmente "consumidas").

**Por card:**
- Logo, nome, mesa, badge cota.
- Tags douradas das categorias ganhas.
- **Indicador "Descrição"** (se `observacao` preenchida) — clique abre painel inline com o texto da observação. Apenas um aberto por vez.
- Botão **"Chamar"** → `POST /api/chamado {acao:'chamar'}`. Quando ativo, vira "Chamado" em verde.

**Outros controles:**
- Busca por nome.
- Toggle "Ocultar chamados / Mostrar chamados".
- Indicador de conexão (LED + "X confirmados" / "sem conexão" após 5 erros).

**Polling:** 4 s, com backoff exponencial até 15 s em caso de erros consecutivos.

> **Nota v2 (ver seção 17):** a ordenação passa a respeitar `display_order_override` quando definido (reordenamento manual via `/admin/queue` com reauth). Cada linha ganha badge `X/Y` com a contagem de presenças.

### 11.3 Controle do telão (`/telao/controle`)

Interface usada por um operador (geralmente lateral, num notebook/tablet) para escolher o que aparece no telão principal.

**Header:** logo, título, indicador "no ar"/"em espera", botão "Ocultar telão", botão "Toggle exibidos".

**Lista (em ordem):**
1. **Slides especiais** (sempre primeiro, com seção própria).
2. **Convidados & Autoridades.**
3. **Patrocinadores.**
4. **Ganhadores.**

Cada item:
- Slide: thumb (imagem) ou ícone play (vídeo), título, ordem, badge "FIXADO" se aplicável, botão de **fixar/desfixar** (pin).
- Empresa: logo, nome, categorias ganhas (texto inline), mesa, badge cota, botão **"Boas-Vindas"** (mão+coração).

**Estados visuais:**
- Card em destaque dourado: está sendo exibido AGORA no telão (`NO TELÃO` badge live com pulse).
- Card mais opaco com borda dourada esmaecida: já foi exibido (`Exibido` badge).

**Lógica de "fixados" para slides:**
- Slides recém-chegados via polling viram fixados por padrão (a primeira vez).
- Fixado: permanece na lista mesmo após exibido.
- Não fixado: some da lista após ser exibido (a menos que `mostrarExibidos` esteja ativo).

**Ações:**
- Click no card de empresa → `POST /api/telao {acao:'exibir', codigo}`. Se já é o ativo, oculta.
- Click no card de slide → `POST /api/telao {acao:'exibir_slide', codigo}`.
- Botão Boas-Vindas → `POST /api/telao {acao:'boas_vindas', codigo}`.
- "Ocultar Telão" → `POST /api/telao {acao:'ocultar'}`.

**Polling:** 3 s, em paralelo `/api/empresas` + `/api/telao`.

**UX:** ao clicar, faz `scrollIntoView` no card ativado.

### 11.4 Telão (`/telao`)

Página de projeção. **Cursor escondido** (`cursor: none`). 5 telas internas comutáveis com transições suaves:

1. **Tela de espera** — logo do evento + nome + cidade/ano. Apresentada quando `ativo=''`.
2. **Tela de empresa (logo quadrada)** — layout 2 colunas: logo grande à esquerda + bloco de texto (categorias, divisor dourado, nome, nome popular). Background tem a logo borrada com overlay escuro. Mesa em badge no canto inferior direito. Badge do evento no canto superior esquerdo.
3. **Tela de empresa (logo retangular)** — barra superior com nome grande + categorias em pares; logo em destaque grande no centro (mesmo layout do vídeo, mas com imagem).
4. **Tela de empresa com vídeo** — barra superior idêntica + vídeo em destaque (`autoplay loop muted playsinline`).
5. **Tela de slide especial** — mídia centralizada + texto/título embaixo + divisor dourado.
6. **Tela de boas-vindas** — animação especial de chegada: logo da empresa + "Seja Bem-Vindo!" + nome + mesa.

**Decisão de qual tela mostrar:**
```
if  tipo == 'boas_vindas'  → exibirBoasVindas()
elif tipo == 'slide'       → exibirSlide()
elif video_url presente    → exibirEmpresaVideo()
elif logo_formato=='retangular' → exibirLogoRetangular()
else                       → exibir() (padrão quadrada)
```

**Responsividade extrema** — vários `@media (min-aspect-ratio:...)` para 16:9 ultrawide, 16:10, 4:3 vertical (tablet), notebook 13". Todas as fontes em `clamp()` com unidades `vh/vw/vmin`.

**Animações:** fade-in/scale-in para todos os elementos, partículas decorativas em background (30 divs animadas), pulse no banner de boas-vindas.

**Polling:** 2 s — o mais rápido do sistema, pois é a tela mais visível.

---

## 12. Tela do convidado (voucher)

`/convidado` tem dois modos:

### Modo login
Se não autenticado:
- Tela centralizada com logo do evento, input grande maiúsculo (8 chars max — embora vouchers sejam 6).
- Aceita login via:
  - QS `?v=<voucher>` (link do QR Code).
  - POST do form.
- Em sucesso, popula sessão `convidado_voucher` + `convidado_empresa` e redireciona.
- Em erro, mostra mensagem inline.

### Modo dashboard (autenticado)
Header sticky com logo + "Ao vivo" (LED verde) + relógio.

**Banner de boas-vindas:** nome da empresa + "Mesa X".

**Banner de chamada (oculto até a empresa ser chamada):**
- Verde, com pulse, com texto "🎉 SUA EMPRESA FOI CHAMADA! Dirija-se ao palco."

**Lista de "Ordem de Apresentação":**
- Apenas confirmadas, ordenadas como na tela do apresentador (cota → chegada).
- Para cada item: número da ordem, nome (com ⭐ se é a empresa do convidado), meta (`Mesa X · badge cota`), badge "Apresentado" se já foi chamada.
- Card destacado em dourado se for a empresa do próprio convidado.
- Cards de empresas já chamadas ficam com `opacity:.4`.

**Botão dourado "Avaliar o Evento"** → `/convidado/pesquisa`.

**Footer:** link "Sair".

**Polling:** 5 s.

> **Nota v2 (ver seção 17):** o convidado vê o contador da própria mesa em destaque ("Você é a 4ª pessoa da sua mesa! 4/8"), e a lista de ordem respeita o `display_order_override` aplicado pelo backstage.

### Pesquisa (`/convidado/pesquisa`)
Form com 6 critérios em estrelas (1–5). Validação:
- Server: cada critério obrigatório, valor 1–5, `qtd_pessoas` da empresa não excedida.
- Client: botão "Enviar Avaliação" só habilita quando todos os 6 critérios estão preenchidos. Hover effects nas estrelas.

Em sucesso, esconde o form e mostra tela "Obrigado pela sua avaliação!" com ícone check animado.

Se a empresa atingiu o limite (`qtdFeitas >= qtdPessoas`), exibe mensagem "Todas as avaliações da sua mesa já foram registradas".

---

## 13. API HTTP

Todas as respostas são `application/json` com `Cache-Control: no-store` em endpoints sensíveis.

### `GET /api/empresas`
Lista enriquecida. Filtra `ativo=true`. Cada empresa vem com `categorias_ganhou_detalhes: [{codigo, nome, numero}]`.

**Resposta:**
```json
{ "ok": true, "empresas": [...], "ts": 1730000000 }
```

### `POST /api/presenca`
Body: `{ "codigo": "EMP...", "acao": "confirmar" | "cancelar" }`.

**Em `confirmar`:**
- Marca `confirmado=true` + `confirmado_em=date('c')`.
- **Side effect:** escreve `telao-state.json` com `{tipo:'boas_vindas', ativo:codigo}` automaticamente (dispara boas-vindas no telão).

**Em `cancelar`:** apaga os campos. Não mexe no telão.

**Resposta:** `{ "ok": true, "empresa": {...} }`.

### `GET /api/presenca`
Retorna apenas confirmados ordenados por `confirmado_em`.

### `POST /api/chamado`
Body: `{ "codigo", "acao": "chamar" | "resetar" }`.

**Em `chamar`:**
- Marca `chamado=true` + `chamado_em`.
- **Side effect condicional:** se `evento.chamado_exibe_telao=true`, escreve `telao-state.json` com `{tipo:'empresa', ativo:codigo}` (auto-projeta).

### `GET /api/telao`
Resposta enriquecida do estado atual:
```json
{
  "ok": true,
  "ativo": "EMP028B33DA",
  "tipo": "empresa",
  "empresa": {
    "...campos...",
    "categorias_detalhes": [...],
    "categorias_ganhou_detalhes": [...],
    "video_url": "https://.../assets/uploads/videos/EMP028B33DA.mp4"
  },
  "slide": null,
  "slides": [{"...todos os slides com midia_url..."}],
  "ts": 1730000000
}
```

`empresa.video_url` só é populado se `video_telao=true` E `video` preenchido.

### `POST /api/telao`
Body: `{ "acao": "...", "codigo": "..." }`.

**Ações suportadas:**
- `exibir` — projeta empresa.
- `exibir_slide` — projeta slide especial.
- `boas_vindas` — projeta tela de boas-vindas da empresa.
- `ocultar` — volta para tela de espera.

Cada ação valida que o código existe antes de gravar.

### `POST /api/avaliacao`
Body: `{ "criterios": { "<codigo>": <1..5>, ... } }`.

Requer `$_SESSION['convidado_empresa']`. Validações:
- Cada critério da config (`pesquisa-criterios.json`) precisa estar presente e ser numérico 1–5.
- Não pode exceder `qtd_pessoas` da empresa.

Adiciona avaliação ao array em `data/avaliacoes/<codigo>.json`.

### `GET /api/avaliacao`
Requer `Auth::isLogged()` (admin). Aceita `?empresa=<codigo>` ou retorna todas.

---

## 14. Sistema de uploads e mídia

### Limites globais
- Apache/.htaccess (via `mod_php.c`): `upload_max_filesize 500M`, `post_max_size 512M`, `memory_limit 768M`, `max_execution_time 300`.
- App: `UPLOAD_MAX_SIZE = 500 * 1024 * 1024` (validado em PHP e em JS).

### Pipeline de logo (em `empresa-form.php`)
1. Recebe upload em `$_FILES['logo']`.
2. Detecta extensão original.
3. Tenta carregar com função apropriada do GD: `imagecreatefromjpeg/png/webp/gif`, ou fallback `imagecreatefromstring`.
4. Se imagem > 800x800, redimensiona mantendo proporção (`imagecreatetruecolor` + `imagecopyresampled`).
5. Salva como WebP com qualidade 85 (`imagewebp`).
6. Nome final: `{codigo}-{slug-do-nome}.webp`.
7. Se GD falha, fallback `move_uploaded_file` mantendo o original.

### Pipeline de vídeo
- Aceita extensões: `mp4, webm, ogg, m4v, mov`.
- Validação dupla: extensão E MIME (`mime_content_type`).
- Nome final: `{codigo}.{ext}`.
- Apaga vídeo anterior automaticamente em substituição.

### Pipeline de logo do evento
- Apenas PNG (validado por MIME `image/png` — para garantir transparência).
- Nome fixo: `logo-evento.png`.
- Apaga logo anterior em upload novo.

### Pipeline de slides
- Mesma lista de extensões de imagem e vídeo.
- Detecta `midia_tipo` (`image` | `video`) por extensão E MIME.
- Nome final: `{codigo}.{ext}`.

### Detecção de POST excedido
Truque clever: se `REQUEST_METHOD=POST`, `$_POST` está vazio E `CONTENT_LENGTH > 0`, significa que o `post_max_size` foi excedido. Mostra mensagem clara em vez de zerar dados da empresa por engano.

### Proteção
- Diretório `data/` bloqueado para acesso direto (`.htaccess` deny `.json`).
- Diretório `backups/` bloqueado por regra explícita (`RewriteRule ^backups/ - [F,L]`).
- Upload validado por MIME, extensão, tamanho.

---

## 15. Voucher e QR Code

### Geração
- 6 caracteres randômicos uniformes do alfabeto `ABCDEFGHJKLMNPQRSTUVWXYZ23456789` (32 símbolos sem `I`, `O`, `0`, `1` para evitar confusão visual).
- Espaço de combinações: 32^6 ≈ 1 bilhão. Ainda assim, em produção convém validar unicidade (o script `gerar-vouchers.php` faz isso por loop `do/while` contra a lista existente).

### Página do voucher (`/admin/empresas/voucher?codigo=...`)
- Página standalone (sem layout admin) — pronta para impressão.
- Identidade visual estilo "ingresso premium":
  - Fundo dark com gradiente.
  - Estrelas decorativas via `radial-gradient` (CSS).
  - Borda dourada com sombra dourada.
  - Tipografia Cinzel + Playfair.
- Conteúdo:
  - Nome do evento + cidade + ano.
  - Nome da empresa em fonte Playfair grande.
  - Linha com mesa, cota, qtd_pessoas.
  - QR Code com URL `BASE_URL/convidado?v=<voucher>` (200x200, gerado por `qrcodejs`).
  - Código do voucher em destaque dourado, espaçado (letter-spacing alto).
  - Instrução curta + assinatura "Reale Tech".
- CSS de print otimizado para A6/A5.

### Login pelo QR
- O convidado escaneia o QR no celular, abre `https://.../convidado?v=ABC123` e está autenticado.
- Não há expiração nem invalidação — quem tem o código tem acesso.

### Risco de segurança a considerar na reescrita
- 6 caracteres é forçável em ataque por enumeração se não houver rate limiting. Em uma cerimônia presencial isso é OK; para uso público convém ampliar para 8+ ou adicionar HMAC + assinatura.

---

## 16. Pesquisa de satisfação

### Critérios fixos
6 critérios pré-definidos em `data/pesquisa-criterios.json`. Não há UI para editar (precisaria editar o arquivo).

### Limite de submissões
- A empresa tem `qtd_pessoas` (ex.: 8). Máximo 8 avaliações por essa empresa.
- A 9ª submissão retorna erro "Limite atingido".
- Se `qtd_pessoas=0`, **sem limite** (interpretado como "infinito"; a UI exibe `∞`).

### Validação
- Front: botão habilita só com todos os 6 critérios marcados.
- Back: cada critério presente, numérico, intervalo 1–5.

### Identificação
- Avaliação é anônima dentro da empresa: salva apenas `id` (gerado), `criterios`, `criado_em`. Não há nome da pessoa.

### Painel admin (`/admin/avaliacoes`)
- Médias agregadas por critério.
- Tabela por empresa: avaliações feitas / capacidade / status (✓ Completo se atingiu o limite).

---

## 17. Totalização de presenças e reordenamento manual (planejado v2)

> **Esta seção descreve um conjunto de funcionalidades acordadas para a v2 — ainda não existem na v1.**
> Atualmente a v1 só tem um boolean `confirmado` por empresa, dispara boas-vindas instantaneamente (sem delay, sem fila) e não permite reordenar manualmente a sequência de chamada.
>
> Esta seção consolida três decisões de produto inter-relacionadas:
> 1. **Totalização individual de presenças** — quantas pessoas chegaram, de quantas a empresa comprou.
> 2. **Fila de boas-vindas** com tempo de deslocamento até a mesa e tempo de exibição configuráveis.
> 3. **Reordenamento manual** da fila do apresentador, protegido por reauth.

### 17.1 Por que essas funcionalidades

#### Totalização

Hoje (v1) o registro de presença é binário. Mas na realidade:
- Cada empresa compra um **pacote** com número fixo de cadeiras (4, 6, 8, 10…).
- As pessoas chegam **em momentos diferentes**.
- O cerimonial precisa saber em tempo real "quanto da mesa já está ocupada".

#### Fila de boas-vindas

Hoje (v1), ao confirmar a chegada, o telão exibe boas-vindas **na hora**. Em uso real, isso é problemático:
- A pessoa que escaneou ainda está **na entrada/recepção** — não chegou a ver a mensagem dela na tela.
- Várias chegadas em poucos segundos sobrescrevem a animação umas das outras.
- O cerimonial não tem como controlar **quando** e **por quanto tempo** cada boas-vindas aparece.

A solução é uma fila com dois parâmetros configuráveis pelo organizador:
- **Tempo de deslocamento** (`welcome_delay_seconds`, default 5s) — atraso entre o check-in e a exibição. Permite à pessoa sair da recepção e caminhar até a mesa para ver-se no telão.
- **Tempo de exibição** (`welcome_duration_seconds`, default 3s) — duração da mensagem no telão antes de dar lugar à próxima da fila.

#### Reordenamento manual

A ordem natural (cota → primeiro check-in) é boa como default, mas o cerimonial precisa **flexibilidade**:
- Empresa chegou cedo, mas pediu para ser chamada mais tarde.
- VIP atrasou e precisa pular para o início.
- Ajustes in-flight do roteiro do palco.

Solução: tela backstage para arrastar e soltar a ordem, protegida por reconfirmação de senha (operação sensível ao vivo).

### 17.2 Modelo de check-in múltiplo (cada pessoa do pacote)

#### Conceito

Cada pessoa que chega leva (ou recebe) o **mesmo voucher da empresa**. Ao escanear o QR Code, o sistema:

1. **Se for a primeira pessoa daquela empresa a escanear** → marca a empresa como confirmada (`confirmed_at = now()`), registra o check-in #1 e **enfileira boas-vindas** (item 17.4).
2. **Se a empresa já está confirmada** → apenas adiciona um novo check-in (incrementa o contador). **Não dispara nova boas-vindas.**
3. **A posição da empresa na ordem do apresentador é definida pelo primeiro check-in** (`confirmed_at`). Pessoas adicionais não mudam a posição.

#### Comportamento ao atingir/exceder a capacidade

| Situação | Ação |
|---|---|
| `headcount < seats` | Aceita normal. UI mostra `X/Y`. |
| `headcount == seats` | Aceita o último. UI mostra `Y/Y` com badge "Mesa completa!". |
| `headcount > seats` | Aceita, mas alerta a recepção. UI mostra `X/Y` em laranja. **Não bloqueia** (decisão operacional do organizador). |

#### Onde aparece o contador

| Tela | Onde |
|---|---|
| Recepção (`/recepcao`) | Card da empresa: `6/8` + barra de progresso. Filtro "Mesas incompletas". |
| Apresentador (`/apresentadores`) | Badge discreta na linha: `6/8`. |
| Convidado (`/convidado`) | Banner próprio: "Você é a 4ª pessoa da sua mesa! (4/8)". |
| Painel admin > Relatório | Coluna "Chegaram" com `X/Y`. |
| Telão (boas-vindas) | Opcionalmente "Mesa completa!" no momento que atingiu o total. |

#### Auditoria de check-ins

Cada check-in registra: `company_id`, `voucher_used`, `arrived_at`, `device_id`, `ip`, `user_agent`, `cancelled_at`, `cancelled_by`. Com isso é possível:
- Desfazer último check-in (scan duplo acidental).
- Detectar scan repetido do mesmo dispositivo em curto intervalo (~30s) — pergunta confirmação.
- Auditar pós-evento ("quem registrou o quê").

### 17.3 Modelo de dados (v2)

#### Empresa — campos relevantes

```php
class Company {
    public string $code;
    public int $seats;                       // ex-qtd_pessoas: capacidade comprada
    public ?Carbon $confirmed_at;            // primeiro check-in não cancelado
    public ?int $display_order_override;     // NOVO — override de posicionamento manual
    public int $headcount_cached;            // NOVO — cache mantido por observer
    public bool $welcome_shown;              // NOVO — flag "boas-vindas já exibida"
    // ...
    public function checkins(): HasMany;
}
```

#### Configurações do evento (`Event` ou `EventSettings`)

```php
class Event {
    // ... campos existentes ...
    public bool $welcome_enabled = true;        // pode desligar boas-vindas inteiramente
    public int $welcome_delay_seconds = 5;      // tempo de deslocamento até a mesa (default 5s)
    public int $welcome_duration_seconds = 3;   // tempo na tela do telão (default 3s)
    public ?string $welcome_sound_path;         // áudio opcional tocado junto
}
```

#### Nova entidade — `PresenceCheckin`

```sql
CREATE TABLE presence_checkins (
    id              uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    event_id        uuid NOT NULL REFERENCES events(id) ON DELETE CASCADE,
    company_id      uuid NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
    voucher_used    varchar(16) NOT NULL,
    sequence        smallint NOT NULL,         -- 1ª, 2ª, 3ª pessoa daquela empresa
    arrived_at      timestamptz NOT NULL DEFAULT now(),
    device_id       uuid,                       -- UUID gerado client-side
    ip              inet,
    user_agent      text,
    cancelled_at    timestamptz,                -- soft delete
    cancelled_by    uuid REFERENCES users(id),
    created_at      timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX idx_presence_company  ON presence_checkins(company_id) WHERE cancelled_at IS NULL;
CREATE INDEX idx_presence_arrival  ON presence_checkins(event_id, arrived_at) WHERE cancelled_at IS NULL;
CREATE INDEX idx_presence_device   ON presence_checkins(device_id, company_id) WHERE cancelled_at IS NULL;
```

#### Nova entidade — `WelcomeQueueEntry`

```sql
CREATE TABLE welcome_queue (
    id                       uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    event_id                 uuid NOT NULL REFERENCES events(id) ON DELETE CASCADE,
    company_id               uuid NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
    triggered_by_checkin_id  uuid REFERENCES presence_checkins(id),
    enqueued_at              timestamptz NOT NULL DEFAULT now(),
    eligible_from            timestamptz NOT NULL,        -- enqueued_at + welcome_delay_seconds (snapshot)
    duration_seconds         smallint NOT NULL,            -- snapshot do welcome_duration_seconds
    started_at               timestamptz,                  -- quando começou a exibir
    finished_at              timestamptz,                  -- quando terminou (ou foi pulado/cancelado)
    status                   varchar(16) NOT NULL DEFAULT 'pending',
                                                            -- pending | playing | done | skipped | cancelled
    UNIQUE (event_id, company_id)                          -- só uma boas-vindas por empresa por evento
);
CREATE INDEX idx_welcome_pending ON welcome_queue(event_id, eligible_from)
    WHERE status = 'pending';
```

> **Por que `UNIQUE (event_id, company_id)`?** Garante que mesmo após cancelar e refazer check-ins, não haja duplicação automática. Para repetir, o admin usa endpoint específico.

#### Cálculo de `headcount_cached`

Mantido por observer/trigger:

```php
// app/Observers/PresenceCheckinObserver.php
public function created(PresenceCheckin $c): void {
    $live = $c->company->checkins()->whereNull('cancelled_at');
    $c->company->update([
        'headcount_cached' => $live->count(),
        'confirmed_at'     => $live->orderBy('arrived_at')->first()?->arrived_at,
    ]);
    if ($c->sequence === 1) {
        WelcomeQueueService::enqueue($c);          // dispara fila de boas-vindas
    }
    broadcast(new CompanyArrived($c->company, $c));
}
```

### 17.4 Fila de boas-vindas — design completo

#### Diagrama de estados

```
[checkin chega]
     │
     │ é o primeiro check-in da empresa? (sequence == 1)
     ├── não → ignora (sem boas-vindas)
     └── sim ↓
[cria WelcomeQueueEntry]
   status = pending
   eligible_from   = now() + welcome_delay_seconds   (snapshot)
   duration_seconds = welcome_duration_seconds       (snapshot)
     │
     │  (passa o tempo de deslocamento)
     │
[telão worker olha a fila]
   pega entrada com status=pending E eligible_from <= now()
   ordenando por eligible_from ASC (FIFO)
     │
     ↓
[entra em status=playing]
   started_at = now()
   telão exibe a animação de boas-vindas com a empresa
     │
     │  (passa duration_seconds)
     │
     ↓
[entra em status=done]
   finished_at = now()
   Company.welcome_shown = true
   worker consulta próxima entrada pending
```

#### Regras de processamento

1. **Ordenação na fila:** FIFO por `eligible_from` (não por `enqueued_at`). Se o operador mudar `welcome_delay_seconds` no meio do evento, entradas existentes não são afetadas — cada uma carrega seu próprio "momento elegível".
2. **Duração:** cada entrada armazena seu próprio `duration_seconds` (snapshot). Mudanças de configuração só afetam novas entradas.
3. **Concorrência:** apenas uma entrada em `status=playing` por evento. O worker só inicia a próxima quando a anterior termina.
4. **Apenas no primeiro check-in:** disparo condicionado a `checkin.sequence == 1`. Após cancelamento total e novo check-in, **não recria automaticamente** (a `UNIQUE constraint` segura). O admin pode forçar via "Repetir boas-vindas".
5. **Coexistência com slides e comandos manuais do telão:**
   - Comando manual do operador (exibir empresa específica / slide específico) > fila de boas-vindas > slide pinado em loop > tela de espera.
   - Quando uma boas-vindas elegível chega e há slide em loop ou tela de espera, ela toma o telão.
   - Quando há comando manual ativo (operador exibindo X), a boas-vindas espera o operador encerrar.
6. **Skip / cancel pelo operador** (no `/telao/controle`):
   - "Pular boas-vindas" — marca a atual como `skipped`, libera para a próxima.
   - "Cancelar fila" — todas as `pending` viram `cancelled`.
   - "Pausar fila" — não inicia novas entradas até retomar (entradas continuam em `pending`).
   - "Repetir boas-vindas de [empresa]" — admin cria entrada manual mesmo se a empresa já tem `welcome_shown=true`.

#### Implementação do worker — duas opções

**Opção A — Telão consulta o backend (poll/long-poll):**
- `/telao` chama `GET /api/v1/screen/welcome-queue/next` em intervalos curtos (1–2s) ou via long-poll.
- Backend retorna `null` ou `{queue_id, company, eligible_from, duration_seconds}`.
- Frontend exibe a animação e, ao terminar localmente o `duration_seconds`, chama `POST /api/v1/screen/welcome-queue/{id}/done`.

**Opção B — Worker no backend dispara via WebSocket (recomendada):**
- Job em fila Laravel agendado com `dispatch()->delay($welcome_delay_seconds)`.
- Quando o job roda: atualiza `status=playing` e faz broadcast `WelcomeStarted{queueId, company, durationSeconds}`.
- Schedula novo job para `duration_seconds` depois → `status=done` + broadcast `WelcomeFinished` + verifica próxima da fila.
- Telão apenas reage aos eventos broadcast.

**Recomendação:** **Opção B**. Mais determinística, sobrevive a reload do telão, evita drift de relógio do navegador e permite múltiplos telões em sincronia (caso futuro).

#### UI de configuração (admin)

Em `/admin/settings` → bloco **"Cerimônia / Boas-Vindas"**:

```
☑ Ativar boas-vindas no telão

Tempo de deslocamento até a mesa (segundos)   [ 5 ]   1–60
  ↳ Atraso entre o check-in da empresa e a exibição da boas-vindas no telão.
    Permite que a pessoa caminhe até a mesa antes de ver-se na tela.

Duração da boas-vindas no telão (segundos)    [ 3 ]   1–30
  ↳ Quanto tempo cada boas-vindas fica visível antes de dar lugar à próxima da fila.

Som de chegada (opcional)                     [ Escolher arquivo ]
  ↳ Toca junto com a animação. Suporta MP3, OGG, WAV.
```

#### UI operacional (controle do telão)

Painel **"Fila de boas-vindas"** dentro de `/telao/controle`:

```
[Fila — 3 pendentes]
  [logo] Empresa A — em ~2s
  [logo] Empresa B — em ~7s
  [logo] Empresa C — em ~12s
  
  [no ar agora] Empresa Z — termina em ~1s

[ Pular atual ]  [ Pausar fila ]  [ Cancelar fila ]
[ Repetir boas-vindas... ▾ ]
```

### 17.5 Reordenamento manual da fila (backstage)

#### Ordenação efetiva

A ordem na tela do apresentador e na lista do convidado segue:
1. **Por categoria de cota:** `autoridade > convidado > ouro > prata > bronze > sem cota`.
2. **Dentro de cada cota:** `COALESCE(display_order_override, 999999)` ASC, depois `confirmed_at` ASC.

Quem tem `display_order_override` definido tem prioridade. Quem não tem entra no fim do subgrupo respeitando ordem natural.

#### Tela de gerenciamento

Nova rota: **`/admin/queue`** (também acessível via botão "Reordenar fila" em `/telao/controle`).

- Lista as empresas confirmadas em ordem efetiva atual, agrupadas por cota.
- Drag-and-drop com Sortable.js.
- Ao soltar, faz `POST /api/v1/queue/reorder` com a lista de IDs na nova ordem.
- Botão "**Resetar ordem**" — limpa todos os `display_order_override` do evento.
- Botão "**Salvar ordem atual como manual**" — snapshot: atribui override sequencial a cada empresa na posição atual (útil para "congelar" a ordem antes do início).

#### Reauth (proteção da operação)

Reordenar a fila é **operação sensível** (afeta a cerimônia ao vivo). Antes de aplicar:
- Modal: "Para alterar a ordem da cerimônia, confirme sua senha:"
- `POST /api/v1/auth/reauth` retorna token de **5 minutos** em cookie HttpOnly.
- O endpoint de reorder exige o token no header `X-Reauth-Token`.
- Após 5 min sem uso, expira → nova confirmação.

> **Nota terminológica:** o pedido original mencionou "senha de moderador" (interpretado como "operador da cerimônia" — analogia com operador de mesa de som/luz). Implementação inicial: **mesma senha do admin com reauth obrigatório**. Se a equipe operacional for distinta dos admins, evolui-se para uma role separada (`stage_manager`) com PIN ou senha própria.

#### Eventos de broadcast

- `QueueReordered { eventId }` → telas que mostram a fila (apresentador, convidado, controle do telão) atualizam.

### 17.6 API (v2) — endpoints novos

#### Check-in

| Método | Endpoint | Descrição |
|---|---|---|
| `POST` | `/api/v1/checkin` | Registra chegada via voucher (auto-checkin do convidado ou recepção). Retorna `{is_first, welcome_enqueued}`. Idempotente por `X-Device-Id`. |
| `DELETE` | `/api/v1/checkin/{id}` | Cancela um check-in. Recalcula contadores. Se for o único, `confirmed_at` volta a NULL e a entrada de boas-vindas é cancelada (se ainda `pending`). |
| `GET` | `/api/v1/companies/{code}/checkins` | Lista check-ins de uma empresa (admin). |

**Body de `POST /api/v1/checkin`:**
```json
{ "voucher": "ABC123" }
```
**Header:** `X-Device-Id: <uuid persistido em localStorage>`.

**Resposta:**
```json
{
  "ok": true,
  "company": { "code": "EMP...", "name": "...", "seats": 8 },
  "checkin": { "id": "uuid", "sequence": 4, "arrived_at": "...", "headcount": 4 },
  "is_first": false,
  "welcome_enqueued": false
}
```

#### Fila de boas-vindas

| Método | Endpoint | Descrição |
|---|---|---|
| `GET` | `/api/v1/screen/welcome-queue` | Lista a fila atual (telão / controle). |
| `POST` | `/api/v1/screen/welcome-queue/{id}/skip` | Pula a entrada atual. |
| `POST` | `/api/v1/screen/welcome-queue/{id}/done` | Marca como exibida (usado se for Opção A do worker). |
| `POST` | `/api/v1/screen/welcome-queue/cancel-all` | Cancela todas as `pending`. |
| `POST` | `/api/v1/screen/welcome-queue/pause` | Pausa a fila. |
| `POST` | `/api/v1/screen/welcome-queue/resume` | Retoma. |
| `POST` | `/api/v1/screen/welcome-queue/replay` | Body: `{ "company_id": "..." }`. Cria entrada manual mesmo se já exibida. |

#### Reordenamento

| Método | Endpoint | Descrição |
|---|---|---|
| `POST` | `/api/v1/queue/reorder` | Reordena a fila do apresentador. **Exige `X-Reauth-Token`.** |
| `POST` | `/api/v1/queue/reset` | Limpa todos os `display_order_override`. **Exige `X-Reauth-Token`.** |
| `POST` | `/api/v1/auth/reauth` | Reconfirma senha e emite token de 5 min. Body: `{ "password": "..." }`. |

### 17.7 Impacto nas telas existentes (resumo)

| Tela | Mudança |
|---|---|
| Recepção | Substitui botão "Chegou" (binário) por contador "+/−". Mostra `X/Y` + barra. Pode operar no modo "scan do voucher" (recepcionista escaneia/digita o voucher e confirma). |
| Apresentadores | Lista respeita `display_order_override`. Badge `X/Y` discreta por linha. |
| Controle do telão | Lista respeita `display_order_override`. Botão "Editar ordem" abre `/admin/queue`. **Novo painel "Fila de boas-vindas"** com pular/pausar/cancelar/repetir. |
| Telão (projeção) | **Passa a consumir a fila** com timing controlado. Animação respeita `duration_seconds`. Não há mais boas-vindas instantânea. |
| Convidado | Mostra "Você é a Nª pessoa da sua mesa". Atualiza em tempo real. |
| Pesquisa | Continua usando `seats` como limite máximo (independente de `headcount` real). |
| Admin > Configurações | Adiciona bloco "Cerimônia / Boas-Vindas" com `welcome_enabled`, `welcome_delay_seconds`, `welcome_duration_seconds`, som opcional. |
| **Novo:** Admin > Fila | `/admin/queue` para reordenamento drag-and-drop com reauth. |

### 17.8 Pontos de decisão para a implementação

Itens que merecem alinhamento explícito antes de implementar:

- [ ] **Identificação do dispositivo no check-in:** cookie HttpOnly + localStorage `device_id` UUID? Ou apenas IP+UA (mais frágil)?
- [ ] **Ao exceder capacidade:** bloquear, alertar ou apenas mostrar laranja? (Recomendação: alertar mas aceitar.)
- [ ] **"Senha de moderador":** mesma do admin com reauth, ou role/PIN separado?
- [ ] **Reordenamento entre grupos de cota:** permitir mover empresa entre grupos? (Recomendação: não — preserva semântica.)
- [ ] **Cancelamento total de check-ins:** empresa volta a "aguardando"? (Recomendação: sim, com confirmação.)
- [ ] **Boas-vindas após "ressuscitar" empresa:** re-enfileirar automático ou exigir comando manual? (Recomendação: exigir manual via "Repetir boas-vindas".)
- [ ] **Limites de configuração:** `welcome_delay_seconds` 1–60, `welcome_duration_seconds` 1–30?
- [ ] **Som de chegada:** tocar junto da animação? Mixar com áudio do palco? Quem controla volume?
- [ ] **Pausar fila vs. esvaziar:** UI deve deixar claro que pausar **mantém** entradas para retomar; esvaziar **descarta**.
- [ ] **Telão offline durante uma boas-vindas:** ao reconectar, atrasar a fila (re-enfileirar) ou pular as expiradas? (Recomendação: pular as expiradas, notificar admin com lista do que foi perdido.)
- [ ] **Múltiplos telões em sincronia:** suportar? (Caso futuro — Opção B do worker já viabiliza.)

---

## 18. Backup e restauração

### Localização
`backups/` — **fora** de `data/` mas no nível da raiz do projeto. Bloqueada por `.htaccess` para acesso web direto.

### Estrutura de cada backup
`backups/backup_YYYY-MM-DD_HH-MM-SS/` — espelha `data/` na íntegra (preservando subpastas `empresas/`, `avaliacoes/`, `csv/`).

### Operações suportadas
- **Criar:** copia recursiva via `RecursiveDirectoryIterator`.
- **Listar:** scaneia `backups/`, mostra nome, data (`filemtime`), tamanho total, contagem de arquivos. Ordenado por mais recente.
- **Restaurar:** apaga `data/empresas/*.json` (atenção: apenas empresas, não as outras pastas), e copia o backup por cima. Limpa cache.
- **Download ZIP:** gera ZIP em `sys_get_temp_dir()` e serve via `readfile`. Requer extensão `zip` do PHP.
- **Deletar:** remoção recursiva.

### Limitações conhecidas
- Restauração não cobre todas as pastas `data/` simetricamente (exclui apenas `empresas/`).
- Não há export incremental ou diferencial.
- Sem versionamento ou retenção automática (backups acumulam).
- Sem upload de backup externo (não dá para restaurar um zip de fora pelo painel).

---

## 19. Importação de dados

### Pelo painel (`/admin/importar`)
Formato JSON esperado (modelo do `ganhadores_2025.json`):
```json
{
  "categorias": [
    {
      "numero": 1,
      "categoria": "Academia de Ginástica/Studio",
      "primeiro_lugar": { "nome": "Arena Top Fitness" }
    }
  ]
}
```

Modos:
- **categorias** — só importa categorias.
- **empresas_ganhadores** — importa empresas como ganhadoras das respectivas categorias.

### Por CSV (script CLI)
`php scripts/importar-ganhadores.php` lê `data/csv/ganhadores.csv`:
- Separador `;`.
- Cabeçalho descartado.
- Coluna 0: "1 - Academia de Ginástica/Studio" (extrai número e nome).
- Coluna 1: "Arena Top Fitness - 860 votos" (extrai nome, descarta votos).
- **Apaga TODAS as empresas existentes** antes de criar (operação destrutiva).
- Agrupa empresas com mesmo nome em um único registro (mesma empresa pode ganhar várias categorias).

### Vinculação de logos (script web/CLI)
`vincular_logos.php` (chamada manual no browser) — varre `data/empresas/*.json` e tenta associar imagens da pasta `assets/uploads/logos/`:
1. Busca pelo código (`EMP*`) no nome do arquivo.
2. Se não acha, tenta `similar_text` ≥ 60% no nome limpo da empresa.
3. Atualiza o campo `logo` do JSON. Imprime relatório HTML com ✅/⚠️.

---

## 20. Identidade visual e UX

### Paleta
- **Ouro principal** `#C9A84C` / **Ouro claro** `#E8C96A`.
- **Prata** `#A8A9AD`. **Bronze** `#8C5A2E`.
- **Dark** `#0D0D1A` (fundo). **Card** `#13132A` / **Card 2** `#1A1A35`.
- **Texto** `#E8E8F0` / **Muted** `#8888AA`.
- **Border** `rgba(201,168,76,0.2)`.

### Tipografia
- **Cinzel** — labels uppercase, headers cerimoniais, badges (estilo "premiação").
- **Playfair Display** — nomes de empresa, títulos grandes (estilo editorial).
- **Outfit** — texto corrido, sans-serif moderna.

### Componentes recorrentes
- `.glass-card` — fundo card + border dourada sutil + raio 1rem.
- `.glass-card2` — fundo card2 + border branca translúcida + raio 0.75rem.
- `.gradient-ouro` — gradiente dourado padrão.
- `.btn-ouro` — gradiente dourado com transform/shadow no hover.
- `.btn-ghost` — transparente com border, vira dourado no hover.
- `.btn-danger` — vermelho translúcido.
- Badges de cota: `badge-ouro`, `badge-prata`, `badge-bronze`, `badge-convidado`, `badge-autoridade`, `badge-premiado`.
- `stars-bg` — fundo estrelado via `radial-gradient`.
- Animações: `fade-in`, `pulse-ouro`, partículas em telas decorativas.

### UX
- Mobile-first em todas as telas operacionais (recepção/apresentadores são feitas para tablet/celular).
- Desktop-first no painel admin (sidebar fixa).
- Cursor escondido no telão (`cursor: none`).
- Toasts: flash messages em `_SESSION['flash']` exibidas no topo do layout admin.
- Confirmações destrutivas com `confirm()` JS nativo.
- Buscas normalizadas (sem acento, lowercase) tanto client quanto server.

### Acessibilidade — gaps
- Sem `aria-*` consistente.
- Contraste OK (dourado sobre dark) mas alguns textos `text-slate-600` ficam baixos no fundo escuro.
- Sem suporte a leitor de tela testado.

---

## 21. Scripts CLI utilitários

### `scripts/gerar-vouchers.php`
Gera `voucher_codigo` único para empresas que não têm. Usado uma vez após importação massiva.

### `scripts/importar-ganhadores.php`
Importa do CSV. Operação destrutiva (apaga empresas antes de recriar).

### `vincular_logos.php` (na raiz)
Match heurístico de imagens com empresas (script visual web).

### `iniciar_claude.sh`
Não é parte funcional do app — script utilitário do ambiente.

---

## 22. Áreas auxiliares (`comercial/`)

`comercial/index.html` é uma landing page comercial estática (sem PHP). Imagens em `comercial/img/`. Não interage com o resto do sistema. Pode ser descartada na reescrita.

---

## 23. Limites técnicos e operacionais conhecidos

### Persistência
- ❌ Sem ACID — concorrência depende apenas de `LOCK_EX`. Em escritas simultâneas pode haver corrupção.
- ❌ Sem backup transacional — restore parcial é possível (mas só apaga `empresas/`).
- ❌ Sem replicação ou disaster recovery.
- ❌ Carrega tudo em memória (`getAllEmpresas` lê todos os arquivos por iteração).

### Tempo real
- ❌ Polling intensivo gera carga linear nas telas operacionais (4 telas × 1 req a cada 2–5 s = ~40 req/min mínimo).
- ❌ Sem WebSocket / SSE; reconexão e backpressure são primitivos.
- ❌ Latência mínima de UI: 2 s (intervalo do polling do telão).

### Segurança
- ❌ Senhas hardcoded como default no código (`#Adtg$1020`, `CDL@@9080`).
- ❌ Chave AES do "lembrar credenciais" hardcoded em JS público.
- ❌ Sem CSRF protection nos forms.
- ❌ Sem rate limiting (login, voucher, API).
- ❌ Voucher de 6 chars é enumerável em pequena escala.
- ❌ `data/` exposta — depende exclusivamente do `.htaccess` (não funciona em Nginx sem reescrita).
- ❌ Sem auditoria/log de quem fez o quê.

### Escalabilidade
- ❌ 1 só evento ativo por instalação.
- ❌ Sem multi-tenant.
- ❌ Sem autenticação multi-perfil (apenas "admin" — todos os admins têm os mesmos poderes).
- ❌ Não suporta rodar em subdiretório (URLs absolutas hardcoded).

### Funcionalidades faltantes
- ❌ Sem perfis de usuário (recepcionista, apresentador, operador).
- ❌ Sem auditoria de mudanças.
- ❌ Sem soft delete.
- ❌ Sem export estruturado (CSV/XLSX) das avaliações.
- ❌ Sem agenda/cronograma.
- ❌ Sem suporte a múltiplos idiomas.
- ❌ Sem modo escuro/claro (só dark).

---

## 24. Mapeamento para a nova arquitetura (Laravel 13 + PostgreSQL 16)

> Esta seção é prescritiva — propõe como cada conceito atual deve renascer no novo stack.

### Stack alvo
- **PHP 8.3+, Laravel 13** (em alinhamento com PHP 8.3 LTS para suporte estendido).
- **PostgreSQL 16** (compatível com Aurora; usar `gen_random_uuid()` nativo, JSONB, generated columns, partitioning se necessário).
- **Object storage:** **MinIO** (S3-compatible, self-hosted, AGPLv3 community) para mídia. Driver Laravel `s3` aponta direto via `endpoint` customizado. Em dev e prod usa o mesmo client/SDK; só muda o endpoint e as credenciais.
- **Cache + sessões + filas:** **Valkey** (fork open-source do Redis pela Linux Foundation, drop-in compatible — Laravel Redis client conecta sem alteração de código). Licença BSD-3.
- **Tempo real:** **Laravel Reverb** (WebSocket nativo Laravel) — substitui polling por eventos broadcast. Backplane via Valkey.
- **Filas:** Laravel Queue sobre Valkey — para conversão de imagem (Spatie Image), ZIP de backup, jobs `StartWelcomeJob`/`FinishWelcomeJob`, envio de voucher por email/WhatsApp.
- **Frontend:** Inertia + **React 19 com TypeScript**. Tailwind CSS. shadcn/ui (componentes Radix-based). Lucide para ícones. SSR opcional via Inertia SSR. PWA com Workbox.
- **Auth:** Laravel Fortify / Sanctum (admin via session com cookies HttpOnly, convidado via token de voucher).
- **PDF:** dompdf ou Browsershot/Puppeteer para o relatório/voucher (em vez de `window.print`).
- **QR:** `simplesoftwareio/simple-qrcode` ou `endroid/qr-code`.

### Notas sobre as escolhas de infraestrutura

**Valkey vs Redis** — escolhemos Valkey pela licença BSD-3 limpa (Redis Inc. mudou para SSPL em 2024, problemático para SaaS) e pelo backing da Linux Foundation + AWS/Google/Oracle. Compatibilidade total com clientes Redis 7.2.x; Laravel não percebe diferença.

**MinIO vs AWS S3** — escolhemos MinIO Community para controle total e custo previsível (sem cobrança por GB transferido). Roda em Docker/K8s, com mesma API S3 — basta mudar `endpoint` no `config/filesystems.php`. Vantagem extra: facilita o **modo offline** (instância MinIO local na LAN do evento, telão consome do storage local). Atenção: licença AGPLv3 — para SaaS hospedado pela Reale Tech está OK (sem distribuição); se um dia entregarmos instância on-premise, alternativas drop-in são Garage e SeaweedFS.

**React 19 vs Vue 3** — escolhemos React por mercado de devs maior, ecossistema mais rico (shadcn/ui, Radix, TanStack), TypeScript mais idiomático e padrão dominante na indústria. Inertia v2 tem adapter React maduro; integração com Laravel é plug-and-play.

### Modelos (Eloquent) propostos

#### `Event` (singleton, mas modelado para multi-tenant futuro)
- `id` (uuid), `tenant_id` (uuid, fk), `name`, `city`, `year`, `logo_path`, `welcome_on_arrival` (`bool`), `presenter_auto_screen` (`bool`), timestamps.
- **Configurações da fila de boas-vindas (v2 — ver seção 17):**
  - `welcome_enabled` (bool, default true).
  - `welcome_delay_seconds` (smallint, default 5; range 1–60).
  - `welcome_duration_seconds` (smallint, default 3; range 1–30).
  - `welcome_sound_path` (text, nullable).
- Index unique `(tenant_id, year)`.

#### `Category`
- `id` (uuid), `event_id` (fk), `code` (string, gerado), `name`, `number` (smallint), timestamps.
- Index unique `(event_id, code)` e `(event_id, number)`.

#### `Company` (= empresa)
- `id` (uuid), `event_id` (fk), `code` (string), `name`, `popular_name`, `link`, `table_number` (string), `seats` (smallint, ex `qtd_pessoas`), `tier` (enum `gold|silver|bronze|guest|authority` ou nullable), `notes` (text), `logo_path`, `video_path`, `active` (bool), `logo_layout` (enum `square|rectangle`), `screen_show_photo` (bool), `screen_show_video` (bool), `voucher_code` (string, unique), `confirmed_at` (nullable), `called_at` (nullable), timestamps.
- **Campos v2 (ver seção 17):**
  - `headcount_cached` (smallint, default 0) — cache de check-ins ativos, mantido por observer.
  - `display_order_override` (smallint, nullable) — posição forçada manualmente.
  - `welcome_shown` (bool, default false) — flag de boas-vindas já exibida.
- Index unique `(event_id, code)` e `voucher_code`.
- Index `(event_id, tier, display_order_override, confirmed_at)` para acelerar a ordenação efetiva.

#### `CompanyCategory` (pivot)
- `company_id`, `category_id`, `won` (bool).
- Index `(category_id, company_id)`.

#### `Slide`
- `id`, `event_id`, `code`, `title`, `media_path`, `media_type` (enum `image|video`), `order`, timestamps.

#### `ScreenState` (singleton por evento)
- `event_id` (PK), `active_id` (uuid nullable — empresa ou slide), `kind` (enum `company|slide|welcome|none`), `updated_at`.
- Mudanças disparam evento de broadcast (`ScreenStateChanged`).

#### `User` (admin)
- Padrão Laravel + spatie/laravel-permission para roles (`admin`, `operator`, `presenter`, `receptionist`).

#### `SurveyCriterion` (configuráve, em vez de fixo)
- `id`, `event_id`, `code`, `name`, `order`, timestamps.

#### `SurveyResponse`
- `id`, `company_id`, `submitted_at`.
- Relação `hasMany` `SurveyAnswer` (`criterion_id`, `score` 1..5).

#### `PresenceCheckin` (novo na v2 — ver seção 17)
- `id`, `event_id`, `company_id`, `voucher_used`, `sequence` (smallint), `arrived_at`, `device_id` (uuid), `ip` (inet), `user_agent`, `cancelled_at`, `cancelled_by`, `created_at`.
- Soft-delete via `cancelled_at` (não é o `deleted_at` padrão Laravel — semântica diferente: cancelamento operacional preserva auditoria).
- Observer recalcula `Company.headcount_cached` e `Company.confirmed_at`.

#### `WelcomeQueueEntry` (novo na v2 — ver seção 17)
- `id`, `event_id`, `company_id`, `triggered_by_checkin_id`, `enqueued_at`, `eligible_from`, `duration_seconds`, `started_at`, `finished_at`, `status` (enum `pending|playing|done|skipped|cancelled`).
- Unique `(event_id, company_id)` — uma boas-vindas por empresa por evento.
- Worker via Laravel Queue: jobs `StartWelcomeJob` (delayed) e `FinishWelcomeJob` (delayed).
- Eventos broadcast: `WelcomeStarted`, `WelcomeFinished`, `WelcomeQueueChanged`.

#### `Audit`
- Polimórfico, registra `actor_id`, `action`, `target`, `before`, `after`, `at`. Usar `spatie/laravel-activitylog`.

### Tabelas extras
- `voucher_logs` — quem acessou cada voucher (IP, user-agent, when) — para detectar abuso.
- `media_assets` — registro central de uploads (com `disk`, `path`, `original_name`, `bytes`, `mime`, `created_by`).

### URL design (revista)
Manter rotas próximas das atuais para não quebrar muscle memory:

```
/admin                       Dashboard
/admin/companies             (era empresas)
/admin/categories
/admin/slides
/admin/surveys
/admin/imports
/admin/reports
/admin/backups
/admin/settings

/reception
/presenters
/screen           (telão)
/screen/control   (controle)
/guest            (convidado)
/guest/survey
```

(em pt-BR, manter `/recepcao`, `/apresentadores`, `/telao`, `/convidado` se quiser preservar.)

### API (revista)
Padronizar JSON:API ou ao menos manter contratos atuais com versionamento `/api/v1/...`.

### Tempo real (revista)
- Substituir polling por broadcast.
- Eventos: `CompanyArrived`, `CompanyCalled`, `ScreenChanged`, `SlideShown`, `WelcomeTriggered`.
- Telão e controle assinam canais `event.{eventId}.screen` e `event.{eventId}.companies`.
- Reduz tráfego em 90%+ e latência <100 ms.

### Migrations / seeders
- Seeder com 222 categorias (manter números).
- Factory para empresa (com Faker).
- Migration de import dos JSONs atuais (módulo "migração v1 → v2").

### Eventos / observers
- `Company::saved` → recalcula contadores no Valkey.
- `Company::saved` com `confirmed_at` mudando → `WelcomeTriggered` (replicar comportamento atual de auto-boas-vindas).

### Validações nativas Laravel
- FormRequests para empresa, categoria, slide, survey.
- Regra customizada `Vimeo|YouTube` no campo `link` (opcional).
- Hash do voucher: `Str::random(6)` com retry em colisão (`unique`).

### File storage
- Usar disk `media` apontando para MinIO (driver `s3` do Laravel + `endpoint` custom).
- Em dev: MinIO local em Docker. Em prod: MinIO em VM/cluster próprio. Mesma codebase, só muda env.
- Conversão WebP via `intervention/image` v3 em job assíncrono (queue Valkey).
- URLs públicas via Laravel signed URLs (com expiração) — útil para vouchers e mídia sensível.
- **Para o modo offline:** Service Worker faz cache das URLs do MinIO. Em LAN do evento, MinIO pode rodar na mesma máquina do servidor Laravel — zero dependência de internet.

### Permissões
- `admin` — tudo.
- `operator` (operador do telão) — só `/screen/control`.
- `presenter` — só `/presenters`.
- `receptionist` — só `/reception` + presença.
- `guest` — auth via voucher; só dashboard + survey.

### Migração de dados
Comando Artisan `php artisan eventflow:migrate-from-json {path}`:
1. Lê `evento.json` → cria `Event`.
2. Lê `categorias.json` → cria `Category` (preserva `code` e `number`).
3. Lê `data/empresas/*.json` → cria `Company` + sincroniza pivot `CompanyCategory`.
4. Lê `slides.json` → cria `Slide`.
5. Lê `usuarios.json` → cria `User` (mantém hashes bcrypt; Laravel aceita).
6. Lê `pesquisa-criterios.json` → cria `SurveyCriterion`.
7. Lê `data/avaliacoes/*.json` → cria `SurveyResponse` + `SurveyAnswer`.
8. Copia `assets/uploads/{logos,videos,slides,evento}` para o disk configurado.
9. **(v2)** Para cada empresa com `confirmado=true` em v1, cria um `PresenceCheckin` sintético com `sequence=1` e `arrived_at = confirmado_em`; marca `Company.welcome_shown = true` (não enfileirar de novo). Aplica defaults `welcome_delay_seconds=5` e `welcome_duration_seconds=3` em `Event`.

---

## 25. Funcionalidade futura: modo offline

> Este é um requisito explicitamente mencionado para a próxima versão.

### Cenário
O dia do evento pode ter conexão instável. A operação **não pode** depender de Internet, especialmente:
- Telão precisa exibir logos e vídeos.
- Controle do telão precisa funcionar.
- Apresentador precisa ver a lista.
- Recepção precisa confirmar chegadas.

### Estratégia recomendada para a v2

**Modelo PWA + IndexedDB + Service Worker:**

1. **Pré-carga (download do "pacote do evento"):**
   - Endpoint `GET /api/v1/event/{id}/bundle` retorna manifest com:
     - Metadados (evento, categorias, empresas, slides, pesquisa).
     - URLs de **todas as logos**, **todos os vídeos**, **todos os slides**.
   - PWA, ao receber, faz cache via Service Worker (Workbox) em `cache-first`.
   - Indicador de progresso (`X/Y arquivos baixados`) e `pre-event-ready` flag local.

2. **Operação offline (fila local):**
   - Recepção/apresentadores/controle continuam respondendo a comandos locais.
   - Cada ação (confirmar, chamar, exibir) vai para uma fila persistente em IndexedDB.
   - Service Worker tenta replicar para a API quando conectividade voltar (`background sync`).
   - Conflitos (improváveis no caso de uso) resolvidos com last-writer-wins por timestamp.

3. **Sincronização em LAN:**
   - Em eventos onde o servidor é local (notebook do operador), basta uma rede local — sem Internet.
   - Reverb (WebSocket) roda na própria máquina/Docker; clientes na LAN se conectam.
   - Telão como página fullscreen no Chrome local apontando para `localhost`.

4. **Telão sempre offline-capable:**
   - Tela de espera, transições, animações: SPA renderiza tudo client-side.
   - Mídias servidas do cache do Service Worker.
   - Estado consumido via WebSocket (com fallback polling se desconectado).

### Tarefas do download
- Logo do evento.
- Todas as logos das empresas (atualmente 10, pode chegar a 200+).
- Todos os vídeos de empresas (cada um pode passar de 50 MB).
- Todos os slides (imagens + vídeos).
- Fontes Google Fonts → empacotar local (no `public/fonts/`).

### UI necessária
- Tela "Preparar para o evento" no painel admin:
  - Botão "Baixar tudo para uso offline".
  - Status de cada categoria (Logos: 198/200 / Vídeos: 18/20).
  - Botão "Re-sincronizar" se houve mudanças.
- Indicador online/offline em todas as telas operacionais.
- Banner amarelo "Você está offline — ações serão sincronizadas quando conectar".

### Considerações
- Vídeos pesados → cota do navegador. Usar `navigator.storage.persist()` e alertar se tamanho total > 2 GB.
- Em iOS Safari, o cache pode ser purgado em background — recomendar Chrome ou app nativo (Capacitor) se a operação for crítica.

---

## 26. Checklist de paridade funcional

> Para garantir que a v2 atende tudo o que a v1 faz.

### Cadastros
- [ ] CRUD de evento (singleton com expansão multi-tenant).
- [ ] CRUD de categorias (com número e código).
- [ ] CRUD de empresas (com logo upload, vídeo upload, conversão WebP, cota, mesa, qtd_pessoas, observação, ativo).
- [ ] CRUD de slides especiais (imagem ou vídeo, ordem).
- [ ] Vinculação empresa↔categoria com flag "ganhou".
- [ ] Edição de senha do admin.
- [ ] Multi-usuário admin (com adição via UI — atualmente só hardcoded).
- [ ] Configuração `chamado_exibe_telao`.

### Operação
- [ ] Confirmar chegada (recepção).
- [ ] Cancelar chegada.
- [ ] Auto-disparo de boas-vindas no telão ao confirmar (na v2 vira **fila com delay e duração configuráveis** — ver seção 17).
- [ ] Chamar empresa (apresentador).
- [ ] Resetar chamado.
- [ ] Auto-projeção condicional ao chamar.
- [ ] Operador escolher empresa para projetar.
- [ ] Operador escolher slide para projetar.
- [ ] Operador disparar boas-vindas manualmente.
- [ ] Operador ocultar telão.
- [ ] Toggle "fixar slide".
- [ ] Toggle "ocultar exibidos".

### Totalização e fila de boas-vindas (v2 — ver seção 17)
- [ ] Auto-checkin no primeiro QR scan da empresa.
- [ ] Contador `X/Y` por empresa em todas as telas (recepção, apresentador, convidado, admin).
- [ ] Cada pessoa que escaneia incrementa o contador (idempotente por `device_id`).
- [ ] Posição na fila baseada no **primeiro** check-in da empresa.
- [ ] Recepção pode adicionar/remover check-in manualmente.
- [ ] Cancelar último check-in (desfazer scan acidental).
- [ ] Fila de boas-vindas com FIFO por `eligible_from`.
- [ ] Configuração `welcome_delay_seconds` (default 5s, range 1–60).
- [ ] Configuração `welcome_duration_seconds` (default 3s, range 1–30).
- [ ] Boas-vindas dispara apenas no **primeiro** check-in da empresa.
- [ ] Painel "Fila de boas-vindas" no controle do telão (pular / pausar / cancelar / repetir).
- [ ] Som de chegada opcional.
- [ ] Limite por capacidade — alerta sem bloquear quando excede.

### Reordenamento manual da fila (v2 — ver seção 17)
- [ ] Tela `/admin/queue` com drag-and-drop.
- [ ] Persistência via `display_order_override` por empresa.
- [ ] Ordenação efetiva: cota → `display_order_override` → `confirmed_at`.
- [ ] Reauth obrigatório (token de 5 min em cookie HttpOnly).
- [ ] Botão "Resetar ordem" (limpa todos os overrides).
- [ ] Broadcast `QueueReordered` para atualização em tempo real.

### Telão
- [ ] Tela de espera com logo do evento.
- [ ] Tela de empresa com logo quadrada.
- [ ] Tela de empresa com logo retangular.
- [ ] Tela de empresa com vídeo.
- [ ] Tela de slide especial.
- [ ] Tela de boas-vindas com animação.
- [ ] Background com logo borrada.
- [ ] Partículas decorativas.
- [ ] Responsivo a múltiplas proporções (16:9, 16:10, 4:3).

### Convidado
- [ ] Login por voucher (form).
- [ ] Login por QR (querystring).
- [ ] Logout.
- [ ] Banner de boas-vindas.
- [ ] Banner de chamada (quando empresa do convidado é chamada).
- [ ] Lista de ordem de apresentação com destaque na própria empresa.
- [ ] Pesquisa de satisfação com 6 critérios em estrelas.
- [ ] Limite de avaliações por empresa = `qtd_pessoas`.

### Voucher
- [ ] Geração automática.
- [ ] Página imprimível com QR + código.
- [ ] Código alfabeto seguro (sem caracteres ambíguos).

### Relatórios
- [ ] Relatório de Ganhadores (ordenação por cota e mesa).
- [ ] Relatório de Ordem de Chegada.
- [ ] Print otimizado para PDF.
- [ ] Painel de avaliações (médias por critério, status por empresa).

### Importação
- [ ] Import via JSON do painel (categorias e empresas ganhadoras).
- [ ] Import de CSV via comando.
- [ ] Geração de vouchers em lote.
- [ ] Vinculação de logos por similaridade de nome.

### Backup
- [ ] Criar backup completo de dados.
- [ ] Listar backups.
- [ ] Restaurar backup.
- [ ] Download como ZIP.
- [ ] Deletar backup.

### Reset
- [ ] Resetar chegadas (com contador).
- [ ] Resetar chamados (com contador).
- [ ] Resetar telão.
- [ ] Resetar tudo.

### Identidade visual
- [ ] Paleta dourado/dark.
- [ ] Tipografia Cinzel/Playfair/Outfit.
- [ ] Componentes: glass-card, badges de cota, gradientes, animações.

### Novidades planejadas para v2 (além da paridade)
- [ ] Modo offline completo (PWA + IndexedDB + cache de mídia).
- [ ] Multi-tenant (vários clientes, vários eventos).
- [ ] Multi-perfil (admin, operador, apresentador, recepcionista).
- [ ] Tempo real via WebSocket (sem polling).
- [ ] Auditoria de ações.
- [ ] Export estruturado de dados (XLSX/CSV).
- [ ] Cronograma do evento (timeline com ações automáticas).
- [ ] Suporte a múltiplos critérios de pesquisa configuráveis pelo admin.
- [ ] Recurso de "reapresentar" (replay) — re-projetar empresa já exibida.
- [ ] Comunicação push para o convidado quando sua empresa for chamada.
- [ ] Logs de acesso por voucher (auditoria).
- [ ] Rate limiting em endpoints públicos.
- [ ] CSRF protection.
- [ ] Cookies HttpOnly + refresh tokens (substitui o `localStorage` AES).

---

## Apêndice A — Resumo dos endpoints (atual)

| Método | Endpoint | Auth | Descrição |
|---|---|---|---|
| GET | `/` | — | Redirect para `/admin` |
| GET | `/admin` | sessão | Dashboard |
| GET/POST | `/admin/login` | — | Login |
| GET | `/admin/logout` | sessão | Logout |
| GET | `/admin/empresas` | sessão | Listagem |
| GET/POST | `/admin/empresas/nova` | sessão | Cadastrar |
| GET/POST | `/admin/empresas/editar?codigo=` | sessão | Editar |
| GET | `/admin/empresas/deletar?codigo=` | sessão | Deletar |
| GET | `/admin/empresas/voucher?codigo=` | sessão | Voucher imprimível |
| GET | `/admin/categorias` | sessão | Listagem |
| GET/POST | `/admin/categorias/nova` | sessão | Cadastrar |
| GET/POST | `/admin/categorias/editar?codigo=` | sessão | Editar |
| GET | `/admin/categorias/deletar?codigo=` | sessão | Deletar |
| GET/POST | `/admin/slides` | sessão | CRUD único |
| GET | `/admin/avaliacoes` | sessão | Painel pesquisa |
| GET/POST | `/admin/importar` | sessão | Importar |
| GET | `/admin/relatorio[?tipo=chegada]` | sessão | Relatório |
| GET/POST | `/admin/backup` | sessão | Backup CRUD + download |
| GET/POST | `/admin/configuracoes` | sessão | Config + reset |
| GET | `/recepcao` | aberto | Tela recepção |
| GET | `/apresentadores` | aberto | Tela apresentador |
| GET | `/telao` | aberto | Telão (projeção) |
| GET | `/telao/controle` | aberto | Controle do telão |
| GET/POST | `/convidado[?v=]` | voucher | Login + dashboard |
| GET | `/convidado/pesquisa` | voucher | Avaliação |
| GET | `/documentacao` | aberto | Documentação interna |
| GET | `/api/empresas` | aberto | Lista enriquecida |
| GET/POST | `/api/presenca` | aberto | Confirmar/cancelar |
| POST | `/api/chamado` | aberto | Chamar/resetar |
| GET/POST | `/api/telao` | aberto | Estado + ações |
| GET | `/api/avaliacao` | sessão admin | Listar |
| POST | `/api/avaliacao` | voucher | Submeter |

> Nota crítica: as APIs `/api/presenca`, `/api/chamado` e `/api/telao` estão **abertas** (sem autenticação). Em uma rede pública isso é um risco — qualquer um pode confirmar presenças, chamar empresas e mexer no telão. A v2 deve exigir token operacional ou IP whitelist por evento.

---

## Apêndice B — Fluxo end-to-end (operação real do evento)

```
[Pré-evento — 1–7 dias antes]
  Admin loga → Configurações → preenche evento (nome, ano, cidade, logo PNG)
  Admin → Categorias → importa via JSON (222 categorias)
  Admin → Empresas → cria/importa empresas, atribui categorias, marca ganhadoras
  Admin → Empresa form → upload logo (vira WebP), opcionalmente vídeo, define mesa, qtd_pessoas, cota
  Admin → Empresa voucher → imprime voucher de cada mesa (QR + código)
  Admin → Slides Especiais → cadastra patrocinadores avulsos
  Admin → Backup → cria backup pré-evento

[Dia do evento — montagem]
  Equipe técnica projeta /telao no projetor (cursor escondido)
  Operador abre /telao/controle no notebook lateral
  Recepcionistas abrem /recepcao no tablet/celular

[Operação — chegadas]
  Convidado chega na mesa → recepcionista busca pelo nome → tap "Chegou"
  → API marca confirmado=true
  → side effect: telao-state passa para "boas_vindas" desta empresa
  → Telão (em poll de 2s) detecta a mudança → exibirBoasVindas() (animação)
  → Após uns segundos, operador do telão "oculta" → volta à espera

  Convidado escaneia QR Code do voucher na mesa → entra em /convidado
  → vê banner "Bem-vindo, [empresa] - Mesa X"
  → vê lista de ordem de apresentação atualizada em tempo (semi)-real

[Operação — abertura]
  Operador → /telao/controle → escolhe "Slide Patrocinador" → exibirSlide
  Apresentador → /apresentadores → vê todos os confirmados ordenados
  Apresentador → toca "Chamar" na próxima empresa (pelo grupo: convidados → cotas → ganhadores)
  → Se evento.chamado_exibe_telao=true → telão exibe automático
  → Senão, operador no /telao/controle escolhe quando exibir
  Telão exibe a empresa (com animação cinematográfica)
  Pessoa da empresa sobe ao palco
  Apresentador apresenta. Operador exibe próximo.

[Operação — pós-cerimônia]
  Convidados em cada mesa avaliam o evento via /convidado/pesquisa
  → 1 avaliação por pessoa (limite = qtd_pessoas da mesa)

[Pós-evento]
  Admin → /admin/avaliacoes → vê médias e taxa de resposta
  Admin → /admin/relatorio → imprime relatório de chegada e ganhadores
  Admin → /admin/backup → cria backup final
  Admin → /admin/configuracoes → "Resetar Tudo" antes da próxima edição
```

---

## Apêndice C — Constantes e configurações sensíveis encontradas (para revisar na reescrita)

| Item | Local | Risco |
|---|---|---|
| Senha default `#Adtg$1020` (Miqueias) | `app/auth.php:63` | Hard-coded |
| Senha default `CDL@@9080` (Clayton) | `app/auth.php:69` | Hard-coded |
| Chave AES `M3lh0r3s#V@l3nc@2025!` | `admin/login.php:83` | Pública no JS |
| `BASE_URL` produção fixa | `config.php:23` | Acoplado ao domínio |
| `APP_ENV` hardcoded | `config.php:6` | Sem env-based config |
| `chamado_exibe_telao` default | `data/evento.json` | Configuração mistura runtime e identidade |
| Caminho de backups `../backups` | `admin/backup.php:5` | Relativo, pode quebrar fora de Apache |

Todos esses pontos devem ser sanados na v2:
- Senhas → seeders apenas em ambiente de desenvolvimento.
- AES → remover; usar passkeys ou só "lembrar email".
- BASE_URL → `config('app.url')`.
- APP_ENV → `.env` + `APP_ENV` Laravel.
- Configurações operacionais → tabela `event_settings` ou `Spatie\LaravelSettings`.
- Backups → disk `s3-backups` ou `local-backups` do Laravel.

---

## Apêndice D — Mapas mentais úteis para a refatoração

### Domínio (DDD lite)

```
Bounded Context: EventOperation
  Aggregate Root: Event
    Entities: Category, Company, Slide
    Value Objects: VoucherCode, Tier, ScreenState
  Aggregate Root: SurveyResponse
    Entities: SurveyAnswer

Bounded Context: Identity
  Aggregate Root: User
  Aggregate Root: GuestSession (transient — voucher-based)

Bounded Context: Media
  Aggregate Root: MediaAsset
  Services: ImageProcessor, VideoTranscoder (futuro)

Bounded Context: Backup
  Aggregate Root: BackupArchive
```

### Serviços de aplicação

- `EventService::createEvent`, `setActiveEvent`, `resetState`, `resetArrivals`, `resetCalls`.
- `CompanyService::confirmArrival(companyId)` — atualiza + publica eventos.
- `CompanyService::call(companyId)` — atualiza + publica eventos.
- `ScreenService::showCompany(eventId, companyId, kind)` — kind ∈ {company, welcome}.
- `ScreenService::showSlide(eventId, slideId)`.
- `ScreenService::hide(eventId)`.
- `VoucherService::generateUnique(eventId)`.
- `SurveyService::submit(companyId, criteriaScores)`.
- `BackupService::create(eventId)`, `restore(archiveId)`, `download(archiveId)`.

### Eventos de domínio (broadcast)

- `event.{eventId}.companies` → `CompanyArrived`, `CompanyArrivalCancelled`, `CompanyCalled`, `CompanyCallReset`, `CompanyUpdated`.
- `event.{eventId}.screen` → `ScreenChanged{ kind, payload }`.

---

**Fim do documento.**

> Este estudo é vivo. À medida que a reescrita avançar, atualize as seções 17 e 24–26 com decisões tomadas (e mantenha o checklist 26 como guia de aceitação).
