# API do AgendAI (leitura) — Horas dos consultores

Guia de uso (login, convenções, erros): https://api-docs.agendai.pro/md/guia.md


Horas realizadas pelos consultores (BI Produtividade).

- `GET /bi-produtividade/por-consultor` — Horas realizadas por pessoa e por mês, com meta individual
- `GET /bi-produtividade/horas-por-mes` — Horas realizadas por mês, com meta e produtividade da equipe
- `GET /bi-produtividade/horas-por-tipo-servico` — Horas realizadas por ano e tipo de serviço
- `GET /bi-produtividade/porcentagem-por-tipo-servico` — Participação de cada tipo de serviço no número de agendas realizadas
- `GET /bi-produtividade/horas-por-tipo-agenda` — Horas realizadas por pessoa e tipo de agenda
- `GET /bi-produtividade/tabela-total-horas` — Tabela simples de horas realizadas por mês (sem pessoa, sem meta)
- `GET /bi-produtividade/horas-realizadas-por-consultor` — Total de horas realizadas por pessoa no período inteiro
- `GET /bi-produtividade/grafico-mes-atual` — Horas realizadas por pessoa no mês corrente

### GET /bi-produtividade/por-consultor

**Horas realizadas por pessoa e por mês, com meta individual** · operationId `listarHorasPorConsultorPorMes`

Autenticação: `Authorization: Bearer <token>`.

Responde: "quantas horas cada pessoa realizou em cada mês do período e qual era a meta dela?". Devolve **uma linha por (mês, pessoa)**, ordenada pela data da primeira agenda da pessoa no mês (crescente; os meses ficam em ordem cronológica), depois nome e id da pessoa.

**Cálculo.** Soma `duracao` (minutos) das agendas REALIZADAS agrupadas por mês da data de referência e por pessoa (responsável da agenda; se a agenda foi reservada num consultor sem usuário, o próprio consultor). A meta individual é `metaTotalHoraMes x 60 x fator` minutos, onde `metaTotalHoraMes` vem da tabela de horas úteis da organização (horas por pessoa no mês) e o fator é 1 para pessoa ativa, no mês da desativação a fração = horas úteis do dia 1 até a desativação ÷ horas úteis do mês (1 nos meses anteriores, 0 nos posteriores e para inativo sem data de desativação) e 0 para quem está fora da base da meta (nesse caso `minutosMetaConsultor` é nulo, mas as horas realizadas continuam aparecendo). Sem filtro de pessoa, só conta na meta quem está hoje no setor Consultoria; com `usuarioIds`/`setor`, as pessoas filtradas (em ambos os casos restritas pela `situacao`). Só com o filtro `consultor` (nome), toda linha recebe fator 1 (meta cheia) e `quantidadeConsultores` = número de nomes distintos.

**Diferença para as vizinhas:** esta é a única rota por pessoa **x mês** com **meta**. `horas-realizadas-por-consultor` dá só o total do período por pessoa (sem meta); `horas-por-mes` dá o total do mês sem separar pessoas; `grafico-mes-atual` dá só o mês corrente.

**Qual rota de horas chamar (as quatro parecem iguais, mas respondem perguntas diferentes):**

| Rota | Agrupa por | Devolve | Quando usar |
|---|---|---|---|
| `GET /bi-produtividade/horas-por-mes` | mês (total da organização/filtro) | minutos, `HH:MM`, **meta e produtividade** | evolução mensal e atingimento da meta |
| `GET /bi-produtividade/por-consultor` | mês **x** pessoa | minutos, `HH:MM`, **meta individual**, cor | evolução mensal por pessoa com meta |
| `GET /bi-produtividade/horas-realizadas-por-consultor` | pessoa (período inteiro) | só `HH:MM` (sem minutos, sem meta) | ranking/total de horas por pessoa no período |
| `GET /bi-produtividade/grafico-mes-atual` | pessoa, **só o mês corrente** | só `HH:MM` | "quanto cada pessoa já realizou NESTE mês"; ignora `dataInicio`/`dataFim` |
| `GET /bi-produtividade/tabela-total-horas` | mês (total, **sem pessoa**) | só `HH:MM`, mês em inglês | tabela simples de horas por mês, sem meta |
| `GET /bi-produtividade/horas-por-tipo-agenda` | pessoa **x** tipo de agenda | minutos e `HH:MM` | em que tipo de agenda (cliente, interno, férias...) cada pessoa gastou o tempo |
| `GET /bi-produtividade/horas-por-tipo-servico` | ano **x** tipo de serviço | só `HH:MM` | distribuição de horas por serviço |
| `GET /bi-produtividade/porcentagem-por-tipo-servico` | tipo de serviço | contagem de agendas e % | participação (rosca) por serviço, em NÚMERO DE AGENDAS, não em horas |


**Regras comuns a todas as rotas `/bi-produtividade/*` de horas:**

- Conta **somente agendas com `status = REALIZADO`** da organização do token (isolamento por tenant). Agendas previstas, confirmadas, reservadas, pendentes ou canceladas ficam de fora. Agendas excluídas não entram.
- A **duração** vem do campo `duracao` da agenda, em **MINUTOS inteiros**. As rotas que devolvem `horas` o fazem como **texto `HH:MM`** (horas totais, pode passar de 24, ex.: `"152:30"`), NUNCA como número decimal.
- A **data de referência** de cada agenda é `COALESCE(dataFim, dataInicio)` (a data de término, ou o início se não houver fim), em **hora de parede gravada como UTC** (agenda digitada às 09:00 está armazenada como `T09:00:00Z`; não há conversão de fuso). Exceção: `porcentagem-por-tipo-servico` usa `dataInicio`.
- Período padrão: sem `dataInicio` e sem `dataFim`, o **ano civil atual**.
- **Permissão:** exige apenas token válido (JWT) e organização identificada. Não há `@Roles`, checagem de tela nem escopo de dados por usuário: qualquer usuário autenticado vê as horas de **todas** as pessoas da organização, sem filtro por quem pergunta.
- Os filtros são lidos direto da query string, sem schema Zod: parâmetros desconhecidos são ignorados. Só falham com 400: `situacao` inválida, `usuarioIds` com UUID inválido (ou mais de 200) e `setor` desconhecido.

#### Parâmetros

| Nome | Onde | Tipo | Obrigatório | Descrição |
|---|---|---|---|---|
| `dataInicio` | query | string | não | Início do período (inclusivo). Aceita qualquer texto que `Date.parse` leia; recomendado `AAAA-MM-DD` (vira `T00:00:00Z`). Valor ilegível é IGNORADO em silêncio (sem erro). Use `AAAA-MM-DD` com data válida; texto que o JavaScript aceita mas o Postgres não (por exemplo `2026-02-30`) pode dar 500. Compara com a data da agenda em hora de parede (ver description da rota). Ex.: `2026-01-01` |
| `dataFim` | query | string | não | Fim do período (inclusivo). Com `AAAA-MM-DD` vira `T23:59:59Z`. Valor ilegível é ignorado em silêncio; data que o JavaScript aceita mas o Postgres não (por exemplo `2026-02-30`) pode dar 500. Se NEM `dataInicio` NEM `dataFim` forem enviados, o período é o ANO CIVIL ATUAL do servidor (01/jan a 31/dez); se só um vier, o intervalo fica aberto do outro lado. Ex.: `2026-12-31` |
| `usuarioIds` | query | lista de string (uuid) | não | Filtra por pessoa (usuário responsável da agenda). UUIDs (maiúsculas são aceitas e convertidas), repetindo o parâmetro (`?usuarioIds=a&usuarioIds=b`) ou separados por vírgula. Máximo de 200 ids. Só entram usuários da organização do token; UUID malformado dá 400. Com este filtro (ou `setor`), a base da meta (`horas-por-mes`) passa a ser as pessoas filtradas. Ex.: `["c0a80101-0000-4000-8000-000000000001"]` |
| `setor` | query | lista de string — um de: `CONSULTORIA`, `PROJETOS`, `COMERCIAL`, `FINANCEIRO`, `MIGRACAO`, `CS` | não | Filtra pelo setor efetivo do responsável da agenda. Valores (sem diferenciar maiúsculas): CONSULTORIA, PROJETOS, COMERCIAL, FINANCEIRO, MIGRACAO, CS. Repetido ou separado por vírgula. Se vier junto com `usuarioIds`, vale a interseção; um setor sem ninguém devolve lista vazia. Valor desconhecido dá 400 ("setor inválido."). Ex.: `["CONSULTORIA"]` |
| `consultor` | query | string[] | não | Filtro legado por NOME: casa o nome do consultor cadastrado OU o nome do usuário responsável (igualdade exata, diferencia maiúsculas). Aceita vários nomes (repetido ou separado por vírgula). Prefira `usuarioIds`. Ex.: `["Consultor Exemplo"]` |
| `situacao` | query | string — um de: `ativos`, `inativos`, `todos`; padrão `"todos"` | não | Situação atual da pessoa responsável: `ativos` (usuário com status diferente de false; sem usuário, o consultor ativo), `inativos` ou `todos`. Ausente ou vazio = `todos`. Outro valor dá 400. Ex.: `ativos` |
| `tipoServico` | query | string[] | não | Filtra pelo NOME do tipo de projeto/serviço (`tipo_projeto.tipoProjeto`, via projeto da agenda), igualdade exata. Repetido ou separado por vírgula. Atenção: filtra o tipo de projeto, não a coluna `tipoServico` da agenda que as rotas `horas-por-tipo-servico` e `porcentagem-por-tipo-servico` devolvem. Ex.: `["Implantação"]` |
| `produto` | query | string[] | não | Filtra pelo NOME do produto do projeto da agenda (`produto.produto`), igualdade exata. Repetido ou separado por vírgula. Ex.: `["Produto Alfa"]` |
| `documento` | query | string[] | não | Filtra pelo número do documento do registro financeiro do projeto (`registro_financeiro.documento`), igualdade exata. Repetido ou separado por vírgula. Ex.: `["DOC-0001"]` |
| `tipoAgenda` | query | lista de string — um de: `INTERNO`, `CLIENTE`, `FERIAS`, `ESTUDO`, `PESSOAL`, `COMERCIAL`, `CS` | não | Filtra pelo tipo da agenda. `CLIENTE` também casa agendas sem tipo (nulo ou vazio). Sem este parâmetro NÃO há filtro de tipo (entram todos os tipos). Valor fora do enum provavelmente quebra a consulta (500; inferido, não validado). Ex.: `["CLIENTE"]` |

