# Citrus — Sistema de Gestão de Diárias
## Spec 5: Dashboard (KPIs, Gráficos e Painel de Alertas) — Documento de Design

| Campo | Valor |
| :---- | :---- |
| Projeto | Citrus Engenharia — Sistema de Gestão de Diárias |
| Spec | 5 de N (Dashboard) |
| Data | 2026-06-19 |
| Stack | Docker + PHP 8.2 + Yii2 + MySQL 8.0 + Bootstrap5 + Chart.js (CDN) |
| Branch | `citrus-dashboard` |
| Requisitos-fonte | `Citrus_Requisitos_v1.0.docx.md` §7 |
| Depende de | Spec 1 (Obra, Profissional, Funcao, BaseActiveRecord, PermissaoBehavior), Spec 2 (Efetivo/EfetivoDia), Spec 4 (Card, Quinzena) |

---

## 0. Contexto e escopo

O dashboard é a tela inicial de gestão: um panorama de leitura sobre os dados já existentes
(obras, profissionais, efetivo, cards). Não cria tabelas de negócio nem altera dados — apenas agrega
e exibe.

O módulo de **Relatórios** (§8 do doc) **NÃO** faz parte deste spec — foi separado para um spec próprio
(Spec 6), por ter um motor de SQL dinâmico com complexidade e segurança próprias.

**No escopo:** 4 KPIs, gráfico de barras (efetivo/dia, 30 dias), gráfico de rosca (distribuição de
diárias por obra no mês), painel de alertas com as 6 regras do §7.2, badge de total de alertas no menu,
acesso por permissão, dashboard como home pós-login.

**Fora do escopo:** relatórios dinâmicos (Spec 6), filtros de período no dashboard (sempre mês/quinzena
corrente), calendário de feriados (dias úteis = exclui apenas sábado/domingo na v1), exportação do
dashboard.

Decisões do brainstorming:
- **Gráficos via Chart.js (CDN)** — carregado só na view do dashboard (`registerJsFile`), dados injetados como JSON.
- **Dashboard é a home pós-login** para quem tem permissão; demais seguem o fluxo atual.
- **As 6 regras de alerta** implementadas; o total alimenta um **badge** no menu lateral.
- **Layout A:** linha de KPIs → dois gráficos lado a lado (barras maior + rosca) → painel de alertas em largura cheia abaixo. No mobile, seções empilhadas.
- `main.php` (menu) **não é alterado** por este spec (trabalho não-commitado do desenvolvedor): o item de menu e o badge são entregues como **trechos** no plano.

---

## 1. Arquitetura

Dois serviços de leitura sem estado, um controller e a view. Sem novas tabelas de negócio (apenas uma
migration que semeia a permissão da nova tela).

- **`app\components\DashboardService`** — calcula os 4 KPIs e os datasets dos 2 gráficos. Métodos puros de leitura, recebendo a "data de referência" para serem testáveis.
- **`app\components\AlertaService`** — avalia as 6 regras e devolve a lista de alertas + total.
- **`app\controllers\DashboardController`** — `actionIndex`, gateado por `PermissaoBehavior` (tela `dashboard`). Monta KPIs e alertas, injeta os dados dos gráficos como JSON na view.
- **`web/js/dashboard.js`** — inicializa o Chart.js lendo o JSON embutido. Sem endpoint extra.

Por que serviços (e não métodos estáticos espalhados): concentram as queries de agregação num lugar
testável, com uma única responsabilidade cada, sem inflar os models de domínio.

---

## 2. KPIs (§7.1)

`DashboardService` expõe um método por KPI (todos recebem `\DateTimeInterface $ref` com default "hoje"):

| KPI | Método | Cálculo |
| :-- | :-- | :-- |
| Obras ativas | `obrasAtivas()` | `Obra::find()->where(['status' => 'ativo'])->count()` |
| Profissionais ativos | `profissionaisAtivos()` | `Profissional::find()->where(['status' => 'ativo'])->count()` |
| Diárias no mês | `diariasNoMes($ref)` | nº de registros de `Efetivo` cujo `efetivo_dia.data` cai no mês de `$ref` (via `joinWith('efetivoDia')`) |
| Cards pendentes | `cardsPendentes($ref)` | `Card` com `status = rascunho` na quinzena corrente de `$ref` (deriva a quinzena de `$ref` por dia ≤15 → Q1, senão Q2) |

Soft-delete já é respeitado pelo `SoftDeleteQuery` padrão. "No mês" usa o primeiro e último dia do
mês de `$ref`.

---

## 3. Gráficos

`DashboardService` devolve arrays prontos para o Chart.js:

- **`efetivoPorDia($ref, $dias = 30)`** → `['labels' => ['01/06', ...], 'valores' => [n, ...]]`. Conta registros de `Efetivo` agrupados por `efetivo_dia.data`, dos últimos `$dias` dias corridos terminando em `$ref` (dias sem registro entram com `0`). Gráfico de **barras**.
- **`distribuicaoPorObra($ref)`** → `[['obra' => 'Nome', 'qtd' => n, 'pct' => 12.5], ...]`. Conta diárias (registros de `Efetivo`) por obra no mês de `$ref`; `pct` é a fração do total (soma dos `pct` = 100, com a sobra de arredondamento na maior fatia). Gráfico de **rosca**.

A view serializa esses arrays com `json_encode` num `<script>`/data-attribute; `dashboard.js` lê e
desenha. Paleta: lime `#C0F024`, verde `#9CCC6C`, ink `#0C0C0C` e tons derivados para fatias extras.

---

## 4. Painel de alertas (§7.2)

`AlertaService::todos($ref): array` devolve uma lista de alertas. Cada item:

```php
[
  'tipo'       => 'registro_sem_saida',          // chave estável
  'severidade' => 'critico',                      // 'critico' | 'atencao'
  'titulo'     => 'Registro sem hora de saída',
  'mensagem'   => '2 registros com entrada e sem saída há mais de 24h.',
  'itens'      => [ /* descrições curtas dos afetados, p/ listar */ ],
]
```

Só entram na lista os alertas com pelo menos um caso. `AlertaService::total($ref): int` =
nº de alertas ativos (para o badge).

As 6 regras:

| # | tipo | severidade | Condição |
| :- | :-- | :-- | :-- |
| 1 | `obra_sem_efetivo` | atencao | Obra ativa sem nenhum `Efetivo` com `data` ≥ (3 dias úteis atrás a partir de `$ref`) |
| 2 | `profissional_sem_funcao` | atencao | Profissional ativo com `funcao_id` nulo |
| 3 | `profissional_sem_registro` | atencao | Profissional ativo sem `Efetivo` com `data` ≥ (5 dias úteis atrás) |
| 4 | `card_rascunho_quinzena_encerrada` | critico | Há `Card` em `rascunho` referente a uma quinzena **já encerrada** (intervalo da quinzena terminou antes de `$ref`) |
| 5 | `registro_sem_saida` | critico | `Efetivo` com `hora_entrada` preenchida, `hora_saida` nula e `efetivo_dia.data` há mais de 24h |
| 6 | `lider_sem_obra` | atencao | Profissional ativo com perfil **Líder** que não é `lider1`/`lider2` de nenhuma obra ativa |

**Dias úteis:** helper `AlertaService::dataDiasUteisAtras(int $n, $ref): string` que recua `$n` dias úteis
(pula sábado/domingo) e devolve `Y-m-d`. Sem calendário de feriados na v1.

**Perfil Líder (regra 6):** `Profissional` guarda perfis numa coluna CSV (`perfis`); usar
`Profissional::temPerfil('lider')` (helper já existente). "Obra vinculada" = o profissional aparecer em
`obra.lider1_id` ou `obra.lider2_id` de alguma obra ativa.

---

## 5. Acesso, home e menu

- **Permissão:** migration `mYYMMDD_NNNNNN_seed_dashboard_permissao.php` insere na tabela `permissao` (colunas `perfil`, `tela`, `acoes`) duas linhas — `{perfil: administrativo, tela: dashboard, acoes: 'index,view'}` e `{perfil: gerente, ...}` —, seguindo o padrão da migration `m260618_000005_create_permissao_table.php`. Sem permissão → 403 pelo `PermissaoBehavior` (que consulta `Permissao::permite($user->identity, $tela, $acao)`).
- **Home pós-login:** `SiteController::actionIndex` passa a redirecionar usuário **logado e com permissão na tela `dashboard`** para `dashboard/index`. Visitantes e usuários sem a permissão seguem renderizando o `index` atual. (`SiteController` não é arquivo do desenvolvedor; pode ser editado.)
- **Menu + badge (entregues como trecho, não committados):** o plano traz o snippet do item de menu "Dashboard" (`['dashboard/index']`) e do **badge** com `AlertaService::total()` para o desenvolvedor colar no `main.php`.

---

## 6. Layout (Layout A)

- **Linha de KPIs:** 4 cards. Desktop em linha; mobile 2×2. O card "Cards pendentes" recebe destaque quando > 0.
- **Gráficos:** desktop lado a lado (barras ~2/3, rosca ~1/3); mobile empilhados.
- **Painel de alertas:** largura cheia abaixo dos gráficos. Lista com bolinha de severidade (crítico vermelho, atenção âmbar), título e mensagem; quando vazio, estado "Nenhuma anomalia detectada".
- Estilos em `web/css/app.css` (arquivo compartilhado), seguindo os tokens/cards já existentes.

---

## 7. Testes (Codeception, contra `citrus_test`)

- **Unit (`DashboardService`):** cada KPI com fixtures conhecidas (obras/profissionais ativos x inativos; diárias dentro/fora do mês; cards rascunho na quinzena); `efetivoPorDia` agrupa por dia e preenche dias vazios com 0; `distribuicaoPorObra` soma 100% e agrupa por obra.
- **Unit (`AlertaService`):** cada uma das 6 regras com cenário que dispara e cenário que não dispara; `dataDiasUteisAtras` pulando fim de semana; `total()` conta só alertas ativos.
- **Functional (`DashboardCest`):** `dashboard/index` retorna 200 para perfil com permissão e **403** sem; os números dos KPIs e as mensagens de alerta aparecem na página; o JSON dos gráficos está presente; usuário logado com permissão é redirecionado de `site/index` para o dashboard.

---

## 8. Arquivos

- Serviços: `components/DashboardService.php`, `components/AlertaService.php`.
- Controller: `controllers/DashboardController.php`.
- Views: `views/dashboard/index.php` (+ parciais `_kpis.php`, `_alertas.php`).
- JS: `web/js/dashboard.js` (init Chart.js).
- CSS: estilos do dashboard em `web/css/app.css`.
- Migration: `migrations/mYYMMDD_NNNNNN_seed_dashboard_permissao.php`.
- Edição: `controllers/SiteController.php` (redirect da home).
- Trechos entregues (não committados): item de menu + badge no `main.php`.

---

## 9. Considerações

- Tudo é leitura; nenhuma rota altera dados. As queries respeitam soft-delete pelo `SoftDeleteQuery`.
- Chart.js entra só na view do dashboard, sem inflar o `AppAsset` global.
- Próximo (Spec 6): Módulo de Relatórios dinâmicos via SQL (§8) — motor de queries cadastradas, render automático por tipo de coluna e export Excel.