#### Resposta 200

Sucesso.

Tipo da raiz: HorasPorConsultorMesItem[]

| Campo | Tipo | Sempre presente | Descrição |
|---|---|---|---|
| `[].ano` | integer | sim |  |
| `[].mesNumero` | integer | sim |  |
| `[].usuarioId` | string (uuid) \| null | sim | Usuário responsável da linha. Nulo quando a agenda está reservada num consultor que não tem usuário vinculado. |
| `[].consultor` | string | sim | Nome da pessoa (nome do usuário; reserva no nome do consultor). Pode repetir entre pessoas diferentes homônimas: distinga por `usuarioId`. |
| `[].cor` | string \| null | sim | Cor hexadecimal da pessoa no calendário. |
| `[].mes` | string | sim | Rótulo `Mon. de aaaa` com mês em inglês. |
| `[].minutosRealizados` | integer | sim | Minutos REALIZADOS da pessoa no mês. |
| `[].horas` | string | sim | `minutosRealizados` em `HH:MM`. |
| `[].metaTotalHoraMes` | number \| null | sim | Meta mensal de horas POR PESSOA (tabela de horas úteis). Nulo sem cadastro. |
| `[].quantidadeConsultores` | number | sim | Pessoas na meta do mês (mesmo valor de `consultoresNaMeta` de `horas-por-mes`), 2 casas. 0 quando não há ninguém na base (aí `minutosMetaConsultor` fica nulo). |
| `[].minutosMetaConsultor` | integer \| null | sim | Meta individual do mês em minutos = `metaTotalHoraMes` x 60 x fator da pessoa (1 ativa; fração = horas úteis do dia 1 até a desativação ÷ horas úteis do mês, só no mês da desativação; 1 nos meses anteriores, 0 nos posteriores e para inativo sem data de desativação). Nulo sem meta ou quando a pessoa está fora da meta (fator 0), mesmo que tenha horas realizadas. |
| `[].metaHorasConsultor` | string \| null | sim | `minutosMetaConsultor` em `HH:MM`. |

Exemplo (dados fictícios):

```json
[
  {
    "ano": 2026,
    "mesNumero": 3,
    "usuarioId": "5d1b6c2e-8f3a-4c71-9b0e-2a7d4e6f1a10",
    "consultor": "Consultor Exemplo",
    "cor": "#3B82F6",
    "mes": "Mar. de 2026",
    "minutosRealizados": 7200,
    "horas": "120:00",
    "metaTotalHoraMes": 160,
    "quantidadeConsultores": 10,
    "minutosMetaConsultor": 9600,
    "metaHorasConsultor": "160:00"
  }
]
```

#### Erros

- **400**: Parâmetros inválidos. `message` é uma lista com um item por problema, no formato `campo: motivo`.
- **401**: Token ausente, inválido, expirado ou de usuário desativado. Faça login de novo.

---

### GET /bi-produtividade/horas-por-mes

**Horas realizadas por mês, com meta e produtividade da equipe** · operationId `listarHorasPorMes`

Autenticação: `Authorization: Bearer <token>`.

Responde: "quantas horas a equipe (ou o recorte filtrado) realizou em cada mês e quanto isso representa da meta?". Devolve **uma linha por mês**, em ordem cronológica; lista vazia se não há agenda REALIZADA no período.

**Cálculo.** `minutosRealizados` = soma das durações REALIZADAS do mês. A meta do mês é `metaTotalHoraMes` (horas por pessoa, da tabela de horas úteis) x 60 x `consultoresNaMeta` (pessoas na base, com fração para desativados). `produtividade` = realizado / meta x 100 (percentual 0-100+, 2 casas). Sem filtro de pessoa a base é o setor Consultoria; com `usuarioIds`/`setor` são as pessoas filtradas; com `consultor` (nome), a base é o número de NOMES distintos informados (homônimos contam 1, sem fração de desativação) (`baseMeta` informa qual caso).

**Diferença para as vizinhas:** agrega o mês inteiro, **sem separar pessoas**; é a rota para atingimento de meta da equipe. Para a mesma conta por pessoa use `por-consultor`; para tabela simples sem meta, `tabela-total-horas`.

**Qual rota de horas chamar (as quatro parecem iguais, mas respondem perguntas diferentes):**

| Rota | Agrupa por | Devolve | Quando usar |
|---|---|---|---|
| `GET /bi-produtividade/horas-por-mes` | mês (total da organização/filtro) | minutos, `HH:MM`, **meta e produtividade** | evolução mensal e atingimento da meta |
| `GET /bi-produtividade/por-consultor` | mês **x** pessoa | minutos, `HH:MM`, **meta individual**, cor | evolução mensal por pessoa com meta |
| `GET /bi-produtividade/horas-realizadas-por-consultor` | pessoa (período inteiro) | só `HH:MM` (sem minutos, sem meta) | ranking/total de horas por pessoa no período |
| `GET /bi-produtividade/grafico-mes-atual` | pessoa, **só o mês corrente** | só `HH:MM` | "quanto cada pessoa já realizou NESTE mês"; ignora `dataInicio`/`dataFim` |
| `GET /bi-produtividade/tabela-total-horas` | mês (total, **sem pessoa**) | só `HH:MM`, mês em inglês | tabela simples de horas por mês, sem meta |
| `GET /bi-produtividade/horas-por-tipo-agenda` | pessoa **x** tipo de agenda | minutos e `HH:MM` | em que tipo de agenda (cliente, interno, férias...) cada pessoa gastou o tempo |
| `GET /bi-produtividade/horas-por-tipo-servico` | ano **x** tipo de serviço | só `HH:MM` | distribuição de horas por serviço |
| `GET /bi-produtividade/porcentagem-por-tipo-servico` | tipo de serviço | contagem de agendas e % | participação (rosca) por serviço, em NÚMERO DE AGENDAS, não em horas |


**Regras comuns a todas as rotas `/bi-produtividade/*` de horas:**

- Conta **somente agendas com `status = REALIZADO`** da organização do token (isolamento por tenant). Agendas previstas, confirmadas, reservadas, pendentes ou canceladas ficam de fora. Agendas excluídas não entram.
- A **duração** vem do campo `duracao` da agenda, em **MINUTOS inteiros**. As rotas que devolvem `horas` o fazem como **texto `HH:MM`** (horas totais, pode passar de 24, ex.: `"152:30"`), NUNCA como número decimal.
- A **data de referência** de cada agenda é `COALESCE(dataFim, dataInicio)` (a data de término, ou o início se não houver fim), em **hora de parede gravada como UTC** (agenda digitada às 09:00 está armazenada como `T09:00:00Z`; não há conversão de fuso). Exceção: `porcentagem-por-tipo-servico` usa `dataInicio`.
- Período padrão: sem `dataInicio` e sem `dataFim`, o **ano civil atual**.
- **Permissão:** exige apenas token válido (JWT) e organização identificada. Não há `@Roles`, checagem de tela nem escopo de dados por usuário: qualquer usuário autenticado vê as horas de **todas** as pessoas da organização, sem filtro por quem pergunta.
- Os filtros são lidos direto da query string, sem schema Zod: parâmetros desconhecidos são ignorados. Só falham com 400: `situacao` inválida, `usuarioIds` com UUID inválido (ou mais de 200) e `setor` desconhecido.

#### Parâmetros

| Nome | Onde | Tipo | Obrigatório | Descrição |
|---|---|---|---|---|
| `dataInicio` | query | string | não | Início do período (inclusivo). Aceita qualquer texto que `Date.parse` leia; recomendado `AAAA-MM-DD` (vira `T00:00:00Z`). Valor ilegível é IGNORADO em silêncio (sem erro). Use `AAAA-MM-DD` com data válida; texto que o JavaScript aceita mas o Postgres não (por exemplo `2026-02-30`) pode dar 500. Compara com a data da agenda em hora de parede (ver description da rota). Ex.: `2026-01-01` |
| `dataFim` | query | string | não | Fim do período (inclusivo). Com `AAAA-MM-DD` vira `T23:59:59Z`. Valor ilegível é ignorado em silêncio; data que o JavaScript aceita mas o Postgres não (por exemplo `2026-02-30`) pode dar 500. Se NEM `dataInicio` NEM `dataFim` forem enviados, o período é o ANO CIVIL ATUAL do servidor (01/jan a 31/dez); se só um vier, o intervalo fica aberto do outro lado. Ex.: `2026-12-31` |
| `usuarioIds` | query | lista de string (uuid) | não | Filtra por pessoa (usuário responsável da agenda). UUIDs (maiúsculas são aceitas e convertidas), repetindo o parâmetro (`?usuarioIds=a&usuarioIds=b`) ou separados por vírgula. Máximo de 200 ids. Só entram usuários da organização do token; UUID malformado dá 400. Com este filtro (ou `setor`), a base da meta (`horas-por-mes`) passa a ser as pessoas filtradas. Ex.: `["c0a80101-0000-4000-8000-000000000001"]` |
| `setor` | query | lista de string — um de: `CONSULTORIA`, `PROJETOS`, `COMERCIAL`, `FINANCEIRO`, `MIGRACAO`, `CS` | não | Filtra pelo setor efetivo do responsável da agenda. Valores (sem diferenciar maiúsculas): CONSULTORIA, PROJETOS, COMERCIAL, FINANCEIRO, MIGRACAO, CS. Repetido ou separado por vírgula. Se vier junto com `usuarioIds`, vale a interseção; um setor sem ninguém devolve lista vazia. Valor desconhecido dá 400 ("setor inválido."). Ex.: `["CONSULTORIA"]` |
| `consultor` | query | string[] | não | Filtro legado por NOME: casa o nome do consultor cadastrado OU o nome do usuário responsável (igualdade exata, diferencia maiúsculas). Aceita vários nomes (repetido ou separado por vírgula). Prefira `usuarioIds`. Ex.: `["Consultor Exemplo"]` |
| `situacao` | query | string — um de: `ativos`, `inativos`, `todos`; padrão `"todos"` | não | Situação atual da pessoa responsável: `ativos` (usuário com status diferente de false; sem usuário, o consultor ativo), `inativos` ou `todos`. Ausente ou vazio = `todos`. Outro valor dá 400. Ex.: `ativos` |
| `tipoServico` | query | string[] | não | Filtra pelo NOME do tipo de projeto/serviço (`tipo_projeto.tipoProjeto`, via projeto da agenda), igualdade exata. Repetido ou separado por vírgula. Atenção: filtra o tipo de projeto, não a coluna `tipoServico` da agenda que as rotas `horas-por-tipo-servico` e `porcentagem-por-tipo-servico` devolvem. Ex.: `["Implantação"]` |
| `produto` | query | string[] | não | Filtra pelo NOME do produto do projeto da agenda (`produto.produto`), igualdade exata. Repetido ou separado por vírgula. Ex.: `["Produto Alfa"]` |
| `documento` | query | string[] | não | Filtra pelo número do documento do registro financeiro do projeto (`registro_financeiro.documento`), igualdade exata. Repetido ou separado por vírgula. Ex.: `["DOC-0001"]` |
| `tipoAgenda` | query | lista de string — um de: `INTERNO`, `CLIENTE`, `FERIAS`, `ESTUDO`, `PESSOAL`, `COMERCIAL`, `CS` | não | Filtra pelo tipo da agenda. `CLIENTE` também casa agendas sem tipo (nulo ou vazio). Sem este parâmetro NÃO há filtro de tipo (entram todos os tipos). Valor fora do enum provavelmente quebra a consulta (500; inferido, não validado). Ex.: `["CLIENTE"]` |

#### Resposta 200

Sucesso.

Tipo da raiz: HorasPorMesItem[]

| Campo | Tipo | Sempre presente | Descrição |
|---|---|---|---|
| `[].ano` | integer | sim |  |
| `[].mesNumero` | integer | sim |  |
| `[].mes` | string | sim | Rótulo `Mon. de aaaa` com a abreviatura do mês em INGLÊS (Jan., Feb., Mar., Apr., May., Jun., Jul., Aug., Sep., Oct., Nov., Dec.), ex.: `Mar. de 2026`. Use `ano` e `mesNumero` para lógica; este texto é só rótulo. |
| `[].minutosRealizados` | integer | sim | Soma das durações das agendas REALIZADAS no mês, em minutos. |
| `[].hoursRealizadas` | string | sim | Mesmo valor de `minutosRealizados` em `HH:MM` (note o nome do campo: `hoursRealizadas`, com "s" e em inglês). |
| `[].metaTotalHoraMes` | number \| null | sim | Meta de horas POR PESSOA no mês, cadastrada na tabela de horas úteis da organização (`horas_uteis.meta_total_hora_mes`). Nulo se não há linha cadastrada para o mês. |
| `[].minutosMeta` | integer \| null | sim | Meta total do mês em minutos = `metaTotalHoraMes` x 60 x `consultoresNaMeta`, arredondado. Nulo se não há meta ou a base de pessoas é nula/zero. |
| `[].metaHoras` | string \| null | sim | `minutosMeta` em `HH:MM`. |
| `[].produtividade` | number \| null | sim | Percentual **0-100+** (pode passar de 100) = `minutosRealizados / minutosMeta x 100`, 2 casas. Nulo sem meta. |
| `[].consultoresNaMeta` | number | sim | Quantidade de pessoas que entram na meta do mês, com 2 casas (fração = horas úteis do dia 1 até a desativação ÷ horas úteis do mês, só no mês da desativação; 1 nos meses anteriores, 0 nos posteriores e para inativo sem data de desativação). 0 quando não há ninguém na base (aí `minutosMeta` fica nulo). |
| `[].baseMeta` | string — um de: `SETOR_CONSULTORIA`, `PESSOAS_FILTRADAS` | sim | `SETOR_CONSULTORIA`: sem filtro de pessoa, a base é o setor Consultoria. `PESSOAS_FILTRADAS`: a base são as pessoas dos filtros `usuarioIds`/`setor`; com `consultor` (nome), a base é o número de NOMES distintos informados (homônimos contam 1, sem fração de desativação). |

Exemplo (dados fictícios):

```json
[
  {
    "ano": 2026,
    "mesNumero": 3,
    "mes": "Mar. de 2026",
    "minutosRealizados": 9150,
    "hoursRealizadas": "152:30",
    "metaTotalHoraMes": 160,
    "minutosMeta": 96000,
    "metaHoras": "1600:00",
    "produtividade": 9.53,
    "consultoresNaMeta": 10,
    "baseMeta": "SETOR_CONSULTORIA"
  }
]
```

#### Erros

- **400**: Parâmetros inválidos. `message` é uma lista com um item por problema, no formato `campo: motivo`.
- **401**: Token ausente, inválido, expirado ou de usuário desativado. Faça login de novo.

---

### GET /bi-produtividade/horas-por-tipo-servico

**Horas realizadas por ano e tipo de serviço** · operationId `listarHorasPorTipoServico`

Autenticação: `Authorization: Bearer <token>`.

Responde: "quantas horas foram realizadas em cada tipo de serviço (`tipoServico` da agenda)?". Devolve uma linha por (ano, `tipoServico`), ordenada pelo total de minutos decrescente. `tipoServico` é nulo para agendas sem esse campo.

**Cálculo.** Soma `duracao` (minutos) das agendas REALIZADAS no período e a formata em `HH:MM`. Atenção: `ano` volta como **texto**. A rota irmã `porcentagem-por-tipo-servico` conta AGENDAS, não horas.

**Qual rota de horas chamar (as quatro parecem iguais, mas respondem perguntas diferentes):**

| Rota | Agrupa por | Devolve | Quando usar |
|---|---|---|---|
| `GET /bi-produtividade/horas-por-mes` | mês (total da organização/filtro) | minutos, `HH:MM`, **meta e produtividade** | evolução mensal e atingimento da meta |
| `GET /bi-produtividade/por-consultor` | mês **x** pessoa | minutos, `HH:MM`, **meta individual**, cor | evolução mensal por pessoa com meta |
| `GET /bi-produtividade/horas-realizadas-por-consultor` | pessoa (período inteiro) | só `HH:MM` (sem minutos, sem meta) | ranking/total de horas por pessoa no período |
| `GET /bi-produtividade/grafico-mes-atual` | pessoa, **só o mês corrente** | só `HH:MM` | "quanto cada pessoa já realizou NESTE mês"; ignora `dataInicio`/`dataFim` |
| `GET /bi-produtividade/tabela-total-horas` | mês (total, **sem pessoa**) | só `HH:MM`, mês em inglês | tabela simples de horas por mês, sem meta |
| `GET /bi-produtividade/horas-por-tipo-agenda` | pessoa **x** tipo de agenda | minutos e `HH:MM` | em que tipo de agenda (cliente, interno, férias...) cada pessoa gastou o tempo |
| `GET /bi-produtividade/horas-por-tipo-servico` | ano **x** tipo de serviço | só `HH:MM` | distribuição de horas por serviço |
| `GET /bi-produtividade/porcentagem-por-tipo-servico` | tipo de serviço | contagem de agendas e % | participação (rosca) por serviço, em NÚMERO DE AGENDAS, não em horas |


**Regras comuns a todas as rotas `/bi-produtividade/*` de horas:**

- Conta **somente agendas com `status = REALIZADO`** da organização do token (isolamento por tenant). Agendas previstas, confirmadas, reservadas, pendentes ou canceladas ficam de fora. Agendas excluídas não entram.
- A **duração** vem do campo `duracao` da agenda, em **MINUTOS inteiros**. As rotas que devolvem `horas` o fazem como **texto `HH:MM`** (horas totais, pode passar de 24, ex.: `"152:30"`), NUNCA como número decimal.
- A **data de referência** de cada agenda é `COALESCE(dataFim, dataInicio)` (a data de término, ou o início se não houver fim), em **hora de parede gravada como UTC** (agenda digitada às 09:00 está armazenada como `T09:00:00Z`; não há conversão de fuso). Exceção: `porcentagem-por-tipo-servico` usa `dataInicio`.
- Período padrão: sem `dataInicio` e sem `dataFim`, o **ano civil atual**.
- **Permissão:** exige apenas token válido (JWT) e organização identificada. Não há `@Roles`, checagem de tela nem escopo de dados por usuário: qualquer usuário autenticado vê as horas de **todas** as pessoas da organização, sem filtro por quem pergunta.
- Os filtros são lidos direto da query string, sem schema Zod: parâmetros desconhecidos são ignorados. Só falham com 400: `situacao` inválida, `usuarioIds` com UUID inválido (ou mais de 200) e `setor` desconhecido.

#### Parâmetros

| Nome | Onde | Tipo | Obrigatório | Descrição |
|---|---|---|---|---|
| `dataInicio` | query | string | não | Início do período (inclusivo). Aceita qualquer texto que `Date.parse` leia; recomendado `AAAA-MM-DD` (vira `T00:00:00Z`). Valor ilegível é IGNORADO em silêncio (sem erro). Use `AAAA-MM-DD` com data válida; texto que o JavaScript aceita mas o Postgres não (por exemplo `2026-02-30`) pode dar 500. Compara com a data da agenda em hora de parede (ver description da rota). Ex.: `2026-01-01` |
| `dataFim` | query | string | não | Fim do período (inclusivo). Com `AAAA-MM-DD` vira `T23:59:59Z`. Valor ilegível é ignorado em silêncio; data que o JavaScript aceita mas o Postgres não (por exemplo `2026-02-30`) pode dar 500. Se NEM `dataInicio` NEM `dataFim` forem enviados, o período é o ANO CIVIL ATUAL do servidor (01/jan a 31/dez); se só um vier, o intervalo fica aberto do outro lado. Ex.: `2026-12-31` |
| `usuarioIds` | query | lista de string (uuid) | não | Filtra por pessoa (usuário responsável da agenda). UUIDs (maiúsculas são aceitas e convertidas), repetindo o parâmetro (`?usuarioIds=a&usuarioIds=b`) ou separados por vírgula. Máximo de 200 ids. Só entram usuários da organização do token; UUID malformado dá 400. Com este filtro (ou `setor`), a base da meta (`horas-por-mes`) passa a ser as pessoas filtradas. Ex.: `["c0a80101-0000-4000-8000-000000000001"]` |
| `setor` | query | lista de string — um de: `CONSULTORIA`, `PROJETOS`, `COMERCIAL`, `FINANCEIRO`, `MIGRACAO`, `CS` | não | Filtra pelo setor efetivo do responsável da agenda. Valores (sem diferenciar maiúsculas): CONSULTORIA, PROJETOS, COMERCIAL, FINANCEIRO, MIGRACAO, CS. Repetido ou separado por vírgula. Se vier junto com `usuarioIds`, vale a interseção; um setor sem ninguém devolve lista vazia. Valor desconhecido dá 400 ("setor inválido."). Ex.: `["CONSULTORIA"]` |
| `consultor` | query | string[] | não | Filtro legado por NOME: casa o nome do consultor cadastrado OU o nome do usuário responsável (igualdade exata, diferencia maiúsculas). Aceita vários nomes (repetido ou separado por vírgula). Prefira `usuarioIds`. Ex.: `["Consultor Exemplo"]` |
| `situacao` | query | string — um de: `ativos`, `inativos`, `todos`; padrão `"todos"` | não | Situação atual da pessoa responsável: `ativos` (usuário com status diferente de false; sem usuário, o consultor ativo), `inativos` ou `todos`. Ausente ou vazio = `todos`. Outro valor dá 400. Ex.: `ativos` |
| `tipoServico` | query | string[] | não | Filtra pelo NOME do tipo de projeto/serviço (`tipo_projeto.tipoProjeto`, via projeto da agenda), igualdade exata. Repetido ou separado por vírgula. Atenção: filtra o tipo de projeto, não a coluna `tipoServico` da agenda que as rotas `horas-por-tipo-servico` e `porcentagem-por-tipo-servico` devolvem. Ex.: `["Implantação"]` |
| `produto` | query | string[] | não | Filtra pelo NOME do produto do projeto da agenda (`produto.produto`), igualdade exata. Repetido ou separado por vírgula. Ex.: `["Produto Alfa"]` |
| `documento` | query | string[] | não | Filtra pelo número do documento do registro financeiro do projeto (`registro_financeiro.documento`), igualdade exata. Repetido ou separado por vírgula. Ex.: `["DOC-0001"]` |
| `tipoAgenda` | query | lista de string — um de: `INTERNO`, `CLIENTE`, `FERIAS`, `ESTUDO`, `PESSOAL`, `COMERCIAL`, `CS` | não | Filtra pelo tipo da agenda. `CLIENTE` também casa agendas sem tipo (nulo ou vazio). Sem este parâmetro NÃO há filtro de tipo (entram todos os tipos). Valor fora do enum provavelmente quebra a consulta (500; inferido, não validado). Ex.: `["CLIENTE"]` |

#### Resposta 200

Sucesso.

Tipo da raiz: HorasTipoServicoItem[]

| Campo | Tipo | Sempre presente | Descrição |
|---|---|---|---|
| `[].ano` | string | sim | Ano da data da agenda, como TEXTO (numérico do Postgres). |
| `[].tipoServico` | string \| null | sim | Valor do campo `tipoServico` da agenda; nulo quando não preenchido. |
| `[].horas` | string | sim | Horas realizadas em `HH:MM`. |

Exemplo (dados fictícios):

```json
[
  {
    "ano": "2026",
    "tipoServico": "Implantação",
    "horas": "210:15"
  },
  {
    "ano": "2026",
    "tipoServico": null,
    "horas": "12:00"
  }
]
```

#### Erros

- **400**: Parâmetros inválidos. `message` é uma lista com um item por problema, no formato `campo: motivo`.
- **401**: Token ausente, inválido, expirado ou de usuário desativado. Faça login de novo.

---

### GET /bi-produtividade/porcentagem-por-tipo-servico

**Participação de cada tipo de serviço no número de agendas realizadas** · operationId `listarPorcentagemPorTipoServico`

Autenticação: `Authorization: Bearer <token>`.

Responde: "qual a fatia (%) de cada tipo de serviço entre as agendas realizadas?". Devolve uma linha por `tipoServico` da agenda (nulo se vazio), ordenada por porcentagem decrescente.

**Cálculo.** `contagem` = número de agendas REALIZADAS do tipo; `porcentagem` = contagem / total x 100, 2 casas. **A unidade é QUANTIDADE DE AGENDAS, não horas** (para horas use `horas-por-tipo-servico`). `porcentagem` chega como **texto** (`"66.67"`). Diferente das demais rotas, o período filtra por `dataInicio` da agenda (não por `COALESCE(dataFim, dataInicio)`).

**Qual rota de horas chamar (as quatro parecem iguais, mas respondem perguntas diferentes):**

| Rota | Agrupa por | Devolve | Quando usar |
|---|---|---|---|
| `GET /bi-produtividade/horas-por-mes` | mês (total da organização/filtro) | minutos, `HH:MM`, **meta e produtividade** | evolução mensal e atingimento da meta |
| `GET /bi-produtividade/por-consultor` | mês **x** pessoa | minutos, `HH:MM`, **meta individual**, cor | evolução mensal por pessoa com meta |
| `GET /bi-produtividade/horas-realizadas-por-consultor` | pessoa (período inteiro) | só `HH:MM` (sem minutos, sem meta) | ranking/total de horas por pessoa no período |
| `GET /bi-produtividade/grafico-mes-atual` | pessoa, **só o mês corrente** | só `HH:MM` | "quanto cada pessoa já realizou NESTE mês"; ignora `dataInicio`/`dataFim` |
| `GET /bi-produtividade/tabela-total-horas` | mês (total, **sem pessoa**) | só `HH:MM`, mês em inglês | tabela simples de horas por mês, sem meta |
| `GET /bi-produtividade/horas-por-tipo-agenda` | pessoa **x** tipo de agenda | minutos e `HH:MM` | em que tipo de agenda (cliente, interno, férias...) cada pessoa gastou o tempo |
| `GET /bi-produtividade/horas-por-tipo-servico` | ano **x** tipo de serviço | só `HH:MM` | distribuição de horas por serviço |
| `GET /bi-produtividade/porcentagem-por-tipo-servico` | tipo de serviço | contagem de agendas e % | participação (rosca) por serviço, em NÚMERO DE AGENDAS, não em horas |


**Regras comuns a todas as rotas `/bi-produtividade/*` de horas:**

- Conta **somente agendas com `status = REALIZADO`** da organização do token (isolamento por tenant). Agendas previstas, confirmadas, reservadas, pendentes ou canceladas ficam de fora. Agendas excluídas não entram.
- A **duração** vem do campo `duracao` da agenda, em **MINUTOS inteiros**. As rotas que devolvem `horas` o fazem como **texto `HH:MM`** (horas totais, pode passar de 24, ex.: `"152:30"`), NUNCA como número decimal.
- A **data de referência** de cada agenda é `COALESCE(dataFim, dataInicio)` (a data de término, ou o início se não houver fim), em **hora de parede gravada como UTC** (agenda digitada às 09:00 está armazenada como `T09:00:00Z`; não há conversão de fuso). Exceção: `porcentagem-por-tipo-servico` usa `dataInicio`.
- Período padrão: sem `dataInicio` e sem `dataFim`, o **ano civil atual**.
- **Permissão:** exige apenas token válido (JWT) e organização identificada. Não há `@Roles`, checagem de tela nem escopo de dados por usuário: qualquer usuário autenticado vê as horas de **todas** as pessoas da organização, sem filtro por quem pergunta.
- Os filtros são lidos direto da query string, sem schema Zod: parâmetros desconhecidos são ignorados. Só falham com 400: `situacao` inválida, `usuarioIds` com UUID inválido (ou mais de 200) e `setor` desconhecido.

#### Parâmetros

| Nome | Onde | Tipo | Obrigatório | Descrição |
|---|---|---|---|---|
| `dataInicio` | query | string | não | Início do período (inclusivo). Aceita qualquer texto que `Date.parse` leia; recomendado `AAAA-MM-DD` (vira `T00:00:00Z`). Valor ilegível é IGNORADO em silêncio (sem erro). Use `AAAA-MM-DD` com data válida; texto que o JavaScript aceita mas o Postgres não (por exemplo `2026-02-30`) pode dar 500. Compara com a data da agenda em hora de parede (ver description da rota). Ex.: `2026-01-01` |
| `dataFim` | query | string | não | Fim do período (inclusivo). Com `AAAA-MM-DD` vira `T23:59:59Z`. Valor ilegível é ignorado em silêncio; data que o JavaScript aceita mas o Postgres não (por exemplo `2026-02-30`) pode dar 500. Se NEM `dataInicio` NEM `dataFim` forem enviados, o período é o ANO CIVIL ATUAL do servidor (01/jan a 31/dez); se só um vier, o intervalo fica aberto do outro lado. Ex.: `2026-12-31` |
| `usuarioIds` | query | lista de string (uuid) | não | Filtra por pessoa (usuário responsável da agenda). UUIDs (maiúsculas são aceitas e convertidas), repetindo o parâmetro (`?usuarioIds=a&usuarioIds=b`) ou separados por vírgula. Máximo de 200 ids. Só entram usuários da organização do token; UUID malformado dá 400. Com este filtro (ou `setor`), a base da meta (`horas-por-mes`) passa a ser as pessoas filtradas. Ex.: `["c0a80101-0000-4000-8000-000000000001"]` |
| `setor` | query | lista de string — um de: `CONSULTORIA`, `PROJETOS`, `COMERCIAL`, `FINANCEIRO`, `MIGRACAO`, `CS` | não | Filtra pelo setor efetivo do responsável da agenda. Valores (sem diferenciar maiúsculas): CONSULTORIA, PROJETOS, COMERCIAL, FINANCEIRO, MIGRACAO, CS. Repetido ou separado por vírgula. Se vier junto com `usuarioIds`, vale a interseção; um setor sem ninguém devolve lista vazia. Valor desconhecido dá 400 ("setor inválido."). Ex.: `["CONSULTORIA"]` |
| `consultor` | query | string[] | não | Filtro legado por NOME: casa o nome do consultor cadastrado OU o nome do usuário responsável (igualdade exata, diferencia maiúsculas). Aceita vários nomes (repetido ou separado por vírgula). Prefira `usuarioIds`. Ex.: `["Consultor Exemplo"]` |
| `situacao` | query | string — um de: `ativos`, `inativos`, `todos`; padrão `"todos"` | não | Situação atual da pessoa responsável: `ativos` (usuário com status diferente de false; sem usuário, o consultor ativo), `inativos` ou `todos`. Ausente ou vazio = `todos`. Outro valor dá 400. Ex.: `ativos` |
| `tipoServico` | query | string[] | não | Filtra pelo NOME do tipo de projeto/serviço (`tipo_projeto.tipoProjeto`, via projeto da agenda), igualdade exata. Repetido ou separado por vírgula. Atenção: filtra o tipo de projeto, não a coluna `tipoServico` da agenda que as rotas `horas-por-tipo-servico` e `porcentagem-por-tipo-servico` devolvem. Ex.: `["Implantação"]` |
| `produto` | query | string[] | não | Filtra pelo NOME do produto do projeto da agenda (`produto.produto`), igualdade exata. Repetido ou separado por vírgula. Ex.: `["Produto Alfa"]` |
| `documento` | query | string[] | não | Filtra pelo número do documento do registro financeiro do projeto (`registro_financeiro.documento`), igualdade exata. Repetido ou separado por vírgula. Ex.: `["DOC-0001"]` |
| `tipoAgenda` | query | lista de string — um de: `INTERNO`, `CLIENTE`, `FERIAS`, `ESTUDO`, `PESSOAL`, `COMERCIAL`, `CS` | não | Filtra pelo tipo da agenda. `CLIENTE` também casa agendas sem tipo (nulo ou vazio). Sem este parâmetro NÃO há filtro de tipo (entram todos os tipos). Valor fora do enum provavelmente quebra a consulta (500; inferido, não validado). Ex.: `["CLIENTE"]` |

#### Resposta 200

Sucesso.

Tipo da raiz: HorasPorcentagemTipoServicoItem[]

| Campo | Tipo | Sempre presente | Descrição |
|---|---|---|---|
| `[].tipoServico` | string \| null | sim | Valor do campo `tipoServico` da agenda; nulo quando não preenchido. |
| `[].contagem` | integer | sim | NÚMERO DE AGENDAS realizadas (não horas) nesse tipo. |
| `[].porcentagem` | string | sim | Participação percentual **0-100** da `contagem` sobre o total, 2 casas, serializada como TEXTO (numérico do Postgres), sem zeros à direita desnecessários (`"100"`, `"66.67"`). Use `Number()` para converter. |

Exemplo (dados fictícios):

```json
[
  {
    "tipoServico": "Implantação",
    "contagem": 2,
    "porcentagem": "66.67"
  },
  {
    "tipoServico": "Suporte",
    "contagem": 1,
    "porcentagem": "33.33"
  }
]
```

#### Erros

- **400**: Parâmetros inválidos. `message` é uma lista com um item por problema, no formato `campo: motivo`.
- **401**: Token ausente, inválido, expirado ou de usuário desativado. Faça login de novo.

---

### GET /bi-produtividade/horas-por-tipo-agenda

**Horas realizadas por pessoa e tipo de agenda** · operationId `listarHorasPorTipoAgenda`

Autenticação: `Authorization: Bearer <token>`.

Responde: "como cada pessoa dividiu suas horas realizadas entre tipos de agenda (CLIENTE, INTERNO, FÉRIAS, ESTUDO...)?". Devolve uma linha por (pessoa, `tipoAgenda`), ordenada por nome, id da pessoa e minutos decrescentes. Agenda sem tipo conta como `CLIENTE`; pessoa sem nome aparece como `Sem consultor`.

**Cálculo.** Soma `duracao` (minutos) das agendas REALIZADAS no período; devolve minutos (inteiro) e `HH:MM`. Como nas demais rotas de BI, não há filtro de tipo por padrão (todos os tipos entram); o filtro `tipoAgenda` restringe. Aqui o filtro `situacao` já se aplica sempre (a consulta sempre junta a pessoa).

**Qual rota de horas chamar (as quatro parecem iguais, mas respondem perguntas diferentes):**

| Rota | Agrupa por | Devolve | Quando usar |
|---|---|---|---|
| `GET /bi-produtividade/horas-por-mes` | mês (total da organização/filtro) | minutos, `HH:MM`, **meta e produtividade** | evolução mensal e atingimento da meta |
| `GET /bi-produtividade/por-consultor` | mês **x** pessoa | minutos, `HH:MM`, **meta individual**, cor | evolução mensal por pessoa com meta |
| `GET /bi-produtividade/horas-realizadas-por-consultor` | pessoa (período inteiro) | só `HH:MM` (sem minutos, sem meta) | ranking/total de horas por pessoa no período |
| `GET /bi-produtividade/grafico-mes-atual` | pessoa, **só o mês corrente** | só `HH:MM` | "quanto cada pessoa já realizou NESTE mês"; ignora `dataInicio`/`dataFim` |
| `GET /bi-produtividade/tabela-total-horas` | mês (total, **sem pessoa**) | só `HH:MM`, mês em inglês | tabela simples de horas por mês, sem meta |
| `GET /bi-produtividade/horas-por-tipo-agenda` | pessoa **x** tipo de agenda | minutos e `HH:MM` | em que tipo de agenda (cliente, interno, férias...) cada pessoa gastou o tempo |
| `GET /bi-produtividade/horas-por-tipo-servico` | ano **x** tipo de serviço | só `HH:MM` | distribuição de horas por serviço |
| `GET /bi-produtividade/porcentagem-por-tipo-servico` | tipo de serviço | contagem de agendas e % | participação (rosca) por serviço, em NÚMERO DE AGENDAS, não em horas |


**Regras comuns a todas as rotas `/bi-produtividade/*` de horas:**

- Conta **somente agendas com `status = REALIZADO`** da organização do token (isolamento por tenant). Agendas previstas, confirmadas, reservadas, pendentes ou canceladas ficam de fora. Agendas excluídas não entram.
- A **duração** vem do campo `duracao` da agenda, em **MINUTOS inteiros**. As rotas que devolvem `horas` o fazem como **texto `HH:MM`** (horas totais, pode passar de 24, ex.: `"152:30"`), NUNCA como número decimal.
- A **data de referência** de cada agenda é `COALESCE(dataFim, dataInicio)` (a data de término, ou o início se não houver fim), em **hora de parede gravada como UTC** (agenda digitada às 09:00 está armazenada como `T09:00:00Z`; não há conversão de fuso). Exceção: `porcentagem-por-tipo-servico` usa `dataInicio`.
- Período padrão: sem `dataInicio` e sem `dataFim`, o **ano civil atual**.
- **Permissão:** exige apenas token válido (JWT) e organização identificada. Não há `@Roles`, checagem de tela nem escopo de dados por usuário: qualquer usuário autenticado vê as horas de **todas** as pessoas da organização, sem filtro por quem pergunta.
- Os filtros são lidos direto da query string, sem schema Zod: parâmetros desconhecidos são ignorados. Só falham com 400: `situacao` inválida, `usuarioIds` com UUID inválido (ou mais de 200) e `setor` desconhecido.

#### Parâmetros

| Nome | Onde | Tipo | Obrigatório | Descrição |
|---|---|---|---|---|
| `dataInicio` | query | string | não | Início do período (inclusivo). Aceita qualquer texto que `Date.parse` leia; recomendado `AAAA-MM-DD` (vira `T00:00:00Z`). Valor ilegível é IGNORADO em silêncio (sem erro). Use `AAAA-MM-DD` com data válida; texto que o JavaScript aceita mas o Postgres não (por exemplo `2026-02-30`) pode dar 500. Compara com a data da agenda em hora de parede (ver description da rota). Ex.: `2026-01-01` |
| `dataFim` | query | string | não | Fim do período (inclusivo). Com `AAAA-MM-DD` vira `T23:59:59Z`. Valor ilegível é ignorado em silêncio; data que o JavaScript aceita mas o Postgres não (por exemplo `2026-02-30`) pode dar 500. Se NEM `dataInicio` NEM `dataFim` forem enviados, o período é o ANO CIVIL ATUAL do servidor (01/jan a 31/dez); se só um vier, o intervalo fica aberto do outro lado. Ex.: `2026-12-31` |
| `usuarioIds` | query | lista de string (uuid) | não | Filtra por pessoa (usuário responsável da agenda). UUIDs (maiúsculas são aceitas e convertidas), repetindo o parâmetro (`?usuarioIds=a&usuarioIds=b`) ou separados por vírgula. Máximo de 200 ids. Só entram usuários da organização do token; UUID malformado dá 400. Com este filtro (ou `setor`), a base da meta (`horas-por-mes`) passa a ser as pessoas filtradas. Ex.: `["c0a80101-0000-4000-8000-000000000001"]` |
| `setor` | query | lista de string — um de: `CONSULTORIA`, `PROJETOS`, `COMERCIAL`, `FINANCEIRO`, `MIGRACAO`, `CS` | não | Filtra pelo setor efetivo do responsável da agenda. Valores (sem diferenciar maiúsculas): CONSULTORIA, PROJETOS, COMERCIAL, FINANCEIRO, MIGRACAO, CS. Repetido ou separado por vírgula. Se vier junto com `usuarioIds`, vale a interseção; um setor sem ninguém devolve lista vazia. Valor desconhecido dá 400 ("setor inválido."). Ex.: `["CONSULTORIA"]` |
| `consultor` | query | string[] | não | Filtro legado por NOME: casa o nome do consultor cadastrado OU o nome do usuário responsável (igualdade exata, diferencia maiúsculas). Aceita vários nomes (repetido ou separado por vírgula). Prefira `usuarioIds`. Ex.: `["Consultor Exemplo"]` |
| `situacao` | query | string — um de: `ativos`, `inativos`, `todos`; padrão `"todos"` | não | Situação atual da pessoa responsável: `ativos` (usuário com status diferente de false; sem usuário, o consultor ativo), `inativos` ou `todos`. Ausente ou vazio = `todos`. Outro valor dá 400. Ex.: `ativos` |
| `tipoServico` | query | string[] | não | Filtra pelo NOME do tipo de projeto/serviço (`tipo_projeto.tipoProjeto`, via projeto da agenda), igualdade exata. Repetido ou separado por vírgula. Atenção: filtra o tipo de projeto, não a coluna `tipoServico` da agenda que as rotas `horas-por-tipo-servico` e `porcentagem-por-tipo-servico` devolvem. Ex.: `["Implantação"]` |
| `produto` | query | string[] | não | Filtra pelo NOME do produto do projeto da agenda (`produto.produto`), igualdade exata. Repetido ou separado por vírgula. Ex.: `["Produto Alfa"]` |
| `documento` | query | string[] | não | Filtra pelo número do documento do registro financeiro do projeto (`registro_financeiro.documento`), igualdade exata. Repetido ou separado por vírgula. Ex.: `["DOC-0001"]` |
| `tipoAgenda` | query | lista de string — um de: `INTERNO`, `CLIENTE`, `FERIAS`, `ESTUDO`, `PESSOAL`, `COMERCIAL`, `CS` | não | Filtra pelo tipo da agenda. `CLIENTE` também casa agendas sem tipo (nulo ou vazio). Sem este parâmetro NÃO há filtro de tipo (entram todos os tipos). Valor fora do enum provavelmente quebra a consulta (500; inferido, não validado). Ex.: `["CLIENTE"]` |

#### Resposta 200

Sucesso.

Tipo da raiz: HorasPorTipoAgendaItem[]

| Campo | Tipo | Sempre presente | Descrição |
|---|---|---|---|
| `[].usuarioId` | string (uuid) \| null | sim | Nulo quando a agenda está reservada num consultor sem usuário vinculado. |
| `[].consultor` | string | sim | Nome da pessoa; `Sem consultor` quando não há nome. |
| `[].tipoAgenda` | string — um de: `INTERNO`, `CLIENTE`, `FERIAS`, `ESTUDO`, `PESSOAL`, `COMERCIAL`, `CS` | sim | Tipo da agenda. Agenda sem tipo aparece como `CLIENTE`. |
| `[].minutosRealizados` | integer | sim | Minutos REALIZADOS da pessoa nesse tipo, no período inteiro. |
| `[].horas` | string | sim | `minutosRealizados` em `HH:MM`. |

Exemplo (dados fictícios):

```json
[
  {
    "usuarioId": "5d1b6c2e-8f3a-4c71-9b0e-2a7d4e6f1a10",
    "consultor": "Consultor Exemplo",
    "tipoAgenda": "CLIENTE",
    "minutosRealizados": 4800,
    "horas": "80:00"
  },
  {
    "usuarioId": "5d1b6c2e-8f3a-4c71-9b0e-2a7d4e6f1a10",
    "consultor": "Consultor Exemplo",
    "tipoAgenda": "INTERNO",
    "minutosRealizados": 600,
    "horas": "10:00"
  }
]
```

#### Erros

- **400**: Parâmetros inválidos. `message` é uma lista com um item por problema, no formato `campo: motivo`.
- **401**: Token ausente, inválido, expirado ou de usuário desativado. Faça login de novo.

---

### GET /bi-produtividade/tabela-total-horas

**Tabela simples de horas realizadas por mês (sem pessoa, sem meta)** · operationId `listarTabelaTotalHoras`

Autenticação: `Authorization: Bearer <token>`.

Responde: "quantas horas no total foram realizadas em cada mês?". Devolve uma linha por (ano, mês), ordenada por **ano decrescente** e depois mês crescente (atenção: diferente de `horas-por-mes`, que é cronológico).

**Cálculo.** Soma `duracao` das agendas REALIZADAS e devolve só `HH:MM`. **Sem minutos, sem meta, sem separar pessoas.** Tipos esquisitos herdados do SQL: `ano` e `mes_numero` são **strings** e `mes` é o nome do mês **em inglês** (`March`). Se precisar de meta ou de números, use `horas-por-mes`.

**Qual rota de horas chamar (as quatro parecem iguais, mas respondem perguntas diferentes):**

| Rota | Agrupa por | Devolve | Quando usar |
|---|---|---|---|
| `GET /bi-produtividade/horas-por-mes` | mês (total da organização/filtro) | minutos, `HH:MM`, **meta e produtividade** | evolução mensal e atingimento da meta |
| `GET /bi-produtividade/por-consultor` | mês **x** pessoa | minutos, `HH:MM`, **meta individual**, cor | evolução mensal por pessoa com meta |
| `GET /bi-produtividade/horas-realizadas-por-consultor` | pessoa (período inteiro) | só `HH:MM` (sem minutos, sem meta) | ranking/total de horas por pessoa no período |
| `GET /bi-produtividade/grafico-mes-atual` | pessoa, **só o mês corrente** | só `HH:MM` | "quanto cada pessoa já realizou NESTE mês"; ignora `dataInicio`/`dataFim` |
| `GET /bi-produtividade/tabela-total-horas` | mês (total, **sem pessoa**) | só `HH:MM`, mês em inglês | tabela simples de horas por mês, sem meta |
| `GET /bi-produtividade/horas-por-tipo-agenda` | pessoa **x** tipo de agenda | minutos e `HH:MM` | em que tipo de agenda (cliente, interno, férias...) cada pessoa gastou o tempo |
| `GET /bi-produtividade/horas-por-tipo-servico` | ano **x** tipo de serviço | só `HH:MM` | distribuição de horas por serviço |
| `GET /bi-produtividade/porcentagem-por-tipo-servico` | tipo de serviço | contagem de agendas e % | participação (rosca) por serviço, em NÚMERO DE AGENDAS, não em horas |


**Regras comuns a todas as rotas `/bi-produtividade/*` de horas:**

- Conta **somente agendas com `status = REALIZADO`** da organização do token (isolamento por tenant). Agendas previstas, confirmadas, reservadas, pendentes ou canceladas ficam de fora. Agendas excluídas não entram.
- A **duração** vem do campo `duracao` da agenda, em **MINUTOS inteiros**. As rotas que devolvem `horas` o fazem como **texto `HH:MM`** (horas totais, pode passar de 24, ex.: `"152:30"`), NUNCA como número decimal.
- A **data de referência** de cada agenda é `COALESCE(dataFim, dataInicio)` (a data de término, ou o início se não houver fim), em **hora de parede gravada como UTC** (agenda digitada às 09:00 está armazenada como `T09:00:00Z`; não há conversão de fuso). Exceção: `porcentagem-por-tipo-servico` usa `dataInicio`.
- Período padrão: sem `dataInicio` e sem `dataFim`, o **ano civil atual**.
- **Permissão:** exige apenas token válido (JWT) e organização identificada. Não há `@Roles`, checagem de tela nem escopo de dados por usuário: qualquer usuário autenticado vê as horas de **todas** as pessoas da organização, sem filtro por quem pergunta.
- Os filtros são lidos direto da query string, sem schema Zod: parâmetros desconhecidos são ignorados. Só falham com 400: `situacao` inválida, `usuarioIds` com UUID inválido (ou mais de 200) e `setor` desconhecido.

#### Parâmetros

| Nome | Onde | Tipo | Obrigatório | Descrição |
|---|---|---|---|---|
| `dataInicio` | query | string | não | Início do período (inclusivo). Aceita qualquer texto que `Date.parse` leia; recomendado `AAAA-MM-DD` (vira `T00:00:00Z`). Valor ilegível é IGNORADO em silêncio (sem erro). Use `AAAA-MM-DD` com data válida; texto que o JavaScript aceita mas o Postgres não (por exemplo `2026-02-30`) pode dar 500. Compara com a data da agenda em hora de parede (ver description da rota). Ex.: `2026-01-01` |
| `dataFim` | query | string | não | Fim do período (inclusivo). Com `AAAA-MM-DD` vira `T23:59:59Z`. Valor ilegível é ignorado em silêncio; data que o JavaScript aceita mas o Postgres não (por exemplo `2026-02-30`) pode dar 500. Se NEM `dataInicio` NEM `dataFim` forem enviados, o período é o ANO CIVIL ATUAL do servidor (01/jan a 31/dez); se só um vier, o intervalo fica aberto do outro lado. Ex.: `2026-12-31` |
| `usuarioIds` | query | lista de string (uuid) | não | Filtra por pessoa (usuário responsável da agenda). UUIDs (maiúsculas são aceitas e convertidas), repetindo o parâmetro (`?usuarioIds=a&usuarioIds=b`) ou separados por vírgula. Máximo de 200 ids. Só entram usuários da organização do token; UUID malformado dá 400. Com este filtro (ou `setor`), a base da meta (`horas-por-mes`) passa a ser as pessoas filtradas. Ex.: `["c0a80101-0000-4000-8000-000000000001"]` |
| `setor` | query | lista de string — um de: `CONSULTORIA`, `PROJETOS`, `COMERCIAL`, `FINANCEIRO`, `MIGRACAO`, `CS` | não | Filtra pelo setor efetivo do responsável da agenda. Valores (sem diferenciar maiúsculas): CONSULTORIA, PROJETOS, COMERCIAL, FINANCEIRO, MIGRACAO, CS. Repetido ou separado por vírgula. Se vier junto com `usuarioIds`, vale a interseção; um setor sem ninguém devolve lista vazia. Valor desconhecido dá 400 ("setor inválido."). Ex.: `["CONSULTORIA"]` |
| `consultor` | query | string[] | não | Filtro legado por NOME: casa o nome do consultor cadastrado OU o nome do usuário responsável (igualdade exata, diferencia maiúsculas). Aceita vários nomes (repetido ou separado por vírgula). Prefira `usuarioIds`. Ex.: `["Consultor Exemplo"]` |
| `situacao` | query | string — um de: `ativos`, `inativos`, `todos`; padrão `"todos"` | não | Situação atual da pessoa responsável: `ativos` (usuário com status diferente de false; sem usuário, o consultor ativo), `inativos` ou `todos`. Ausente ou vazio = `todos`. Outro valor dá 400. Ex.: `ativos` |
| `tipoServico` | query | string[] | não | Filtra pelo NOME do tipo de projeto/serviço (`tipo_projeto.tipoProjeto`, via projeto da agenda), igualdade exata. Repetido ou separado por vírgula. Atenção: filtra o tipo de projeto, não a coluna `tipoServico` da agenda que as rotas `horas-por-tipo-servico` e `porcentagem-por-tipo-servico` devolvem. Ex.: `["Implantação"]` |
| `produto` | query | string[] | não | Filtra pelo NOME do produto do projeto da agenda (`produto.produto`), igualdade exata. Repetido ou separado por vírgula. Ex.: `["Produto Alfa"]` |
| `documento` | query | string[] | não | Filtra pelo número do documento do registro financeiro do projeto (`registro_financeiro.documento`), igualdade exata. Repetido ou separado por vírgula. Ex.: `["DOC-0001"]` |
| `tipoAgenda` | query | lista de string — um de: `INTERNO`, `CLIENTE`, `FERIAS`, `ESTUDO`, `PESSOAL`, `COMERCIAL`, `CS` | não | Filtra pelo tipo da agenda. `CLIENTE` também casa agendas sem tipo (nulo ou vazio). Sem este parâmetro NÃO há filtro de tipo (entram todos os tipos). Valor fora do enum provavelmente quebra a consulta (500; inferido, não validado). Ex.: `["CLIENTE"]` |

#### Resposta 200

Sucesso.

Tipo da raiz: HorasTabelaMesItem[]

| Campo | Tipo | Sempre presente | Descrição |
|---|---|---|---|
| `[].ano` | string | sim | Ano, como texto. |
| `[].mes` | string | sim | Nome do mês em inglês completo (January...December). |
| `[].mes_numero` | string | sim | Número do mês (1-12), como texto, sem zero à esquerda. |
| `[].horas` | string | sim | Horas realizadas no mês em `HH:MM`. |

Exemplo (dados fictícios):

```json
[
  {
    "ano": "2026",
    "mes": "March",
    "mes_numero": "3",
    "horas": "152:30"
  },
  {
    "ano": "2026",
    "mes": "February",
    "mes_numero": "2",
    "horas": "140:00"
  }
]
```

#### Erros

- **400**: Parâmetros inválidos. `message` é uma lista com um item por problema, no formato `campo: motivo`.
- **401**: Token ausente, inválido, expirado ou de usuário desativado. Faça login de novo.

---

### GET /bi-produtividade/horas-realizadas-por-consultor

**Total de horas realizadas por pessoa no período inteiro** · operationId `listarHorasRealizadasPorConsultor`

Autenticação: `Authorization: Bearer <token>`.

Responde: "qual o total de horas realizadas por cada pessoa no período?" (ranking). Devolve **uma linha por pessoa**, ordenada por total decrescente (numérico, correto) e depois nome.

**Cálculo.** Soma `duracao` das agendas REALIZADAS do período (padrão: ano civil atual) por pessoa e devolve só `HH:MM`. **Não separa por mês, não traz minutos nem meta** (para isso use `por-consultor`). Para só o mês corrente use `grafico-mes-atual`. Homônimos sem usuário vinculado aparecem em linhas separadas (`usuarioId` nulo).

**Qual rota de horas chamar (as quatro parecem iguais, mas respondem perguntas diferentes):**

| Rota | Agrupa por | Devolve | Quando usar |
|---|---|---|---|
| `GET /bi-produtividade/horas-por-mes` | mês (total da organização/filtro) | minutos, `HH:MM`, **meta e produtividade** | evolução mensal e atingimento da meta |
| `GET /bi-produtividade/por-consultor` | mês **x** pessoa | minutos, `HH:MM`, **meta individual**, cor | evolução mensal por pessoa com meta |
| `GET /bi-produtividade/horas-realizadas-por-consultor` | pessoa (período inteiro) | só `HH:MM` (sem minutos, sem meta) | ranking/total de horas por pessoa no período |
| `GET /bi-produtividade/grafico-mes-atual` | pessoa, **só o mês corrente** | só `HH:MM` | "quanto cada pessoa já realizou NESTE mês"; ignora `dataInicio`/`dataFim` |
| `GET /bi-produtividade/tabela-total-horas` | mês (total, **sem pessoa**) | só `HH:MM`, mês em inglês | tabela simples de horas por mês, sem meta |
| `GET /bi-produtividade/horas-por-tipo-agenda` | pessoa **x** tipo de agenda | minutos e `HH:MM` | em que tipo de agenda (cliente, interno, férias...) cada pessoa gastou o tempo |
| `GET /bi-produtividade/horas-por-tipo-servico` | ano **x** tipo de serviço | só `HH:MM` | distribuição de horas por serviço |
| `GET /bi-produtividade/porcentagem-por-tipo-servico` | tipo de serviço | contagem de agendas e % | participação (rosca) por serviço, em NÚMERO DE AGENDAS, não em horas |


**Regras comuns a todas as rotas `/bi-produtividade/*` de horas:**

- Conta **somente agendas com `status = REALIZADO`** da organização do token (isolamento por tenant). Agendas previstas, confirmadas, reservadas, pendentes ou canceladas ficam de fora. Agendas excluídas não entram.
- A **duração** vem do campo `duracao` da agenda, em **MINUTOS inteiros**. As rotas que devolvem `horas` o fazem como **texto `HH:MM`** (horas totais, pode passar de 24, ex.: `"152:30"`), NUNCA como número decimal.
- A **data de referência** de cada agenda é `COALESCE(dataFim, dataInicio)` (a data de término, ou o início se não houver fim), em **hora de parede gravada como UTC** (agenda digitada às 09:00 está armazenada como `T09:00:00Z`; não há conversão de fuso). Exceção: `porcentagem-por-tipo-servico` usa `dataInicio`.
- Período padrão: sem `dataInicio` e sem `dataFim`, o **ano civil atual**.
- **Permissão:** exige apenas token válido (JWT) e organização identificada. Não há `@Roles`, checagem de tela nem escopo de dados por usuário: qualquer usuário autenticado vê as horas de **todas** as pessoas da organização, sem filtro por quem pergunta.
- Os filtros são lidos direto da query string, sem schema Zod: parâmetros desconhecidos são ignorados. Só falham com 400: `situacao` inválida, `usuarioIds` com UUID inválido (ou mais de 200) e `setor` desconhecido.

#### Parâmetros

| Nome | Onde | Tipo | Obrigatório | Descrição |
|---|---|---|---|---|
| `dataInicio` | query | string | não | Início do período (inclusivo). Aceita qualquer texto que `Date.parse` leia; recomendado `AAAA-MM-DD` (vira `T00:00:00Z`). Valor ilegível é IGNORADO em silêncio (sem erro). Use `AAAA-MM-DD` com data válida; texto que o JavaScript aceita mas o Postgres não (por exemplo `2026-02-30`) pode dar 500. Compara com a data da agenda em hora de parede (ver description da rota). Ex.: `2026-01-01` |
| `dataFim` | query | string | não | Fim do período (inclusivo). Com `AAAA-MM-DD` vira `T23:59:59Z`. Valor ilegível é ignorado em silêncio; data que o JavaScript aceita mas o Postgres não (por exemplo `2026-02-30`) pode dar 500. Se NEM `dataInicio` NEM `dataFim` forem enviados, o período é o ANO CIVIL ATUAL do servidor (01/jan a 31/dez); se só um vier, o intervalo fica aberto do outro lado. Ex.: `2026-12-31` |
| `usuarioIds` | query | lista de string (uuid) | não | Filtra por pessoa (usuário responsável da agenda). UUIDs (maiúsculas são aceitas e convertidas), repetindo o parâmetro (`?usuarioIds=a&usuarioIds=b`) ou separados por vírgula. Máximo de 200 ids. Só entram usuários da organização do token; UUID malformado dá 400. Com este filtro (ou `setor`), a base da meta (`horas-por-mes`) passa a ser as pessoas filtradas. Ex.: `["c0a80101-0000-4000-8000-000000000001"]` |
| `setor` | query | lista de string — um de: `CONSULTORIA`, `PROJETOS`, `COMERCIAL`, `FINANCEIRO`, `MIGRACAO`, `CS` | não | Filtra pelo setor efetivo do responsável da agenda. Valores (sem diferenciar maiúsculas): CONSULTORIA, PROJETOS, COMERCIAL, FINANCEIRO, MIGRACAO, CS. Repetido ou separado por vírgula. Se vier junto com `usuarioIds`, vale a interseção; um setor sem ninguém devolve lista vazia. Valor desconhecido dá 400 ("setor inválido."). Ex.: `["CONSULTORIA"]` |
| `consultor` | query | string[] | não | Filtro legado por NOME: casa o nome do consultor cadastrado OU o nome do usuário responsável (igualdade exata, diferencia maiúsculas). Aceita vários nomes (repetido ou separado por vírgula). Prefira `usuarioIds`. Ex.: `["Consultor Exemplo"]` |
| `situacao` | query | string — um de: `ativos`, `inativos`, `todos`; padrão `"todos"` | não | Situação atual da pessoa responsável: `ativos` (usuário com status diferente de false; sem usuário, o consultor ativo), `inativos` ou `todos`. Ausente ou vazio = `todos`. Outro valor dá 400. Ex.: `ativos` |
| `tipoServico` | query | string[] | não | Filtra pelo NOME do tipo de projeto/serviço (`tipo_projeto.tipoProjeto`, via projeto da agenda), igualdade exata. Repetido ou separado por vírgula. Atenção: filtra o tipo de projeto, não a coluna `tipoServico` da agenda que as rotas `horas-por-tipo-servico` e `porcentagem-por-tipo-servico` devolvem. Ex.: `["Implantação"]` |
| `produto` | query | string[] | não | Filtra pelo NOME do produto do projeto da agenda (`produto.produto`), igualdade exata. Repetido ou separado por vírgula. Ex.: `["Produto Alfa"]` |
| `documento` | query | string[] | não | Filtra pelo número do documento do registro financeiro do projeto (`registro_financeiro.documento`), igualdade exata. Repetido ou separado por vírgula. Ex.: `["DOC-0001"]` |
| `tipoAgenda` | query | lista de string — um de: `INTERNO`, `CLIENTE`, `FERIAS`, `ESTUDO`, `PESSOAL`, `COMERCIAL`, `CS` | não | Filtra pelo tipo da agenda. `CLIENTE` também casa agendas sem tipo (nulo ou vazio). Sem este parâmetro NÃO há filtro de tipo (entram todos os tipos). Valor fora do enum provavelmente quebra a consulta (500; inferido, não validado). Ex.: `["CLIENTE"]` |

#### Resposta 200

Sucesso.

Tipo da raiz: HorasConsultorTotalItem[]

| Campo | Tipo | Sempre presente | Descrição |
|---|---|---|---|
| `[].usuarioId` | string (uuid) \| null | sim | Nulo quando a agenda está reservada num consultor sem usuário vinculado (nesse caso homônimos aparecem em linhas separadas). |
| `[].consultor` | string \| null | sim | Nome da pessoa (usuário, com reserva no consultor). |
| `[].horas` | string | sim | Horas realizadas em `HH:MM`. |

Exemplo (dados fictícios):

```json
[
  {
    "usuarioId": "5d1b6c2e-8f3a-4c71-9b0e-2a7d4e6f1a10",
    "consultor": "Consultor Exemplo",
    "horas": "620:30"
  },
  {
    "usuarioId": "9a3c7e10-2b4d-4f68-a1c5-7e8d9f0a1b22",
    "consultor": "Outra Consultora Exemplo",
    "horas": "480:00"
  }
]
```

#### Erros

- **400**: Parâmetros inválidos. `message` é uma lista com um item por problema, no formato `campo: motivo`.
- **401**: Token ausente, inválido, expirado ou de usuário desativado. Faça login de novo.

---

### GET /bi-produtividade/grafico-mes-atual

**Horas realizadas por pessoa no mês corrente** · operationId `obterGraficoHorasMesAtual`

Autenticação: `Authorization: Bearer <token>`.

Responde: "quanto cada pessoa já realizou NESTE mês?". Devolve `{ mesAtual, consultores }`: `mesAtual` é uma lista de um item com o nome do mês corrente em português minúsculo; `consultores` tem uma linha por pessoa.

**Cálculo.** Igual a `horas-realizadas-por-consultor`, mas o período é **sempre o mês corrente do servidor** (do dia 1 ao último dia). **`dataInicio` e `dataFim` são ignorados** mesmo se enviados. Só devolve `HH:MM`. Ordenação por `horas` decrescente **como texto** (um total de `100:00` fica abaixo de `99:00`), depois nome. Sem agenda realizada no mês, `consultores` vem vazio.

**Qual rota de horas chamar (as quatro parecem iguais, mas respondem perguntas diferentes):**

| Rota | Agrupa por | Devolve | Quando usar |
|---|---|---|---|
| `GET /bi-produtividade/horas-por-mes` | mês (total da organização/filtro) | minutos, `HH:MM`, **meta e produtividade** | evolução mensal e atingimento da meta |
| `GET /bi-produtividade/por-consultor` | mês **x** pessoa | minutos, `HH:MM`, **meta individual**, cor | evolução mensal por pessoa com meta |
| `GET /bi-produtividade/horas-realizadas-por-consultor` | pessoa (período inteiro) | só `HH:MM` (sem minutos, sem meta) | ranking/total de horas por pessoa no período |
| `GET /bi-produtividade/grafico-mes-atual` | pessoa, **só o mês corrente** | só `HH:MM` | "quanto cada pessoa já realizou NESTE mês"; ignora `dataInicio`/`dataFim` |
| `GET /bi-produtividade/tabela-total-horas` | mês (total, **sem pessoa**) | só `HH:MM`, mês em inglês | tabela simples de horas por mês, sem meta |
| `GET /bi-produtividade/horas-por-tipo-agenda` | pessoa **x** tipo de agenda | minutos e `HH:MM` | em que tipo de agenda (cliente, interno, férias...) cada pessoa gastou o tempo |
| `GET /bi-produtividade/horas-por-tipo-servico` | ano **x** tipo de serviço | só `HH:MM` | distribuição de horas por serviço |
| `GET /bi-produtividade/porcentagem-por-tipo-servico` | tipo de serviço | contagem de agendas e % | participação (rosca) por serviço, em NÚMERO DE AGENDAS, não em horas |


**Regras comuns a todas as rotas `/bi-produtividade/*` de horas:**

- Conta **somente agendas com `status = REALIZADO`** da organização do token (isolamento por tenant). Agendas previstas, confirmadas, reservadas, pendentes ou canceladas ficam de fora. Agendas excluídas não entram.
- A **duração** vem do campo `duracao` da agenda, em **MINUTOS inteiros**. As rotas que devolvem `horas` o fazem como **texto `HH:MM`** (horas totais, pode passar de 24, ex.: `"152:30"`), NUNCA como número decimal.
- A **data de referência** de cada agenda é `COALESCE(dataFim, dataInicio)` (a data de término, ou o início se não houver fim), em **hora de parede gravada como UTC** (agenda digitada às 09:00 está armazenada como `T09:00:00Z`; não há conversão de fuso). Exceção: `porcentagem-por-tipo-servico` usa `dataInicio`.
- Período padrão: sem `dataInicio` e sem `dataFim`, o **ano civil atual**.
- **Permissão:** exige apenas token válido (JWT) e organização identificada. Não há `@Roles`, checagem de tela nem escopo de dados por usuário: qualquer usuário autenticado vê as horas de **todas** as pessoas da organização, sem filtro por quem pergunta.
- Os filtros são lidos direto da query string, sem schema Zod: parâmetros desconhecidos são ignorados. Só falham com 400: `situacao` inválida, `usuarioIds` com UUID inválido (ou mais de 200) e `setor` desconhecido.

#### Parâmetros

| Nome | Onde | Tipo | Obrigatório | Descrição |
|---|---|---|---|---|
| `usuarioIds` | query | lista de string (uuid) | não | Filtra por pessoa (usuário responsável da agenda). UUIDs (maiúsculas são aceitas e convertidas), repetindo o parâmetro (`?usuarioIds=a&usuarioIds=b`) ou separados por vírgula. Máximo de 200 ids. Só entram usuários da organização do token; UUID malformado dá 400. Com este filtro (ou `setor`), a base da meta (`horas-por-mes`) passa a ser as pessoas filtradas. Ex.: `["c0a80101-0000-4000-8000-000000000001"]` |
| `setor` | query | lista de string — um de: `CONSULTORIA`, `PROJETOS`, `COMERCIAL`, `FINANCEIRO`, `MIGRACAO`, `CS` | não | Filtra pelo setor efetivo do responsável da agenda. Valores (sem diferenciar maiúsculas): CONSULTORIA, PROJETOS, COMERCIAL, FINANCEIRO, MIGRACAO, CS. Repetido ou separado por vírgula. Se vier junto com `usuarioIds`, vale a interseção; um setor sem ninguém devolve lista vazia. Valor desconhecido dá 400 ("setor inválido."). Ex.: `["CONSULTORIA"]` |
| `consultor` | query | string[] | não | Filtro legado por NOME: casa o nome do consultor cadastrado OU o nome do usuário responsável (igualdade exata, diferencia maiúsculas). Aceita vários nomes (repetido ou separado por vírgula). Prefira `usuarioIds`. Ex.: `["Consultor Exemplo"]` |
| `situacao` | query | string — um de: `ativos`, `inativos`, `todos`; padrão `"todos"` | não | Situação atual da pessoa responsável: `ativos` (usuário com status diferente de false; sem usuário, o consultor ativo), `inativos` ou `todos`. Ausente ou vazio = `todos`. Outro valor dá 400. Ex.: `ativos` |
| `tipoServico` | query | string[] | não | Filtra pelo NOME do tipo de projeto/serviço (`tipo_projeto.tipoProjeto`, via projeto da agenda), igualdade exata. Repetido ou separado por vírgula. Atenção: filtra o tipo de projeto, não a coluna `tipoServico` da agenda que as rotas `horas-por-tipo-servico` e `porcentagem-por-tipo-servico` devolvem. Ex.: `["Implantação"]` |
| `produto` | query | string[] | não | Filtra pelo NOME do produto do projeto da agenda (`produto.produto`), igualdade exata. Repetido ou separado por vírgula. Ex.: `["Produto Alfa"]` |
| `documento` | query | string[] | não | Filtra pelo número do documento do registro financeiro do projeto (`registro_financeiro.documento`), igualdade exata. Repetido ou separado por vírgula. Ex.: `["DOC-0001"]` |
| `tipoAgenda` | query | lista de string — um de: `INTERNO`, `CLIENTE`, `FERIAS`, `ESTUDO`, `PESSOAL`, `COMERCIAL`, `CS` | não | Filtra pelo tipo da agenda. `CLIENTE` também casa agendas sem tipo (nulo ou vazio). Sem este parâmetro NÃO há filtro de tipo (entram todos os tipos). Valor fora do enum provavelmente quebra a consulta (500; inferido, não validado). Ex.: `["CLIENTE"]` |

#### Resposta 200

Sucesso.

Tipo da raiz: HorasGraficoMesAtual

| Campo | Tipo | Sempre presente | Descrição |
|---|---|---|---|
| `mesAtual` | string[] | sim | Lista de um único item: nome do mês corrente em português minúsculo (`novembro`), pelo relógio do servidor. |
| `consultores` | HorasConsultorTotalItem[] | sim | Uma linha por pessoa, ordenada por `horas` DECRESCENTE **comparando como texto** (não numericamente: `"100:00"` fica depois de `"99:00"`), depois por nome. |
| `consultores[].usuarioId` | string (uuid) \| null | sim | Nulo quando a agenda está reservada num consultor sem usuário vinculado (nesse caso homônimos aparecem em linhas separadas). |
| `consultores[].consultor` | string \| null | sim | Nome da pessoa (usuário, com reserva no consultor). |
| `consultores[].horas` | string | sim | Horas realizadas em `HH:MM`. |

Exemplo (dados fictícios):

```json
{
  "mesAtual": [
    "novembro"
  ],
  "consultores": [
    {
      "usuarioId": "5d1b6c2e-8f3a-4c71-9b0e-2a7d4e6f1a10",
      "consultor": "Consultor Exemplo",
      "horas": "64:30"
    },
    {
      "usuarioId": "9a3c7e10-2b4d-4f68-a1c5-7e8d9f0a1b22",
      "consultor": "Outra Consultora Exemplo",
      "horas": "42:00"
    }
  ]
}
```

#### Erros

- **400**: Parâmetros inválidos. `message` é uma lista com um item por problema, no formato `campo: motivo`.
- **401**: Token ausente, inválido, expirado ou de usuário desativado. Faça login de novo.
