# API do AgendAI (leitura) Versão 1.0.0. URL base: https://api-prod.agendai.pro. OpenAPI: https://api-docs.agendai.pro/openapi.json ## Guia de uso API REST em JSON do AgendAI, sistema de gestão de uma consultoria: agenda dos consultores, horas realizadas, CRM, faturamento e cobrança. Esta documentação cobre **só leitura**: horas dos consultores, comercial (oportunidades, metas, comissões, dashboard) e inadimplência. Chamar rotas que não estão aqui não é suportado. - **URL base:** `https://api-prod.agendai.pro` - **Especificação OpenAPI 3.1:** `/openapi.json` (deste site) - **Tudo em um arquivo Markdown:** `/llms-full.txt` ### 1. Autenticação 1. Faça `POST /login` com `{"email": "...", "password": "..."}`. A resposta é **201** com `{"token": ""}`. 2. Envie o token em toda chamada: `Authorization: Bearer `. 3. O token vale **7 dias** e não há refresh. Guarde-o e reutilize-o. Quando uma chamada responder **401**, faça login de novo e repita a chamada uma vez; se o 401 persistir, pare e avise (credencial trocada ou usuário desativado). ```bash TOKEN=$(curl -s -X POST https://api-prod.agendai.pro/login \ -H 'Content-Type: application/json' \ -d '{"email":"integracao@exemplo.com.br","password":"sua-senha"}' | jq -r .token) curl -s https://api-prod.agendai.pro/metas/dashboard \ -H "Authorization: Bearer $TOKEN" ``` As credenciais são entregues fora desta documentação. Nunca as grave em código versionado nem em logs. ### 2. O que o token enxerga - **Organização:** cada usuário pertence a uma organização (tenant) e toda resposta já vem filtrada por ela. Não existe parâmetro de tenant. - **Permissões:** o que cada rota devolve depende das telas e do papel do usuário do token, conferidos a cada chamada no banco (mudar a permissão do usuário vale na hora, sem novo login). Sem permissão, a rota responde 403 ou devolve só os registros que o usuário pode ver; a descrição de cada rota diz qual dos dois. ### 3. Convenções dos dados - **Datas e horas da agenda são "hora de parede":** o horário digitado no AgendAI é gravado como se fosse UTC. Um agendamento das 09:00 em Brasília chega como `2026-03-10T09:00:00.000Z`. Leia os componentes (data e hora) direto da string; **não converta de UTC para America/Sao_Paulo**, senão o horário fica 3 horas adiantado ou atrasado. - **Datas em parâmetros:** `YYYY-MM-DD`, salvo quando a rota diz outra coisa. - **Valores decimais** guardados como decimal no banco podem vir como **string** (`"1500.50"`). A descrição de cada campo indica o tipo e a unidade (minutos ou horas, reais ou centavos, percentual 0–100 ou 0–1). - **Ids:** alguns recursos usam UUID e outros inteiros; use o tipo indicado em cada rota. - **Campos ausentes:** campos `null` vêm como `null`; não presuma zero. ### 4. Erros Todo erro tem o corpo: ```json { "status": 403, "path": "/rota", "message": "texto ou lista", "code": "OPCIONAL" } ``` | Status | Significado | O que fazer | |---|---|---| | 400 | Parâmetro inválido. `message` é uma lista `campo: motivo`. | Corrija a chamada; não repita igual. | | 401 | Sem token, token inválido ou expirado, usuário desativado. | Faça login de novo e repita uma vez. | | 403 | O usuário não tem a permissão da rota. | Não repita; peça acesso ao administrador do AgendAI. | | 404 | Registro não existe (ou é de outra organização). | Não repita. | | 5xx | Erro do servidor. | Repita com espera crescente (1 s, 2 s, 4 s), no máximo 3 vezes. | Quando houver `code`, decida pelo `code`, não pelo texto da mensagem. ### 5. Boas práticas para agentes - Para mostrar dados a uma pessoa, use os nomes que já vêm nas respostas (objetos `{ id, nome }`) e as rotas `filter-options` para traduzir ids. - Prefira filtrar no servidor (parâmetros de período, vendedor, status) a baixar tudo e filtrar localmente. - Respeite a paginação: siga `page`/`pageSize` (ou o que a rota indicar) até o total informado. - Não faça mais de algumas requisições por segundo; os dados mudam ao longo do dia, não a cada segundo. ## Autenticação Login com e-mail e senha para obter o JWT usado em todas as outras chamadas. - `POST /login` — Obter o token (JWT) ### POST /login **Obter o token (JWT)** · operationId `login` Autenticação: nenhuma (rota pública). Troca e-mail e senha por um JWT. Use o token em todas as outras chamadas no cabeçalho `Authorization: Bearer `. - Responde **201** (não 200) com `{ "token": "..." }`. - O e-mail é comparado sem diferenciar maiúsculas. - O token vale **7 dias** e não há refresh: quando qualquer chamada responder 401, faça login de novo e repita a chamada. - Guarde o token e reutilize-o; não faça login a cada requisição. - Usuário inativo (ou com convite pendente) recebe 401 com mensagem própria. Credenciais erradas recebem 401 `Invalid credentials.` - Desativar o usuário no AgendAI invalida os tokens já emitidos na próxima chamada. #### Corpo Tipo da raiz: object | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `email` | string (email) | sim | | | `password` | string | sim | | ```json { "email": "integracao@exemplo.com.br", "password": "sua-senha" } ``` #### Resposta 201 Token emitido. Tipo da raiz: object | Campo | Tipo | Sempre presente | Descrição | |---|---|---|---| | `token` | string | sim | JWT HS256, validade de 7 dias. | Exemplo (dados fictícios): ```json { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIuLi4ifQ.assinatura" } ``` #### Erros - **400**: Parâmetros inválidos. `message` é uma lista com um item por problema, no formato `campo: motivo`. - **401**: Credenciais inválidas, e-mail ambíguo ou usuário inativo. ## Horas dos consultores 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 `. 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 `. 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 `. 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 `. 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 `. 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 `. 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 `. 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 `. 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. ## Inadimplência Títulos vencidos e em aberto dos clientes, a partir dos recebíveis do Omie. - `GET /financeiro/inadimplencia/resumo` — Resumo (BI) da inadimplência financeira - `GET /financeiro/inadimplencia/itens` — Itens (linhas) em atraso do BI de inadimplência, paginados - `GET /financeiro/inadimplencia/filter-options/{dimension}` — Catálogo paginado de opções para um filtro da inadimplência ### GET /financeiro/inadimplencia/resumo **Resumo (BI) da inadimplência financeira** · operationId `obterResumoInadimplencia` Autenticação: `Authorization: Bearer `. Devolve os indicadores do BI de inadimplência: cartões de totais e cinco agregações (por departamento, maiores devedores, evolução por mês de vencimento e histórico ano x mês). Todos os números respeitam os mesmos filtros e o mesmo cálculo de `/financeiro/inadimplencia/itens`; não há paginação. Todos os valores monetários são em **reais** (número JSON com 2 casas). A resposta vem embrulhada em `{ "data": ... }`. Atenção: no resumo da fonte canônica cada **parcela/título** é um item (`quantidadeItensAtrasados`), enquanto em `/itens` a paginação conta **linhas** agrupadas por OS. Os valores monetários totais batem entre as duas rotas; as contagens não. **Filtros em lista:** valores separados por vírgula ou parâmetro repetido; texto vazio e segmentos vazios (`a,,b`) são ignorados; o texto `null` **não** é ignorado (vira um valor literal no filtro de lista e, em datas, `incluirIgnoradas`, `page` e `pageSize`, responde 400). ### Como a inadimplência é calculada A pergunta respondida é "quanto a organização tem a receber, já vencido e não pago, de títulos faturados". Os números saem da fila de **recebíveis do Omie** sincronizados no AgendAI (`omie_receivable`), não de um cálculo sobre a agenda. Tudo é restrito à organização (tenant) do token. **Duas fontes de dados (a organização cai em uma delas):** - **Canônica** (a fonte atual): usada quando a organização tem ao menos um cadastro Omie ativo e **todos** os cadastros ativos terminaram a carga inicial de recebíveis (fase `READY`). Cada título/parcela do Omie é um recebível. - **Legada**: usada quando não há cadastro Omie ativo, ou algum cadastro ainda está em carga (`BACKFILL_PENDING`) ou falhou (`BACKFILL_FAILED`). Nesse caso a base é a **ordem de serviço** (OS) em vez do título: OS com status `BOLETO_GERADO` ou `FATURADA`, valor total > 0, **uma única parcela**, vencimento anterior ao início do dia (UTC), de projeto não excluído, com `naoFaturarFinanceiro = false`, de tipo de projeto pós-pago elegível ao financeiro **ou sem tipo de projeto** e com cliente vinculado. O valor em atraso é o valor total da OS. - A resposta não diz qual fonte foi usada, exceto pelos itens: na fonte legada `origem = "Legado"`, `omieTenantAppId = null`, `rowKey = "OS:"` e `detalhe.parcelas = []`. **O que entra (fonte canônica)** — um recebível só conta se **todas** as condições valem: 1. O cadastro Omie dele (`omie_tenant_app`) está **ativo**. Cadastro Omie inativo (por exemplo empresa baixada) sai da inadimplência; reativar devolve os títulos. 2. O estado é `OPEN` (em aberto) ou `PARTIALLY_PAID` (parcialmente pago) e o saldo em aberto (`open_amount`) é maior que zero. Títulos liquidados (`SETTLED`) e cancelados não entram. 3. A **data de vencimento efetiva** é anterior ao início do dia de hoje em Brasília (00:00 BRT). A data efetiva é a mais tardia entre o vencimento (`dDtVenc`) e a previsão de recebimento (`dDtPrevisao`) do Omie: um título vencido cujo recebimento foi reprogramado para o futuro não conta. Os filtros `dataVencimento*` e o campo `vencimento` usam essa mesma data. Título com data efetiva hoje ainda não é inadimplente. 4. O título tem **número de documento fiscal** preenchido (isto é, a nota foi emitida). 5. O tipo de documento **não** é `REE`, `DAM` nem `99999` (sem diferenciar maiúsculas de minúsculas). 6. Salvo `incluirIgnoradas=true`: a OS do título **não** tem regra de "inadimplência ignorada" ativa e o contrato (direto ou herdado da OS) também não. Essas regras são cadastradas no módulo de inadimplência (`inadimplencia_ignorada`, alvo `ORDEM_SERVICO` ou `CONTRATO`). 7. Os filtros da consulta (empresa Omie, cliente, projeto, produto, tipo de projeto, departamento, categoria, tipo de documento, intervalo de vencimento). Overrides de status e regras de inadimplência por cliente/projeto (módulo `inadimplencia-status`/`inadimplencia-regras`) **não** influenciam estas três rotas; só as regras de "ignorada" acima entram no cálculo. **Valores:** reais (BRL), números JSON com 2 casas (não centavos, não string). **Datas:** `vencimento` e demais datas de calendário são `YYYY-MM-DD`. **Dias de atraso:** dias inteiros entre o vencimento (o mais antigo, no caso de linha com várias parcelas) e hoje; 0 se não vencido. **Permissão:** capacidade `VER_BI_INADIMPLENCIA`. Sem ela a resposta é 403. Em geral: administrador global, administrador do tipo Financeiro, ou usuário com a tela `/financeiro/inadimplencia` liberada nas permissões de tela (lida do banco a cada requisição, então revogação vale na hora). Com a regra de acesso por setor ligada na organização, vale somente a tela efetiva do usuário. Não há filtro por registros: quem passa vê todos os dados da organização. #### Parâmetros | Nome | Onde | Tipo | Obrigatório | Descrição | |---|---|---|---|---| | `empresaOmie` | query | integer[] | não | Ids dos cadastros Omie (`omie_tenant_app`) da organização. Aceita parâmetro repetido (`?empresaOmie=1&empresaOmie=2`) ou lista separada por vírgula (`empresaOmie=1,2`); valores duplicados são descartados. Só tem efeito na fonte canônica; na fonte legada qualquer valor aqui zera o resultado. Os ids válidos vêm de `filter-options/empresaOmie`. Ex.: `[1]` | | `cliente` | query | string[] | não | Clientes. Fonte canônica: ids (UUID) do cliente ou o texto exato do campo `cliente` dos itens (nome fantasia; senão a empresa do projeto; senão o nome no Omie; ou `Sem cliente vinculado`); os dois formatos podem ser misturados. Fonte legada: somente o nome fantasia. Mesmo formato de lista de `empresaOmie` (repetido ou separado por vírgula; por isso um nome com vírgula não pode ser filtrado). Na fonte canônica use o `id` de `filter-options/cliente`; na legada, o `label`. Ex.: `["0b6a1d52-3f0e-4c6a-9d0e-5a1f2f6d7a11"]` | | `projeto` | query | string[] | não | Projetos. Fonte canônica: ids (UUID) ou nome do projeto; fonte legada: nome. Mesmo formato de lista. Na fonte canônica use o `id` de `filter-options/projeto`; na legada, o `label`. Ex.: `["Implantação Alfa"]` | | `produto` | query | string[] | não | Produtos (id do produto). Fonte canônica: casa o título pela categoria Omie mapeada ao produto (`omie_tenant_billing_mapping` ativo). Na fonte legada qualquer valor aqui zera o resultado. Ids em `filter-options/produto`. Ex.: `["5f0c1e7a-8d2b-4a39-b0c4-1e2d3c4b5a69"]` | | `tipoProjeto` | query | string[] | não | Tipos de projeto. Fonte canônica: id do tipo de projeto; fonte legada: nome do tipo (`Sem tipo de projeto` seleciona os projetos sem tipo). Na fonte canônica use o `id` de `filter-options/tipoProjeto`; na legada, o `label`. Ex.: `["a1b2c3d4-0000-4000-8000-000000000001"]` | | `departamento` | query | string[] | não | Departamentos. Fonte canônica: código do departamento Omie (`SEM_DEPARTAMENTO` para títulos sem roteamento de departamento); fonte legada: nome/descrição do departamento (`Sem departamento` quando vazio). Na fonte canônica use o `id` de `filter-options/departamento`; na legada, o `label`. Ex.: `["SEM_DEPARTAMENTO"]` | | `categoria` | query | string[] | não | Códigos de categoria Omie do título (`category_code`). Só tem efeito na fonte canônica; na legada qualquer valor zera o resultado. Códigos em `filter-options/categoria`. Ex.: `["1.01.02"]` | | `tipoDocumento` | query | string[] | não | Tipos de documento Omie do título (`BOL`, `REC`, `TED`...). Só tem efeito na fonte canônica; na legada qualquer valor zera o resultado. Códigos em `filter-options/tipoDocumento`. Ex.: `["BOL"]` | | `dataVencimentoInicio` | query | string (date) | não | Vencimento mínimo, inclusivo, no formato `YYYY-MM-DD` (data de calendário válida). Fonte canônica: o dia começa à 00:00 de Brasília (03:00Z). Fonte legada: dia em UTC. Texto vazio é ignorado. Ex.: `2026-01-01` | | `dataVencimentoFim` | query | string (date) | não | Vencimento máximo, inclusivo, `YYYY-MM-DD`. Deve ser maior ou igual a `dataVencimentoInicio` (senão 400). Como só entram parcelas vencidas antes de hoje, valores futuros não ampliam o resultado. Ex.: `2026-10-31` | | `incluirIgnoradas` | query | boolean; padrão `false` | não | Aceita `true` ou `false`. Padrão (omitido ou `false`): ordens de serviço e contratos com regra de "ignorada" ativa ficam fora. Com `true` eles entram nos números. Ex.: `false` | #### Resposta 200 Resumo calculado. Tipo da raiz: InadimplenciaResumoResponse | Campo | Tipo | Sempre presente | Descrição | |---|---|---|---| | `data` | object | sim | | | `data.cards` | object | sim | | | `data.cards.quantidadeItensAtrasados` | integer | sim | Quantidade de itens em atraso. Fonte canônica: número de **parcelas/títulos** em atraso (não de linhas agrupadas por OS; por isso pode ser maior que `pagination.total` de `/itens`). Fonte legada: número de OS. | | `data.cards.valorTotalEmAtraso` | number | sim | Soma do saldo em atraso, em reais. | | `data.cards.maiorValorEmAtraso` | number | sim | Maior valor individual entre os itens em atraso (uma parcela na fonte canônica; uma OS na legada), em reais. 0 quando não há itens. | | `data.cards.clientesInadimplentes` | integer | sim | Quantidade de clientes distintos com item em atraso. Fonte canônica: título sem cliente vinculado conta como um "cliente" próprio por título (id sintético `OMIE::`). | | `data.graficos` | object | sim | | | `data.graficos.porDepartamento` | object[] | sim | Agrupado por departamento, ordenado por valor decrescente e depois por nome. | | `data.graficos.porDepartamento[].departamento` | string | sim | Nome/descrição do departamento Omie, ou "Sem departamento". | | `data.graficos.porDepartamento[].quantidadeItensAtrasados` | integer | sim | Itens em atraso do departamento. | | `data.graficos.porDepartamento[].valorTotalEmAtraso` | number | sim | Valor em atraso do departamento, em reais. | | `data.graficos.topDevedores` | object[] | sim | Os 10 clientes com maior valor em atraso (empate por nome). | | `data.graficos.topDevedores[].clienteId` | string | sim | Id do cliente (UUID) ou id sintético `OMIE::` para título sem cliente vinculado. | | `data.graficos.topDevedores[].cliente` | string | sim | Nome do cliente. | | `data.graficos.topDevedores[].quantidadeItensAtrasados` | integer | sim | Itens em atraso do cliente. | | `data.graficos.topDevedores[].valorTotalEmAtraso` | number | sim | Valor em atraso do cliente, em reais. | | `data.graficos.evolucaoPorMesVencimento` | object[] | sim | Itens agrupados pelo mês de vencimento, em ordem cronológica. Só meses com itens aparecem. | | `data.graficos.evolucaoPorMesVencimento[].mes` | string | sim | Mês de vencimento `YYYY-MM` (UTC). | | `data.graficos.evolucaoPorMesVencimento[].label` | string | sim | Rótulo `MM/YYYY`. | | `data.graficos.evolucaoPorMesVencimento[].quantidadeItensAtrasados` | integer | sim | Itens em atraso que venceram no mês. | | `data.graficos.evolucaoPorMesVencimento[].valorTotalEmAtraso` | number | sim | Valor em atraso dos itens que venceram no mês, em reais. | | `data.graficos.matrizHistoricaAnoMes` | object | sim | | | `data.graficos.matrizHistoricaAnoMes.meses` | object[] | sim | Sempre os 12 meses fixos, Jan a Dez. | | `data.graficos.matrizHistoricaAnoMes.meses[].mes` | integer | sim | Número do mês (1-12). | | `data.graficos.matrizHistoricaAnoMes.meses[].key` | string — um de: `01`, `02`, `03`, `04`, `05`, `06`, `07`, `08`, `09`, `10`, `11`, `12` | sim | Chave `01`..`12`. | | `data.graficos.matrizHistoricaAnoMes.meses[].label` | string | sim | Abreviação em português: Jan, Fev, Mar, Abr, Mai, Jun, Jul, Ago, Set, Out, Nov, Dez. | | `data.graficos.matrizHistoricaAnoMes.anos` | object[] | sim | Uma linha por ano de vencimento com itens, em ordem crescente. | | `data.graficos.matrizHistoricaAnoMes.anos[].ano` | integer | sim | Ano de vencimento. | | `data.graficos.matrizHistoricaAnoMes.anos[].totalAno` | number | sim | Soma do ano, em reais. | | `data.graficos.matrizHistoricaAnoMes.anos[].valoresPorMes` | object | sim | Valor em atraso por mês de vencimento; meses sem itens valem 0. | | `data.graficos.matrizHistoricaAnoMes.anos[].valoresPorMes.10` | number | sim | Valor em atraso que venceu neste mês, em reais. | | `data.graficos.matrizHistoricaAnoMes.anos[].valoresPorMes.11` | number | sim | Valor em atraso que venceu neste mês, em reais. | | `data.graficos.matrizHistoricaAnoMes.anos[].valoresPorMes.12` | number | sim | Valor em atraso que venceu neste mês, em reais. | | `data.graficos.matrizHistoricaAnoMes.anos[].valoresPorMes.01` | number | sim | Valor em atraso que venceu neste mês, em reais. | | `data.graficos.matrizHistoricaAnoMes.anos[].valoresPorMes.02` | number | sim | Valor em atraso que venceu neste mês, em reais. | | `data.graficos.matrizHistoricaAnoMes.anos[].valoresPorMes.03` | number | sim | Valor em atraso que venceu neste mês, em reais. | | `data.graficos.matrizHistoricaAnoMes.anos[].valoresPorMes.04` | number | sim | Valor em atraso que venceu neste mês, em reais. | | `data.graficos.matrizHistoricaAnoMes.anos[].valoresPorMes.05` | number | sim | Valor em atraso que venceu neste mês, em reais. | | `data.graficos.matrizHistoricaAnoMes.anos[].valoresPorMes.06` | number | sim | Valor em atraso que venceu neste mês, em reais. | | `data.graficos.matrizHistoricaAnoMes.anos[].valoresPorMes.07` | number | sim | Valor em atraso que venceu neste mês, em reais. | | `data.graficos.matrizHistoricaAnoMes.anos[].valoresPorMes.08` | number | sim | Valor em atraso que venceu neste mês, em reais. | | `data.graficos.matrizHistoricaAnoMes.anos[].valoresPorMes.09` | number | sim | Valor em atraso que venceu neste mês, em reais. | | `data.graficos.serieHistoricaAnoMes` | object[] | sim | Mesma informação da matriz, em série por ano (12 pontos por ano). | | `data.graficos.serieHistoricaAnoMes[].ano` | integer | sim | Ano de vencimento. | | `data.graficos.serieHistoricaAnoMes[].totalAno` | number | sim | Soma do ano, em reais. | | `data.graficos.serieHistoricaAnoMes[].pontos` | object[] | sim | | | `data.graficos.serieHistoricaAnoMes[].pontos[].mes` | integer | sim | Número do mês (1-12). | | `data.graficos.serieHistoricaAnoMes[].pontos[].key` | string — um de: `01`, `02`, `03`, `04`, `05`, `06`, `07`, `08`, `09`, `10`, `11`, `12` | sim | Chave `01`..`12`. | | `data.graficos.serieHistoricaAnoMes[].pontos[].label` | string | sim | Abreviação do mês. | | `data.graficos.serieHistoricaAnoMes[].pontos[].valorTotalEmAtraso` | number | sim | Valor em atraso que venceu no mês, em reais. | Exemplo (dados fictícios): ```json { "data": { "cards": { "quantidadeItensAtrasados": 7, "valorTotalEmAtraso": 31250.5, "maiorValorEmAtraso": 12000, "clientesInadimplentes": 4 }, "graficos": { "porDepartamento": [ { "departamento": "Serviços", "quantidadeItensAtrasados": 5, "valorTotalEmAtraso": 26000.5 }, { "departamento": "Sem departamento", "quantidadeItensAtrasados": 2, "valorTotalEmAtraso": 5250 } ], "topDevedores": [ { "clienteId": "0b6a1d52-3f0e-4c6a-9d0e-5a1f2f6d7a11", "cliente": "Empresa Alfa Ltda", "quantidadeItensAtrasados": 3, "valorTotalEmAtraso": 16500 } ], "evolucaoPorMesVencimento": [ { "mes": "2026-09", "label": "09/2026", "quantidadeItensAtrasados": 4, "valorTotalEmAtraso": 20250.5 }, { "mes": "2026-10", "label": "10/2026", "quantidadeItensAtrasados": 3, "valorTotalEmAtraso": 11000 } ], "matrizHistoricaAnoMes": { "meses": [ { "mes": 1, "key": "01", "label": "Jan" }, { "mes": 2, "key": "02", "label": "Fev" }, { "mes": 3, "key": "03", "label": "Mar" }, { "mes": 4, "key": "04", "label": "Abr" }, { "mes": 5, "key": "05", "label": "Mai" }, { "mes": 6, "key": "06", "label": "Jun" }, { "mes": 7, "key": "07", "label": "Jul" }, { "mes": 8, "key": "08", "label": "Ago" }, { "mes": 9, "key": "09", "label": "Set" }, { "mes": 10, "key": "10", "label": "Out" }, { "mes": 11, "key": "11", "label": "Nov" }, { "mes": 12, "key": "12", "label": "Dez" } ], "anos": [ { "ano": 2026, "totalAno": 31250.5, "valoresPorMes": { "10": 11000, "11": 0, "12": 0, "01": 0, "02": 0, "03": 0, "04": 0, "05": 0, "06": 0, "07": 0, "08": 0, "09": 20250.5 } } ] }, "serieHistoricaAnoMes": [ { "ano": 2026, "totalAno": 31250.5, "pontos": [ { "mes": 9, "key": "09", "label": "Set", "valorTotalEmAtraso": 20250.5 }, { "mes": 10, "key": "10", "label": "Out", "valorTotalEmAtraso": 11000 } ] } ] } } } ``` #### 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. - **403**: O usuário do token não tem a permissão exigida pela rota. --- ### GET /financeiro/inadimplencia/itens **Itens (linhas) em atraso do BI de inadimplência, paginados** · operationId `listarItensInadimplencia` Autenticação: `Authorization: Bearer `. Lista paginada das **linhas** inadimplentes. Na fonte canônica uma linha agrupa todas as parcelas em atraso da mesma OS (`OS:`) ou, sem OS, um título (`TITLE::`); o valor da linha é a soma dos saldos e o vencimento é o mais antigo. Cada linha traz o detalhamento das parcelas, da OS, do contrato e do projeto. Valores em **reais**; datas de calendário `YYYY-MM-DD`; resposta em `{ "data": { items, pagination } }`. **Ordenação** (`sortBy`/`sortOrder`, padrão `vencimento` ascendente, ou seja, o mais antigo primeiro). O desempate é sempre vencimento mais antigo e depois a chave da linha. `sortBy=diasAtraso` com `asc` devolve primeiro os com menos dias de atraso, nas duas fontes. **Filtros em lista:** valores separados por vírgula ou parâmetro repetido; texto vazio e segmentos vazios (`a,,b`) são ignorados; o texto `null` **não** é ignorado (vira um valor literal no filtro de lista e, em datas, `incluirIgnoradas`, `page` e `pageSize`, responde 400). ### Como a inadimplência é calculada A pergunta respondida é "quanto a organização tem a receber, já vencido e não pago, de títulos faturados". Os números saem da fila de **recebíveis do Omie** sincronizados no AgendAI (`omie_receivable`), não de um cálculo sobre a agenda. Tudo é restrito à organização (tenant) do token. **Duas fontes de dados (a organização cai em uma delas):** - **Canônica** (a fonte atual): usada quando a organização tem ao menos um cadastro Omie ativo e **todos** os cadastros ativos terminaram a carga inicial de recebíveis (fase `READY`). Cada título/parcela do Omie é um recebível. - **Legada**: usada quando não há cadastro Omie ativo, ou algum cadastro ainda está em carga (`BACKFILL_PENDING`) ou falhou (`BACKFILL_FAILED`). Nesse caso a base é a **ordem de serviço** (OS) em vez do título: OS com status `BOLETO_GERADO` ou `FATURADA`, valor total > 0, **uma única parcela**, vencimento anterior ao início do dia (UTC), de projeto não excluído, com `naoFaturarFinanceiro = false`, de tipo de projeto pós-pago elegível ao financeiro **ou sem tipo de projeto** e com cliente vinculado. O valor em atraso é o valor total da OS. - A resposta não diz qual fonte foi usada, exceto pelos itens: na fonte legada `origem = "Legado"`, `omieTenantAppId = null`, `rowKey = "OS:"` e `detalhe.parcelas = []`. **O que entra (fonte canônica)** — um recebível só conta se **todas** as condições valem: 1. O cadastro Omie dele (`omie_tenant_app`) está **ativo**. Cadastro Omie inativo (por exemplo empresa baixada) sai da inadimplência; reativar devolve os títulos. 2. O estado é `OPEN` (em aberto) ou `PARTIALLY_PAID` (parcialmente pago) e o saldo em aberto (`open_amount`) é maior que zero. Títulos liquidados (`SETTLED`) e cancelados não entram. 3. A **data de vencimento efetiva** é anterior ao início do dia de hoje em Brasília (00:00 BRT). A data efetiva é a mais tardia entre o vencimento (`dDtVenc`) e a previsão de recebimento (`dDtPrevisao`) do Omie: um título vencido cujo recebimento foi reprogramado para o futuro não conta. Os filtros `dataVencimento*` e o campo `vencimento` usam essa mesma data. Título com data efetiva hoje ainda não é inadimplente. 4. O título tem **número de documento fiscal** preenchido (isto é, a nota foi emitida). 5. O tipo de documento **não** é `REE`, `DAM` nem `99999` (sem diferenciar maiúsculas de minúsculas). 6. Salvo `incluirIgnoradas=true`: a OS do título **não** tem regra de "inadimplência ignorada" ativa e o contrato (direto ou herdado da OS) também não. Essas regras são cadastradas no módulo de inadimplência (`inadimplencia_ignorada`, alvo `ORDEM_SERVICO` ou `CONTRATO`). 7. Os filtros da consulta (empresa Omie, cliente, projeto, produto, tipo de projeto, departamento, categoria, tipo de documento, intervalo de vencimento). Overrides de status e regras de inadimplência por cliente/projeto (módulo `inadimplencia-status`/`inadimplencia-regras`) **não** influenciam estas três rotas; só as regras de "ignorada" acima entram no cálculo. **Valores:** reais (BRL), números JSON com 2 casas (não centavos, não string). **Datas:** `vencimento` e demais datas de calendário são `YYYY-MM-DD`. **Dias de atraso:** dias inteiros entre o vencimento (o mais antigo, no caso de linha com várias parcelas) e hoje; 0 se não vencido. **Permissão:** capacidade `VER_BI_INADIMPLENCIA`. Sem ela a resposta é 403. Em geral: administrador global, administrador do tipo Financeiro, ou usuário com a tela `/financeiro/inadimplencia` liberada nas permissões de tela (lida do banco a cada requisição, então revogação vale na hora). Com a regra de acesso por setor ligada na organização, vale somente a tela efetiva do usuário. Não há filtro por registros: quem passa vê todos os dados da organização. #### Parâmetros | Nome | Onde | Tipo | Obrigatório | Descrição | |---|---|---|---|---| | `empresaOmie` | query | integer[] | não | Ids dos cadastros Omie (`omie_tenant_app`) da organização. Aceita parâmetro repetido (`?empresaOmie=1&empresaOmie=2`) ou lista separada por vírgula (`empresaOmie=1,2`); valores duplicados são descartados. Só tem efeito na fonte canônica; na fonte legada qualquer valor aqui zera o resultado. Os ids válidos vêm de `filter-options/empresaOmie`. Ex.: `[1]` | | `cliente` | query | string[] | não | Clientes. Fonte canônica: ids (UUID) do cliente ou o texto exato do campo `cliente` dos itens (nome fantasia; senão a empresa do projeto; senão o nome no Omie; ou `Sem cliente vinculado`); os dois formatos podem ser misturados. Fonte legada: somente o nome fantasia. Mesmo formato de lista de `empresaOmie` (repetido ou separado por vírgula; por isso um nome com vírgula não pode ser filtrado). Na fonte canônica use o `id` de `filter-options/cliente`; na legada, o `label`. Ex.: `["0b6a1d52-3f0e-4c6a-9d0e-5a1f2f6d7a11"]` | | `projeto` | query | string[] | não | Projetos. Fonte canônica: ids (UUID) ou nome do projeto; fonte legada: nome. Mesmo formato de lista. Na fonte canônica use o `id` de `filter-options/projeto`; na legada, o `label`. Ex.: `["Implantação Alfa"]` | | `produto` | query | string[] | não | Produtos (id do produto). Fonte canônica: casa o título pela categoria Omie mapeada ao produto (`omie_tenant_billing_mapping` ativo). Na fonte legada qualquer valor aqui zera o resultado. Ids em `filter-options/produto`. Ex.: `["5f0c1e7a-8d2b-4a39-b0c4-1e2d3c4b5a69"]` | | `tipoProjeto` | query | string[] | não | Tipos de projeto. Fonte canônica: id do tipo de projeto; fonte legada: nome do tipo (`Sem tipo de projeto` seleciona os projetos sem tipo). Na fonte canônica use o `id` de `filter-options/tipoProjeto`; na legada, o `label`. Ex.: `["a1b2c3d4-0000-4000-8000-000000000001"]` | | `departamento` | query | string[] | não | Departamentos. Fonte canônica: código do departamento Omie (`SEM_DEPARTAMENTO` para títulos sem roteamento de departamento); fonte legada: nome/descrição do departamento (`Sem departamento` quando vazio). Na fonte canônica use o `id` de `filter-options/departamento`; na legada, o `label`. Ex.: `["SEM_DEPARTAMENTO"]` | | `categoria` | query | string[] | não | Códigos de categoria Omie do título (`category_code`). Só tem efeito na fonte canônica; na legada qualquer valor zera o resultado. Códigos em `filter-options/categoria`. Ex.: `["1.01.02"]` | | `tipoDocumento` | query | string[] | não | Tipos de documento Omie do título (`BOL`, `REC`, `TED`...). Só tem efeito na fonte canônica; na legada qualquer valor zera o resultado. Códigos em `filter-options/tipoDocumento`. Ex.: `["BOL"]` | | `dataVencimentoInicio` | query | string (date) | não | Vencimento mínimo, inclusivo, no formato `YYYY-MM-DD` (data de calendário válida). Fonte canônica: o dia começa à 00:00 de Brasília (03:00Z). Fonte legada: dia em UTC. Texto vazio é ignorado. Ex.: `2026-01-01` | | `dataVencimentoFim` | query | string (date) | não | Vencimento máximo, inclusivo, `YYYY-MM-DD`. Deve ser maior ou igual a `dataVencimentoInicio` (senão 400). Como só entram parcelas vencidas antes de hoje, valores futuros não ampliam o resultado. Ex.: `2026-10-31` | | `incluirIgnoradas` | query | boolean; padrão `false` | não | Aceita `true` ou `false`. Padrão (omitido ou `false`): ordens de serviço e contratos com regra de "ignorada" ativa ficam fora. Com `true` eles entram nos números. Ex.: `false` | | `page` | query | integer; padrão `1`; mín. 1 | não | Página, base 1 (mínimo 1; 400 se menor). Texto vazio vale o padrão. Ex.: `1` | | `pageSize` | query | integer; padrão `20`; mín. 1; máx. 100 | não | Linhas por página (1 a 100; 400 fora disso). Ex.: `20` | | `sortBy` | query | string — um de: `vencimento`, `diasAtraso`, `valorEmAtraso`, `cliente`, `projeto`, `departamento`, `ordemServico`, `contrato`, `produto`, `tipoProjeto`; padrão `"vencimento"` | não | Campo de ordenação. `ordemServico` ordena número de OS só de dígitos como número e o resto como texto; `contrato` pelo rótulo do contrato; textos ausentes vão ao fim nos dois sentidos. Ex.: `vencimento` | | `sortOrder` | query | string — um de: `asc`, `desc`; padrão `"asc"` | não | Sentido da ordenação. Ex.: `asc` | #### Resposta 200 Página de itens em atraso. Tipo da raiz: InadimplenciaItensResponse | Campo | Tipo | Sempre presente | Descrição | |---|---|---|---| | `data` | object | sim | | | `data.items` | InadimplenciaItem[] | sim | | | `data.items[].rowKey` | string | sim | Chave estável da linha: `OS:` (títulos ligados a uma OS são agrupados em uma linha) ou `TITLE::` (título sem OS). Em casos raros (títulos da mesma OS ligados a projeto, cliente ou contrato diferentes) a mesma chave pode aparecer em mais de uma linha; nesse caso cada linha traz todas as parcelas da OS em `detalhe.parcelas` e a soma delas pode não bater com `valorEmAtraso`. | | `data.items[].omieTenantAppId` | integer \| null | sim | Id do cadastro Omie de origem. `null` na fonte legada. | | `data.items[].origem` | string | sim | Apelido do cadastro Omie (ou `Omie app `); "Legado" na fonte legada. | | `data.items[].codTituloOmie` | string \| null | sim | Código do título no Omie; preenchido só em linha sem OS. | | `data.items[].cpfCnpjCliente` | string \| null | sim | CPF/CNPJ do cliente no recebível (dado pessoal, sem máscara garantida). `null` na fonte legada. | | `data.items[].ordemServicoId` | integer \| null | sim | Id interno da OS. | | `data.items[].ordemServicoNumero` | string \| null | sim | Número da OS. | | `data.items[].codOsOmie` | string \| null | sim | Código da OS no Omie. | | `data.items[].contratoId` | integer \| null | sim | Id interno do contrato vinculado. | | `data.items[].contratoLabel` | string | sim | Rótulo do contrato (ver `detalhe.contrato.label`). | | `data.items[].clienteId` | string \| null | sim | Id do cliente (UUID). | | `data.items[].cliente` | string | sim | Nome do cliente (nome fantasia; senão a empresa do projeto; senão o nome vindo do Omie; senão "Sem cliente vinculado" na canônica / "Cliente não identificado" na legada). | | `data.items[].projetoId` | string \| null | sim | Id do projeto (UUID). | | `data.items[].projeto` | string | sim | Nome do projeto; `Projeto ` se o projeto não tem nome; "Sem projeto vinculado" sem projeto (só canônica). | | `data.items[].produtoId` | string \| null | sim | Id do produto (UUID). | | `data.items[].produto` | string | sim | Nome do produto ou "Sem produto". | | `data.items[].tipoProjetoId` | string \| null | sim | Id do tipo de projeto. | | `data.items[].tipoProjeto` | string | sim | Nome do tipo de projeto ou "Sem tipo de projeto". | | `data.items[].departamento` | string | sim | Departamento ou "Sem departamento". | | `data.items[].tipoDocumento` | string \| null | sim | Tipos de documento Omie dos títulos da linha, distintos e separados por ", " (ex.: `BOL, REC`). `null` na fonte legada. | | `data.items[].status` | string | sim | Canônica: `OPEN`. Legada: `BOLETO_GERADO` ou `FATURADA`. | | `data.items[].statusDescricao` | string | sim | Canônica: "Recebível em aberto". Legada: descrição do status da OS. | | `data.items[].vencimento` | string (date) | sim | Vencimento mais antigo entre as parcelas da linha, `YYYY-MM-DD`. Usa a data de vencimento efetiva (a mais tardia entre o vencimento e a previsão de recebimento do Omie). | | `data.items[].diasAtraso` | integer | sim | Dias inteiros de atraso a partir do vencimento mais antigo da linha. | | `data.items[].valorEmAtraso` | number | sim | Soma do saldo em aberto das parcelas da linha que passam nos filtros, em reais. | | `data.items[].historico` | boolean | sim | `true` quando o projeto foi excluído (soft delete) ou o cliente está inativo; o item continua devendo, mas é "histórico" no catálogo. Sempre `false` na fonte legada. | | `data.items[].catalogoAusente` | boolean | sim | `true` quando cliente, produto ou tipo de projeto referenciado não existe, foi excluído ou (cliente) não é mais da relação CLIENTE. Sempre `false` na fonte legada. | | `data.items[].detalhe` | object | sim | | | `data.items[].detalhe.tipoDocumento` | string \| null | sim | Igual a `tipoDocumento`. | | `data.items[].detalhe.parcelas` | InadimplenciaParcela[] | sim | Parcelas em atraso da linha (vazio na fonte legada). | | `data.items[].detalhe.parcelas[].codTituloOmie` | string | sim | Código do título no Omie. | | `data.items[].detalhe.parcelas[].numero` | string \| null | sim | Número da parcela (por exemplo `1/3`), quando existe. | | `data.items[].detalhe.parcelas[].vencimento` | string (date) | sim | Vencimento da parcela, `YYYY-MM-DD`. Usa a data de vencimento efetiva (a mais tardia entre o vencimento e a previsão de recebimento do Omie). | | `data.items[].detalhe.parcelas[].valorOriginal` | number | sim | Valor original da parcela, em reais. | | `data.items[].detalhe.parcelas[].valorEmAberto` | number | sim | Saldo em aberto da parcela, em reais. | | `data.items[].detalhe.parcelas[].estado` | string — um de: `OPEN`, `PARTIALLY_PAID` | sim | `OPEN` (em aberto) ou `PARTIALLY_PAID` (parcialmente paga). | | `data.items[].detalhe.parcelas[].ultimaSincronizacao` | string (date-time) | sim | Instante (ISO 8601, UTC) da última sincronização do recebível com o Omie. | | `data.items[].detalhe.parcelas[].compoeTotalFiltrado` | boolean | sim | Sempre `true`: toda parcela devolvida compõe o total filtrado. | | `data.items[].detalhe.ordemServico` | InadimplenciaOrdemServicoDetalhe \| qualquer \| null | sim | `null` quando a linha é de um título sem OS. | | `data.items[].detalhe.contrato` | InadimplenciaContratoDetalhe | sim | | | `data.items[].detalhe.contrato.existeVinculo` | boolean | sim | `false` quando a linha não tem contrato vinculado (então `label` = "Sem contrato vinculado"). | | `data.items[].detalhe.contrato.id` | integer \| null | sim | Id interno do contrato de serviço. | | `data.items[].detalhe.contrato.numero` | string \| null | sim | Número do contrato (`numCtr`). | | `data.items[].detalhe.contrato.label` | string | sim | Rótulo para exibição: número do contrato, senão código Omie, senão `Contrato #`; "Sem contrato vinculado" sem vínculo. | | `data.items[].detalhe.contrato.codCtrOmie` | string \| null | sim | Código do contrato no Omie. | | `data.items[].detalhe.contrato.situacao` | string \| null | sim | Situação do contrato. Só preenchido na fonte legada; sempre `null` na canônica. | | `data.items[].detalhe.contrato.descStatus` | string \| null | sim | Descrição do status do contrato. Só na fonte legada. | | `data.items[].detalhe.contrato.vigenciaInicio` | string (date) \| null | sim | Início da vigência, `YYYY-MM-DD`. Só na fonte legada. | | `data.items[].detalhe.contrato.vigenciaFim` | string (date) \| null | sim | Fim da vigência, `YYYY-MM-DD`. Só na fonte legada. | | `data.items[].detalhe.contrato.valorTotalMensal` | number \| null | sim | Valor mensal total do contrato, em reais. Só na fonte legada. | | `data.items[].detalhe.projeto` | InadimplenciaProjetoDetalhe \| qualquer \| null | sim | `null` quando não há projeto vinculado. | | `data.pagination` | object | sim | | | `data.pagination.page` | integer | sim | Página atual (base 1). | | `data.pagination.pageSize` | integer | sim | Tamanho da página. | | `data.pagination.total` | integer | sim | Total de **linhas** (grupos por OS ou título) que passam nos filtros. Na fonte canônica, quando `page` passa da última página, vem `0` (e `totalPages` = 0); para paginar, pare quando `items` vier com menos de `pageSize` linhas ou quando `page >= totalPages` da página anterior. | | `data.pagination.totalPages` | integer | sim | Total de páginas; 0 quando não há linhas. | Exemplo (dados fictícios): ```json { "data": { "items": [ { "rowKey": "OS:48213", "omieTenantAppId": 3, "origem": "Empresa Alfa Serviços", "codTituloOmie": null, "cpfCnpjCliente": "12345678000190", "ordemServicoId": 48213, "ordemServicoNumero": "000412", "codOsOmie": "7001234567", "contratoId": 912, "contratoLabel": "CTR-0091", "clienteId": "0b6a1d52-3f0e-4c6a-9d0e-5a1f2f6d7a11", "cliente": "Empresa Alfa Ltda", "projetoId": "7e3d1c20-9a54-4d3b-8f21-0c6b5a4d3e22", "projeto": "Implantação Alfa", "produtoId": "5f0c1e7a-8d2b-4a39-b0c4-1e2d3c4b5a69", "produto": "Plataforma Exemplo", "tipoProjetoId": "a1b2c3d4-0000-4000-8000-000000000001", "tipoProjeto": "Implantação", "departamento": "Serviços", "tipoDocumento": "BOL", "status": "OPEN", "statusDescricao": "Recebível em aberto", "vencimento": "2026-09-10", "diasAtraso": 62, "valorEmAtraso": 4500, "historico": false, "catalogoAusente": false, "detalhe": { "tipoDocumento": "BOL", "parcelas": [ { "codTituloOmie": "9988776655", "numero": "1/1", "vencimento": "2026-09-10", "valorOriginal": 4500, "valorEmAberto": 4500, "estado": "OPEN", "ultimaSincronizacao": "2026-11-11T09:30:00.000Z", "compoeTotalFiltrado": true } ], "ordemServico": { "id": 48213, "numero": "000412", "codOsOmie": "7001234567", "status": "OPEN", "statusDescricao": "Recebível em aberto", "vencimento": "2026-09-10", "dataFaturamento": null, "diasAtraso": 62, "valorEmAtraso": 4500, "quantidadeParcelas": 1, "parcelas": [ { "codTituloOmie": "9988776655", "numero": "1/1", "vencimento": "2026-09-10", "valorOriginal": 4500, "valorEmAberto": 4500, "estado": "OPEN", "ultimaSincronizacao": "2026-11-11T09:30:00.000Z", "compoeTotalFiltrado": true } ] }, "contrato": { "existeVinculo": true, "id": 912, "numero": "CTR-0091", "label": "CTR-0091", "codCtrOmie": "6001122334", "situacao": null, "descStatus": null, "vigenciaInicio": null, "vigenciaFim": null, "valorTotalMensal": null }, "projeto": { "id": "7e3d1c20-9a54-4d3b-8f21-0c6b5a4d3e22", "nome": "Implantação Alfa", "clienteId": "0b6a1d52-3f0e-4c6a-9d0e-5a1f2f6d7a11", "cliente": "Empresa Alfa Ltda", "produtoId": "5f0c1e7a-8d2b-4a39-b0c4-1e2d3c4b5a69", "produto": "Plataforma Exemplo", "tipoProjetoId": "a1b2c3d4-0000-4000-8000-000000000001", "tipoProjeto": "Implantação", "departamento": "Serviços" } } } ], "pagination": { "page": 1, "pageSize": 20, "total": 1, "totalPages": 1 } } } ``` #### 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. - **403**: O usuário do token não tem a permissão exigida pela rota. --- ### GET /financeiro/inadimplencia/filter-options/{dimension} **Catálogo paginado de opções para um filtro da inadimplência** · operationId `listarOpcoesFiltroInadimplencia` Autenticação: `Authorization: Bearer `. Devolve as opções (id + rótulo) que alimentam um filtro das rotas `/resumo` e `/itens`. O catálogo é da organização inteira (não só de quem está inadimplente hoje) e, na fonte canônica, `id` é o valor que os filtros esperam. Na fonte legada (itens com `origem = "Legado"`), `cliente`, `projeto`, `tipoProjeto` e `departamento` filtram por nome: envie o `label` em vez do `id`; `empresaOmie`, `produto`, `categoria` e `tipoDocumento` zeram o resultado na fonte legada. **Dimensões aceitas** (`dimension`): `empresaOmie`, `cliente`, `projeto`, `produto`, `tipoProjeto`, `departamento`, `categoria`, `tipoDocumento`. Outro valor responde 400. O que cada uma lista: - `empresaOmie`: cadastros Omie; id numérico em texto, rótulo = apelido (ou `Omie app `). - `cliente`: empresas com relação CLIENTE, não excluídas e com nome fantasia; id UUID. - `projeto`: projetos com nome; id UUID. - `produto` e `tipoProjeto`: id UUID, rótulo = nome. - `departamento`: departamentos do roteamento Omie; id = código do departamento Omie. `SEM_DEPARTAMENTO` (títulos sem roteamento) pode ser enviado no filtro mesmo quando não aparece na lista; ele só é listado quando algum roteamento tem código vazio (e esse caso não casa no filtro). - `categoria`: códigos de categoria Omie existentes nos recebíveis; id = código, rótulo = "código - descrição" quando mapeada. - `tipoDocumento`: tipos de documento existentes nos recebíveis (exceto `REE`, `DAM`, `99999`); id = código, rótulo traduzido (ex.: `BOL - Boleto`). Só entram opções **ativas**; inativas ou excluídas só aparecem se o id vier em `selectedId` (para mostrar uma seleção que ficou obsoleta; `availability` diz o estado). Ordem: rótulo (sem diferenciar caixa) e depois id. **Paginação por cursor:** envie `limit` e, para a página seguinte, o `nextCursor` recebido em `cursor`. Há mais páginas quando `hasMore=true`. Cursor adulterado responde 400 ("Cursor de catálogo inválido."). **Busca:** `q` filtra por trecho do rótulo, sem diferenciar maiúsculas (os caracteres `%` e `_` valem como curingas do SQL `LIKE`). **Permissão:** a mesma das demais rotas de inadimplência (capacidade `VER_BI_INADIMPLENCIA`): administrador global, administrador Financeiro ou tela `/financeiro/inadimplencia` liberada; senão 403. Resposta em `{ "data": { items, hasMore, nextCursor } }`. #### Parâmetros | Nome | Onde | Tipo | Obrigatório | Descrição | |---|---|---|---|---| | `dimension` | path | string — um de: `empresaOmie`, `cliente`, `projeto`, `produto`, `tipoProjeto`, `departamento`, `categoria`, `tipoDocumento` | sim | Dimensão do catálogo. Ex.: `cliente` | | `q` | query | string | não | Busca por trecho do rótulo (1 a 120 caracteres após aparar espaços). Vazio é ignorado; só espaços responde 400. Ex.: `Alfa` | | `cursor` | query | string | não | Cursor opaco devolvido em `nextCursor` (1 a 512 caracteres). | | `limit` | query | integer; padrão `25`; mín. 1; máx. 50 | não | Itens por página (1 a 50); vazio, não inteiro ou fora disso responde 400. Ex.: `25` | | `selectedId` | query | string[] | não | Ids já selecionados; trazem de volta opções inativas (empresa Omie, cliente, produto, tipo de projeto) ou projetos excluídos (cliente excluído nunca volta). Sem efeito em `departamento`, `categoria` e `tipoDocumento`. Repetido ou separado por vírgula; cada id com até 120 caracteres. Ex.: `["0b6a1d52-3f0e-4c6a-9d0e-5a1f2f6d7a11"]` | #### Resposta 200 Página do catálogo. Tipo da raiz: InadimplenciaOpcoesFiltroResponse | Campo | Tipo | Sempre presente | Descrição | |---|---|---|---| | `data` | object | sim | | | `data.items` | InadimplenciaOpcaoFiltro[] | sim | | | `data.items[].id` | string | sim | Valor a enviar no filtro correspondente da consulta. | | `data.items[].label` | string | sim | Rótulo para exibição. | | `data.items[].availability` | string — um de: `ACTIVE`, `INACTIVE`, `DELETED_HISTORICAL` | sim | `ACTIVE`, `INACTIVE` (cadastro inativo) ou `DELETED_HISTORICAL` (projeto excluído). Opções não ativas só aparecem quando seu id foi pedido em `selectedId`. | | `data.hasMore` | boolean | sim | `true` se há mais opções após esta página. | | `data.nextCursor` | string \| null | sim | Cursor opaco para a próxima página; `null` na última. | Exemplo (dados fictícios): ```json { "data": { "items": [ { "id": "0b6a1d52-3f0e-4c6a-9d0e-5a1f2f6d7a11", "label": "Empresa Alfa Ltda", "availability": "ACTIVE" }, { "id": "3c9e8f10-1111-4222-8333-444455556666", "label": "Empresa Beta S.A.", "availability": "ACTIVE" } ], "hasMore": true, "nextCursor": "eyJsYWJlbCI6ImVtcHJlc2EgYmV0YSBzLmEuIiwiaWQiOiIzYzllOGYxMC0xMTExLTQyMjItODMzMy00NDQ0NTU1NTY2NjYifQ" } } ``` #### 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. - **403**: O usuário do token não tem a permissão exigida pela rota. ## Oportunidades Funil comercial (CRM) — oportunidades, etapas, histórico. - `GET /oportunidades` — Lista oportunidades (paginada, com filtros e ordenação) - `GET /oportunidades/{id}` — Detalha uma oportunidade - `GET /oportunidades/{id}/historico` — Linha do tempo (histórico) da oportunidade, com paginação por cursor ### GET /oportunidades **Lista oportunidades (paginada, com filtros e ordenação)** · operationId `listarOportunidades` Autenticação: `Authorization: Bearer `. Lista as oportunidades comerciais (CRM) do tenant do usuário, uma linha por oportunidade, com paginação por página (`page`/`pageSize`), filtros combináveis (todos opcionais, combinados com E) e ordenação. ## Modelo de dados - Uma **oportunidade** pertence a um **pipeline** e está em uma **etapa** dele (`pipeline` → `etapas`, ordenadas por `position`). O `fluxo` do pipeline (`COMERCIAL`, `CS_PARA_COMERCIAL`, `CS_PARA_CS`, `COMERCIAL_PARA_CS`) é herdado pela oportunidade. - **`status`** (ciclo de vida): `ABERTA` (em negociação), `ACEITA` (**ganha**; o enum usa "ACEITA", não "ganha") e `PERDIDA`. As etapas têm `papel` (`NENHUM`, `PROPOSTA_ENVIADA`, `AJUSTE_SOLICITADO`, `ACEITE`, `PERDA`); as etapas de papel `ACEITE` e `PERDA` são as colunas "fechadas" do quadro. - **`tipo`**: `VENDA_NOVA`, `ADITIVO` ou `SERVICO`. - **Pessoas**: `vendedor` é um registro de **vendedor** (tabela de vendedores, não de usuários; seu `id` é o aceito em `vendedorId`). O **CS responsável** (`csResponsavel`, um usuário) só aparece no detalhe (`GET /oportunidades/{id}`). - **`empresa`**: a empresa (cliente ou prospect) dona da oportunidade. `contato` é opcional. - **Valores**: `valorEstimado` é um **número JSON em reais (BRL)**, com até 2 casas decimais (não é string, não é centavos); `null` = sem valor estimado. Datas de calendário (`dataPrevisaoFechamento`) são `YYYY-MM-DD`; instantes (`createdAt`, `entrouNaEtapaEm`) são ISO 8601 em UTC. Dias "civis" (filtros `criadaDe`/`criadaAte`, `atrasada`, `diasNaEtapa`) usam o fuso de São Paulo. ## Quem enxerga o quê (escopo de visibilidade) Não há erro 403 por falta de permissão: o escopo **filtra a lista**. - Usuário com escopo total vê **todas** as oportunidades do tenant. Legado (acesso por setor desligado): papel manager/admin/owner, `type=ADMIN` ou `is_admin` (gestor de qualquer setor). Acesso por setor ligado: Dono/Admin, gestor do Comercial e, com `ESCOPO_COMERCIAL` desligado, gestor de qualquer setor. - Usuário vinculado a **um único vendedor** vê **somente as oportunidades desse vendedor** (`vendedorId` igual ao dele). Com `ESCOPO_COMERCIAL` ligado, o vínculo de vendedor só vale para membro ou gestor do Comercial; os demais caem no perfil restrito. - Qualquer outro usuário (perfil "restrito") vê **somente as oportunidades em que é responsável por alguma tarefa**; nele, `contrato` vem sempre `null`. Filtros como `vendedorId` só reduzem ainda mais o que o escopo já permite. Sempre há isolamento por tenant. ## Filtros Todos opcionais. Combinações sem resultado possível são recusadas com `400` em vez de devolver lista vazia: `semPrevisao` com `previsaoDe`/`previsaoAte`/`atrasada`; `semValor` com `valorMin`; `situacao` com `status`; `criadaDe` maior que `criadaAte`. Parâmetros desconhecidos também geram `400`. Os booleanos de query são as strings `true`/`false`; `false` equivale a "sem filtro" (não filtra pelo oposto). ## Ordenação e paginação `sort` (`criadaEm` padrão, `valor`, `previsao`, `codigo`) e `dir` (`desc` padrão). Em `valor` e `previsao` os nulos ficam por último em qualquer direção; `codigo` ordena pela data de criação/sequência (equivale a `criadaEm`), e o `id` desempata. `page` começa em 1; `pageSize` padrão 25, máximo 100. O corpo traz `total` (de todos os registros que casam com o filtro, sem paginar). ## Campos derivados do item - `diasNaEtapa`: dias civis desde `entrouNaEtapaEm`. - `proximaTarefa`: tarefa aberta de menor prazo (tarefas `ADIADA` não contam; só tarefas que o usuário enxerga); `tarefasVencidas`: abertas com prazo vencido. - `proposta`: selo da versão mais recente da proposta que não é rascunho nem substituída; `termometro`/`termometroMotivo` só existem enquanto a proposta espera o cliente (`ENVIADA` ou `AJUSTE_SOLICITADO`) e a oportunidade está aberta. - `contrato`: selo do contrato da oportunidade. #### Parâmetros | Nome | Onde | Tipo | Obrigatório | Descrição | |---|---|---|---|---| | `search` | query | string | não | Busca textual (sem diferenciar maiúsculas) em código, título, nome fantasia ou razão social da empresa e nome do contato. Máx. 100 caracteres; espaços das pontas são removidos. Ex.: `Alfa` | | `vendedorId` | query | string (uuid) | não | UUID do vendedor (o `vendedor.id` que vem nos itens desta lista). Ex.: `0b8f6a52-3c41-4d0e-9a77-5f2d1c6e8a10` | | `empresaId` | query | string (uuid) | não | UUID da empresa. Ex.: `7d3c2f10-8a4b-4e65-b1d9-2c9e5a7f0b33` | | `produtoId` | query | string (uuid) | não | UUID do produto da oportunidade (o `produto.id` que vem nos itens desta lista). Ex.: `5a1e9c47-2b60-4f8d-8e13-6d0a4b92c7f1` | | `fluxo` | query | string — um de: `COMERCIAL`, `CS_PARA_COMERCIAL`, `CS_PARA_CS`, `COMERCIAL_PARA_CS` | não | Fluxo do pipeline em que a oportunidade está. Ex.: `COMERCIAL` | | `comTarefaVencida` | query | string — um de: `true`, `false` | não | `true` = só oportunidades com pelo menos uma tarefa vencida (entre as tarefas que o usuário enxerga). `false` = sem filtro. Ex.: `true` | | `previsaoDe` | query | string (date) | não | Previsão de fechamento a partir desta data (inclusiva), `YYYY-MM-DD`. Ex.: `2026-11-01` | | `previsaoAte` | query | string (date) | não | Previsão de fechamento até esta data (inclusiva), `YYYY-MM-DD`. Ex.: `2026-12-31` | | `semPrevisao` | query | string — um de: `true`, `false` | não | `true` = só oportunidades sem data de previsão. Não combina com `previsaoDe`, `previsaoAte` nem `atrasada`. `false` = sem filtro. Ex.: `true` | | `atrasada` | query | string — um de: `true`, `false` | não | `true` = só `ABERTA` com previsão de fechamento anterior a hoje (dia civil de São Paulo). `false` = sem filtro. Ex.: `true` | | `situacao` | query | string — um de: `abertas`, `fechadas` | não | `abertas` = status `ABERTA`; `fechadas` = `ACEITA` ou `PERDIDA`. Não combina com `status`. Ex.: `abertas` | | `valorMin` | query | number; mín. 0; máx. 999999999999.99 | não | Valor estimado mínimo, inclusivo, em reais (0 a 999999999999.99). Oportunidades sem valor ficam de fora. Não combina com `semValor`. Atenção: `valorMin=` vazio vira 0 (sem 400) e exclui as sem valor. Ex.: `10000` | | `semValor` | query | string — um de: `true`, `false` | não | `true` = só oportunidades sem valor estimado. Não combina com `valorMin`. `false` = sem filtro. Ex.: `true` | | `criadaDe` | query | string (date) | não | Criada a partir deste dia (inclusivo, dia civil de São Paulo), `YYYY-MM-DD`. Ex.: `2026-10-01` | | `criadaAte` | query | string (date) | não | Criada até este dia (inclusivo, dia civil de São Paulo), `YYYY-MM-DD`. Deve ser maior ou igual a `criadaDe`. Ex.: `2026-10-31` | | `pipelineId` | query | string (uuid) | não | UUID do pipeline (o `pipeline.id` que vem nos itens desta lista). Ex.: `9c4e7a12-5d38-4b0f-a6e1-3f8b2d1c0e55` | | `etapaId` | query | string (uuid) | não | UUID da etapa (ids de etapa vêm em `etapas[].id` do quadro ou em `etapasDoPipeline[].id` do detalhe). Ex.: `2e6b9d03-7c14-4a8f-9b52-1d7f3a0c6e88` | | `status` | query | string — um de: `ABERTA`, `ACEITA`, `PERDIDA` | não | Status exato. Não combina com `situacao`. Ex.: `ABERTA` | | `tipo` | query | string — um de: `VENDA_NOVA`, `ADITIVO`, `SERVICO` | não | Tipo da oportunidade. Ex.: `VENDA_NOVA` | | `projetoId` | query | string (uuid) | não | UUID do projeto vinculado. Ex.: `4f0a8c36-1e92-4d7b-8c05-9a3e6b1d2f70` | | `sort` | query | string — um de: `criadaEm`, `valor`, `previsao`, `codigo`; padrão `"criadaEm"` | não | Campo de ordenação. `valor` e `previsao` põem nulos por último. Ex.: `valor` | | `dir` | query | string — um de: `asc`, `desc`; padrão `"desc"` | não | Direção da ordenação. Ex.: `desc` | | `page` | query | integer; padrão `1`; mín. 1 | não | Número da página, a partir de 1. Ex.: `1` | | `pageSize` | query | integer; padrão `25`; mín. 1; máx. 100 | não | Itens por página (1 a 100). Ex.: `25` | #### Resposta 200 Página de oportunidades. Tipo da raiz: OportunidadeLista | Campo | Tipo | Sempre presente | Descrição | |---|---|---|---| | `items` | qualquer[] | sim | | | `items[].id` | string (uuid) | sim | | | `items[].codigo` | string | sim | Código legível "OP-AAAA-NNNN" (sequência por ano). | | `items[].titulo` | string | sim | | | `items[].tipo` | string — um de: `VENDA_NOVA`, `ADITIVO`, `SERVICO` | sim | | | `items[].status` | string — um de: `ABERTA`, `ACEITA`, `PERDIDA` | sim | ACEITA = ganha. | | `items[].fluxo` | string — um de: `COMERCIAL`, `CS_PARA_COMERCIAL`, `CS_PARA_CS`, `COMERCIAL_PARA_CS` | sim | Herdado do pipeline. | | `items[].etapaId` | string (uuid) | sim | Etapa atual (nome em `etapa.nome` da lista, ou `etapas[].nome` do quadro). | | `items[].position` | integer | sim | Ordem do cartão dentro da etapa (ordem de arraste). | | `items[].valorEstimado` | number \| null | sim | Valor estimado em reais (até 2 casas); nulo = não informado. | | `items[].dataPrevisaoFechamento` | string (date) \| null | sim | YYYY-MM-DD. | | `items[].empresa` | OportunidadeEmpresa | sim | | | `items[].empresa.id` | string (uuid) | sim | | | `items[].empresa.nome` | string | sim | Nome fantasia (ou razão social, ou "(sem nome)"). | | `items[].empresa.cnpj` | string \| null | sim | CNPJ normalizado (14 caracteres, sem pontuação) ou nulo. | | `items[].empresa.relacao` | string — um de: `CLIENTE`, `PROSPECT`, `TERCEIRO` | sim | | | `items[].empresa.ativo` | boolean | sim | Falso para cliente inativado. | | `items[].contato` | OportunidadeContato \| qualquer \| null | sim | | | `items[].vendedor` | OportunidadeVendedorRef | sim | Vendedor da oportunidade (id do cadastro de vendedores, o mesmo de `vendedorId` nos filtros). | | `items[].vendedor.id` | string (uuid) | sim | | | `items[].vendedor.nome` | string | sim | | | `items[].vendedor.iniciais` | string | sim | Primeira letra do primeiro e do último nome, maiúsculas (ex.: "VE"); "?" se sem nome. | | `items[].entrouNaEtapaEm` | string (date-time) | sim | | | `items[].diasNaEtapa` | integer | sim | Dias civis (São Paulo) na etapa atual. | | `items[].proximaTarefa` | OportunidadeProximaTarefa \| qualquer \| null | sim | Tarefa aberta de menor prazo (ADIADA não conta; só as que o usuário enxerga). | | `items[].tarefasVencidas` | integer | sim | | | `items[].proposta` | OportunidadePropostaSelo \| qualquer \| null | sim | | | `items[].termometro` | string — um de: `QUENTE`, `MORNA`, `FRIA`, `null` \| null | sim | Só enquanto a proposta espera o cliente (ENVIADA/AJUSTE_SOLICITADO) e a oportunidade está ABERTA. | | `items[].termometroMotivo` | string — um de: `AJUSTE_SOLICITADO`, `ITEM_ALTERADO`, `ABERTURAS_RECENTES`, `VENCE_SEM_ABERTURA`, `SEM_ABERTURA_RECENTE`, `ABERTA`, `AGUARDANDO_ABERTURA`, `null` \| null | sim | Regra que decidiu o termômetro. QUENTE: ajuste solicitado, item alterado ou 2+ aberturas reais nas últimas 72 h. FRIA: validade em até 3 dias sem abertura, ou sem abertura/entrega há mais de 5 dias. MORNA: o resto. | | `items[].contrato` | OportunidadeContratoSelo \| qualquer \| null | sim | Nulo para o usuário restrito (que não vê contratos). | | `items[].pipeline` | OportunidadePessoaRef | sim | Referência {id, nome}. nome nunca é nulo (nome ausente no cadastro vira "(sem nome)"). | | `items[].pipeline.id` | string (uuid) | sim | | | `items[].pipeline.nome` | string | sim | | | `items[].etapa` | object | sim | | | `items[].etapa.id` | string (uuid) | sim | | | `items[].etapa.nome` | string | sim | | | `items[].etapa.papel` | string — um de: `NENHUM`, `PROPOSTA_ENVIADA`, `AJUSTE_SOLICITADO`, `ACEITE`, `PERDA` | sim | | | `items[].produto` | OportunidadePessoaRef | sim | Referência {id, nome}. nome nunca é nulo (nome ausente no cadastro vira "(sem nome)"). | | `items[].produto.id` | string (uuid) | sim | | | `items[].produto.nome` | string | sim | | | `items[].createdAt` | string (date-time) | sim | | | `page` | integer | sim | | | `pageSize` | integer | sim | | | `total` | integer | sim | Total de registros que casam com os filtros (todas as páginas). | Exemplo (dados fictícios): ```json { "items": [ { "id": "3b7e1a90-6c24-4f58-8d0b-e5a2c91f7d46", "codigo": "OP-2026-0042", "titulo": "Implantação ERP - Empresa Alfa Ltda", "tipo": "VENDA_NOVA", "status": "ABERTA", "fluxo": "COMERCIAL", "etapaId": "2e6b9d03-7c14-4a8f-9b52-1d7f3a0c6e88", "position": 3, "valorEstimado": 48500.5, "dataPrevisaoFechamento": "2026-12-15", "empresa": { "id": "7d3c2f10-8a4b-4e65-b1d9-2c9e5a7f0b33", "nome": "Empresa Alfa Ltda", "cnpj": "12345678000195", "relacao": "PROSPECT", "ativo": true }, "contato": { "id": "c1a4d8e2-0b97-4f36-a5d1-7e2b9c3f6084", "nome": "Contato Exemplo", "cargo": "Diretor Financeiro", "email": "contato@alfa.example", "telefone": "11999990000" }, "vendedor": { "id": "0b8f6a52-3c41-4d0e-9a77-5f2d1c6e8a10", "nome": "Vendedor Exemplo", "iniciais": "VE" }, "entrouNaEtapaEm": "2026-11-03T14:22:10.000Z", "diasNaEtapa": 8, "proximaTarefa": { "id": "a9d2f4b1-3e60-4c78-9b15-0f8e7d6c5a21", "titulo": "Ligar para o contato", "tipo": "LIGACAO", "prazo": "2026-11-12T17:00:00.000Z", "vencida": false }, "tarefasVencidas": 0, "proposta": { "versao": 1, "status": "ENVIADA", "aberturas": 2, "validadeAte": "2026-11-30" }, "termometro": "MORNA", "termometroMotivo": "ABERTA", "contrato": { "situacao": "semContrato", "contratoId": null, "categoria": null, "assinados": null, "total": null, "quantidade": 0, "clienteObrigatorio": false, "prospect": true, "podeGerar": false }, "pipeline": { "id": "9c4e7a12-5d38-4b0f-a6e1-3f8b2d1c0e55", "nome": "Pipeline Comercial" }, "etapa": { "id": "2e6b9d03-7c14-4a8f-9b52-1d7f3a0c6e88", "nome": "Proposta enviada", "papel": "PROPOSTA_ENVIADA" }, "produto": { "id": "5a1e9c47-2b60-4f8d-8e13-6d0a4b92c7f1", "nome": "Produto Exemplo" }, "createdAt": "2026-10-20T12:05:00.000Z" } ], "page": 1, "pageSize": 25, "total": 1 } ``` #### 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 /oportunidades/{id} **Detalha uma oportunidade** · operationId `obterOportunidade` Autenticação: `Authorization: Bearer `. Devolve o detalhe completo de uma oportunidade: dados do cartão mais pipeline e todas as suas etapas (`etapasDoPipeline`), empresa, contato, projeto, produto, tipo de projeto, vendedor, CS responsável, briefing, cláusula especial, motivo de perda, escopo (itens com horas), auditoria de criação e as `permissoes` que o usuário tem sobre ela. - `valorEstimado` e `horasEstimadas` (e `escopo[].horas`) são **números JSON** (reais e horas decimais), `null` quando não informados. - `fechadaEm` só existe quando `status` é `ACEITA` ou `PERDIDA`; `motivoPerda`/`motivoPerdaDescricao` só quando `PERDIDA` (`PRECO`, `CONCORRENTE`, `SEM_ORCAMENTO`, `SEM_RETORNO`, `ADIADO`, `OUTRO`). - `etapa.probabilidade` é um inteiro 0 a 100 (ou `null`), usado no valor ponderado do quadro. - `permissoes` é um reflexo informativo (o usuário desta API é somente leitura): `podeEditar`/`podeMover` exigem status `ABERTA` e que o usuário seja escopo total ou dono (vendedor) da oportunidade; `podeTrocarPipeline` exige gestor do CRM e status `ABERTA`; `podeReabrir` exige gestor do CRM e status `PERDIDA`. Gestor = papel manager/admin/owner no legado; Dono/Admin ou gestor do Comercial no acesso por setor. ## Permissão e escopo Qualquer usuário autenticado, mas só enxerga oportunidades do seu escopo (todas / as do seu vendedor / aquelas em que é responsável por tarefa). Uma oportunidade que existe mas está **fora do escopo** responde `404` com `code = OPORTUNIDADE_NAO_ENCONTRADA` (nunca 403), para não revelar existência. Id que não é UUID responde `400`. #### Parâmetros | Nome | Onde | Tipo | Obrigatório | Descrição | |---|---|---|---|---| | `id` | path | string (uuid) | sim | UUID da oportunidade. Ex.: `3b7e1a90-6c24-4f58-8d0b-e5a2c91f7d46` | #### Resposta 200 Detalhe da oportunidade. Tipo da raiz: OportunidadeDetalhe | Campo | Tipo | Sempre presente | Descrição | |---|---|---|---| | `id` | string (uuid) | sim | | | `codigo` | string | sim | "OP-AAAA-NNNN". | | `numero` | integer | sim | Número sequencial (parte final do código). | | `titulo` | string | sim | | | `tipo` | string — um de: `VENDA_NOVA`, `ADITIVO`, `SERVICO` | sim | | | `status` | string — um de: `ABERTA`, `ACEITA`, `PERDIDA` | sim | | | `pipeline` | OportunidadePessoaRef | sim | Referência {id, nome}. nome nunca é nulo (nome ausente no cadastro vira "(sem nome)"). | | `pipeline.id` | string (uuid) | sim | | | `pipeline.nome` | string | sim | | | `fluxo` | string — um de: `COMERCIAL`, `CS_PARA_COMERCIAL`, `CS_PARA_CS`, `COMERCIAL_PARA_CS` | sim | | | `etapa` | object | sim | | | `etapa.id` | string (uuid) | sim | | | `etapa.nome` | string | sim | | | `etapa.papel` | string — um de: `NENHUM`, `PROPOSTA_ENVIADA`, `AJUSTE_SOLICITADO`, `ACEITE`, `PERDA` | sim | | | `etapa.probabilidade` | integer \| null | sim | 0 a 100. | | `etapasDoPipeline` | object[] | sim | Todas as etapas do pipeline, em ordem. | | `etapasDoPipeline[].id` | string (uuid) | sim | | | `etapasDoPipeline[].nome` | string | sim | | | `etapasDoPipeline[].position` | integer | sim | | | `etapasDoPipeline[].papel` | string — um de: `NENHUM`, `PROPOSTA_ENVIADA`, `AJUSTE_SOLICITADO`, `ACEITE`, `PERDA` | sim | | | `etapasDoPipeline[].cor` | string \| null | sim | | | `entrouNaEtapaEm` | string (date-time) | sim | | | `diasNaEtapa` | integer | sim | | | `empresa` | OportunidadeEmpresa | sim | | | `empresa.id` | string (uuid) | sim | | | `empresa.nome` | string | sim | Nome fantasia (ou razão social, ou "(sem nome)"). | | `empresa.cnpj` | string \| null | sim | CNPJ normalizado (14 caracteres, sem pontuação) ou nulo. | | `empresa.relacao` | string — um de: `CLIENTE`, `PROSPECT`, `TERCEIRO` | sim | | | `empresa.ativo` | boolean | sim | Falso para cliente inativado. | | `contato` | OportunidadeContato \| qualquer \| null | sim | | | `projeto` | OportunidadePessoaRef \| qualquer \| null | sim | Projeto vinculado (nome do projeto). | | `produto` | OportunidadePessoaRef | sim | Referência {id, nome}. nome nunca é nulo (nome ausente no cadastro vira "(sem nome)"). | | `produto.id` | string (uuid) | sim | | | `produto.nome` | string | sim | | | `tipoProjeto` | OportunidadePessoaRef \| qualquer \| null | sim | | | `vendedor` | OportunidadePessoaRef | sim | Referência {id, nome}. nome nunca é nulo (nome ausente no cadastro vira "(sem nome)"). | | `vendedor.id` | string (uuid) | sim | | | `vendedor.nome` | string | sim | | | `csResponsavel` | OportunidadePessoaRef \| qualquer \| null | sim | Usuário de CS responsável (id de usuário). | | `valorEstimado` | number \| null | sim | Reais. | | `horasEstimadas` | number \| null | sim | Horas (decimal). | | `dataPrevisaoFechamento` | string (date) \| null | sim | | | `briefing` | string \| null | sim | | | `clausulaEspecial` | string \| null | sim | Texto livre que, no contrato gerado, prevalece sobre as demais cláusulas. | | `motivoPerda` | string — um de: `PRECO`, `CONCORRENTE`, `SEM_ORCAMENTO`, `SEM_RETORNO`, `ADIADO`, `OUTRO`, `null` \| null | sim | | | `motivoPerdaDescricao` | string \| null | sim | | | `fechadaEm` | string (date-time) \| null | sim | | | `escopo` | object[] | sim | | | `escopo[].id` | string (uuid) | sim | | | `escopo[].ordem` | integer | sim | | | `escopo[].descricao` | string \| null | sim | | | `escopo[].horas` | number \| null | sim | Horas (decimal). | | `escopo[].modulo` | OportunidadePessoaRef \| qualquer \| null | sim | | | `escopo[].subModulo` | OportunidadePessoaRef \| qualquer \| null | sim | | | `createdBy` | OportunidadePessoaRef | sim | Referência {id, nome}. nome nunca é nulo (nome ausente no cadastro vira "(sem nome)"). | | `createdBy.id` | string (uuid) | sim | | | `createdBy.nome` | string | sim | | | `createdAt` | string (date-time) | sim | | | `updatedAt` | string (date-time) | sim | | | `proposta` | OportunidadePropostaSelo \| qualquer \| null | sim | | | `termometro` | string — um de: `QUENTE`, `MORNA`, `FRIA`, `null` \| null | sim | | | `termometroMotivo` | string — um de: `AJUSTE_SOLICITADO`, `ITEM_ALTERADO`, `ABERTURAS_RECENTES`, `VENCE_SEM_ABERTURA`, `SEM_ABERTURA_RECENTE`, `ABERTA`, `AGUARDANDO_ABERTURA`, `null` \| null | sim | | | `contrato` | OportunidadeContratoSelo \| qualquer \| null | sim | Nulo para o usuário restrito. | | `permissoes` | object | sim | | | `permissoes.podeEditar` | boolean | sim | | | `permissoes.podeMover` | boolean | sim | | | `permissoes.podeTrocarPipeline` | boolean | sim | | | `permissoes.podeReabrir` | boolean | sim | | Exemplo (dados fictícios): ```json { "id": "3b7e1a90-6c24-4f58-8d0b-e5a2c91f7d46", "codigo": "OP-2026-0042", "numero": 42, "titulo": "Implantação ERP - Empresa Alfa Ltda", "tipo": "VENDA_NOVA", "status": "ABERTA", "pipeline": { "id": "9c4e7a12-5d38-4b0f-a6e1-3f8b2d1c0e55", "nome": "Pipeline Comercial" }, "fluxo": "COMERCIAL", "etapa": { "id": "2e6b9d03-7c14-4a8f-9b52-1d7f3a0c6e88", "nome": "Proposta enviada", "papel": "PROPOSTA_ENVIADA", "probabilidade": 50 }, "etapasDoPipeline": [ { "id": "1a5c7e90-3b24-4d68-9f01-8e2b4a6c0d13", "nome": "Qualificação", "position": 1, "papel": "NENHUM", "cor": "#9ca3af" }, { "id": "2e6b9d03-7c14-4a8f-9b52-1d7f3a0c6e88", "nome": "Proposta enviada", "position": 3, "papel": "PROPOSTA_ENVIADA", "cor": "#3b82f6" } ], "entrouNaEtapaEm": "2026-11-03T14:22:10.000Z", "diasNaEtapa": 8, "empresa": { "id": "7d3c2f10-8a4b-4e65-b1d9-2c9e5a7f0b33", "nome": "Empresa Alfa Ltda", "cnpj": "12345678000195", "relacao": "PROSPECT", "ativo": true }, "contato": { "id": "c1a4d8e2-0b97-4f36-a5d1-7e2b9c3f6084", "nome": "Contato Exemplo", "cargo": "Diretor Financeiro", "email": "contato@alfa.example", "telefone": "11999990000" }, "projeto": null, "produto": { "id": "5a1e9c47-2b60-4f8d-8e13-6d0a4b92c7f1", "nome": "Produto Exemplo" }, "tipoProjeto": { "id": "8e2d4a60-9b13-4c75-a0f8-1b6c3d7e9f42", "nome": "Implantação" }, "vendedor": { "id": "0b8f6a52-3c41-4d0e-9a77-5f2d1c6e8a10", "nome": "Vendedor Exemplo" }, "csResponsavel": null, "valorEstimado": 48500.5, "horasEstimadas": 120, "dataPrevisaoFechamento": "2026-12-15", "briefing": "Cliente quer migrar do sistema atual até o fim do ano.", "clausulaEspecial": null, "motivoPerda": null, "motivoPerdaDescricao": null, "fechadaEm": null, "escopo": [ { "id": "e7a3c9d1-5b02-4f64-8a1d-3c0b6e9f2d85", "ordem": 1, "descricao": "Treinamento da equipe", "horas": 40, "modulo": { "id": "d3f9b5a7-4c20-4e18-8a6b-0e1c7f2d9b54", "nome": "Módulo Exemplo" }, "subModulo": null } ], "createdBy": { "id": "f4c7a1d9-6e32-4b85-9c0a-2d8e5b1f7a63", "nome": "Usuário Exemplo" }, "createdAt": "2026-10-20T12:05:00.000Z", "updatedAt": "2026-11-03T14:22:10.000Z", "proposta": { "versao": 1, "status": "ENVIADA", "aberturas": 0, "validadeAte": "2026-11-30" }, "termometro": "MORNA", "termometroMotivo": "AGUARDANDO_ABERTURA", "contrato": { "situacao": "semContrato", "contratoId": null, "categoria": null, "assinados": null, "total": null, "quantidade": 0, "clienteObrigatorio": false, "prospect": true, "podeGerar": false }, "permissoes": { "podeEditar": true, "podeMover": true, "podeTrocarPipeline": false, "podeReabrir": false } } ``` #### 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. - **404**: Registro não encontrado (ou de outra organização). --- ### GET /oportunidades/{id}/historico **Linha do tempo (histórico) da oportunidade, com paginação por cursor** · operationId `listarHistoricoOportunidade` Autenticação: `Authorization: Bearer `. Linha do tempo da oportunidade, do **mais recente para o mais antigo** (`createdAt` desc, `id` desc): criação, edições, mudanças de etapa/pipeline, perda/aceite/reabertura, tarefas, propostas (inclusive ações do cliente na proposta pública), notas, contrato e formulário. - Paginação por **cursor**: envie o `nextCursor` recebido como `cursor` na chamada seguinte, com os mesmos demais parâmetros. `nextCursor = null` indica a última página. O cursor é opaco (base64url); um cursor malformado gera `400` com `message` em string única (`"Cursor inválido."`), não na lista `campo: motivo` dos demais erros de validação. `limit` padrão 30, máximo 100. - `origem` filtra pela origem do evento (`OPORTUNIDADE`, `TAREFA`, `PROPOSTA`, `NOTA`, `CONTRATO`, `FORMULARIO`). - Notas excluídas não aparecem. - Cada item: `acao` é um código textual (ex.: `CRIADA`, `EDITADA`, `ETAPA_ALTERADA`, `PIPELINE_ALTERADO`, `ACEITA`, `PERDIDA`, `REABERTA`, `NOTA_ADICIONADA`, `TAREFA_CRIADA`, `TAREFA_CONCLUIDA`, `PROPOSTA_ENVIADA`, `ANEXO_ADICIONADO`...; a lista não é fechada, trate valores desconhecidos como texto); `dados` é um objeto JSON livre com os detalhes da ação (depende de `acao`; pode ser `null`); `texto` é o conteúdo livre (por exemplo, o texto da nota). `autor` é o usuário que agiu; `null` quando quem agiu foi o cliente na proposta pública (aí `destinatario` traz o contato) ou o próprio sistema (expiração automática, eventos do provedor de assinatura, falha técnica do formulário). Nesse último caso `destinatario` também é `null`. `editadaEm`/`editadaPor` só existem em nota editada. `podeAlterar` indica se o usuário poderia editar/excluir a nota (só `NOTA_ADICIONADA`). ## Permissão e escopo Qualquer usuário autenticado que enxergue a oportunidade (mesmo escopo do detalhe); fora do escopo ou inexistente = `404` `OPORTUNIDADE_NAO_ENCONTRADA`. Para o perfil **restrito** (só vê oportunidades em que tem tarefa), eventos de origem `CONTRATO` e formulários nascidos de contrato são omitidos, e `origem=CONTRATO` devolve lista vazia. #### Parâmetros | Nome | Onde | Tipo | Obrigatório | Descrição | |---|---|---|---|---| | `id` | path | string (uuid) | sim | UUID da oportunidade. Ex.: `3b7e1a90-6c24-4f58-8d0b-e5a2c91f7d46` | | `origem` | query | string — um de: `OPORTUNIDADE`, `TAREFA`, `PROPOSTA`, `NOTA`, `CONTRATO`, `FORMULARIO` | não | Filtra pela origem do evento. Ex.: `NOTA` | | `cursor` | query | string | não | Cursor opaco devolvido em `nextCursor` da página anterior. Ex.: `WyIyMDI2LTExLTAzVDE0OjIyOjEwLjAwMFoiLCI3YzJhOWYxMC00ZDM4LTRiNjUtOGUwMS0zYTVjN2U5ZDJiNDYiXQ` | | `limit` | query | integer; padrão `30`; mín. 1; máx. 100 | não | Itens por página (1 a 100). Ex.: `30` | #### Resposta 200 Página do histórico. Tipo da raiz: OportunidadeHistoricoPagina | Campo | Tipo | Sempre presente | Descrição | |---|---|---|---| | `items` | OportunidadeHistoricoItem[] | sim | | | `items[].id` | string (uuid) | sim | | | `items[].origem` | string — um de: `OPORTUNIDADE`, `TAREFA`, `PROPOSTA`, `NOTA`, `CONTRATO`, `FORMULARIO` | sim | | | `items[].acao` | string | sim | Código da ação (ex.: CRIADA, ETAPA_ALTERADA, NOTA_ADICIONADA). Lista não fechada. | | `items[].entidadeTipo` | string \| null | sim | Tipo da entidade relacionada (ex.: "proposta", "oportunidade_anexo"), quando houver. | | `items[].entidadeId` | string (uuid) \| null | sim | | | `items[].texto` | string \| null | sim | | | `items[].dados` | object \| null | sim | Detalhes da ação; a forma depende de `acao`. | | `items[].autor` | OportunidadePessoaRef \| qualquer \| null | sim | Usuário que agiu; nulo quando quem agiu foi o cliente na proposta pública (aí `destinatario` traz o contato) ou o próprio sistema (expiração automática, eventos do provedor de assinatura, falha técnica do formulário; aí `destinatario` também é nulo). | | `items[].destinatario` | OportunidadePessoaRef \| qualquer \| null | sim | Contato do cliente que agiu na proposta (só id e nome). | | `items[].createdAt` | string (date-time) | sim | | | `items[].editadaEm` | string (date-time) \| null | sim | | | `items[].editadaPor` | OportunidadePessoaRef \| qualquer \| null | sim | | | `items[].podeAlterar` | boolean | sim | Só verdadeiro em `NOTA_ADICIONADA` do próprio autor (ou escopo total). | | `nextCursor` | string \| null | sim | Cursor para a próxima página; nulo na última. | Exemplo (dados fictícios): ```json { "items": [ { "id": "7c2a9f10-4d38-4b65-8e01-3a5c7e9d2b46", "origem": "NOTA", "acao": "NOTA_ADICIONADA", "entidadeTipo": null, "entidadeId": null, "texto": "Cliente pediu desconto no setup.", "dados": null, "autor": { "id": "f4c7a1d9-6e32-4b85-9c0a-2d8e5b1f7a63", "nome": "Usuário Exemplo" }, "destinatario": null, "createdAt": "2026-11-03T14:22:10.000Z", "editadaEm": null, "editadaPor": null, "podeAlterar": true }, { "id": "1d8b4e70-9a25-4c63-b0f7-5e2a8c1d3f94", "origem": "OPORTUNIDADE", "acao": "ETAPA_ALTERADA", "entidadeTipo": null, "entidadeId": null, "texto": null, "dados": { "de": { "id": "1a5c7e90-3b24-4d68-9f01-8e2b4a6c0d13", "nome": "Qualificação", "papel": "NENHUM" }, "para": { "id": "2e6b9d03-7c14-4a8f-9b52-1d7f3a0c6e88", "nome": "Proposta enviada", "papel": "PROPOSTA_ENVIADA" }, "origem": "MANUAL", "posicao": 0, "pendencias": [] }, "autor": { "id": "f4c7a1d9-6e32-4b85-9c0a-2d8e5b1f7a63", "nome": "Usuário Exemplo" }, "destinatario": null, "createdAt": "2026-11-01T10:00:00.000Z", "editadaEm": null, "editadaPor": null, "podeAlterar": false } ], "nextCursor": null } ``` #### 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. - **404**: Registro não encontrado (ou de outra organização). ## Metas Metas comerciais e o realizado contra elas. - `GET /metas` — Lista as metas (objetivos) de vendas de um ano - `GET /metas/realizados` — Vendas realizadas que contam para as metas (lista paginada) - `GET /metas/dashboard` — Painel anual de metas x realizado, por produto e classe ### GET /metas **Lista as metas (objetivos) de vendas de um ano** · operationId `listarMetas` Autenticação: `Authorization: Bearer `. Devolve as **metas cadastradas** do tenant para um ano. Uma meta é um **objetivo mensal por produto e classe**: a combinação (`produtoId`, `classe`, `ano`, `mes`) é única no tenant. **Não há meta por vendedor ou por equipe**: as metas são globais ao tenant. - `classe`: `NOVO` (venda nova) ou `ADITIVO` (aditivo a cliente existente). É a classificação do tipo de projeto da venda: `ADITIVO` = tipo "Aditivo"; `NOVO` = "Implantação e Consultoria" ou "CS" (nomes comparados sem acento, sem diferenciar maiúsculas e sem espaços nas pontas). Qualquer outro tipo (ex.: "Serviço de base") nunca entra no realizado. - `quantidadeMeta`: número inteiro de vendas desejadas no mês. No dashboard, vale tanto para MRR quanto para serviço. - `valorSaasMeta`: meta de **MRR** (receita recorrente mensal do SaaS) do mês, em **reais**, número JSON (não string, não centavos). - `valorServicoMeta`: meta de **valor de serviço** (implantação/setup, receita não recorrente) do mês, em **reais**, número JSON. - `mes`: 1 a 12. Só existem linhas para os meses efetivamente cadastrados (meses sem meta não aparecem aqui). - Ordem: nome do produto, depois `classe`, depois `mes`. O corpo é um **array simples** (sem paginação). Esta rota traz só os **objetivos**; o **realizado** vem de `GET /metas/dashboard` (agregado) e `GET /metas/realizados` (vendas uma a uma). Escopo de dados: metas não são filtradas por vendedor. ## Permissão Qualquer usuário autenticado do tenant: o código não aplica checagem de capacidade nem de tela nesta rota (a leitura dos *realizados* é que é restrita ao Comercial/Administração). Não há `403` específico. #### Parâmetros | Nome | Onde | Tipo | Obrigatório | Descrição | |---|---|---|---|---| | `ano` | query | integer; mín. 1 | sim | Ano das metas (inteiro maior ou igual a 1). Ex.: `2026` | | `produtoId` | query | string (uuid) | não | Restringe a um produto (UUID; o mesmo `produto.id` dos itens de `GET /oportunidades`). Ex.: `5a1e9c47-2b60-4f8d-8e13-6d0a4b92c7f1` | | `classe` | query | string — um de: `ADITIVO`, `NOVO` | não | Restringe a uma classe de meta. Ex.: `NOVO` | #### Resposta 200 Metas do ano (array, possivelmente vazio). Tipo da raiz: MetasMeta[] | Campo | Tipo | Sempre presente | Descrição | |---|---|---|---| | `[].id` | string (uuid) | sim | | | `[].produtoId` | string (uuid) | sim | | | `[].produtoNome` | string | sim | Vazio se o produto não tem nome. | | `[].classe` | string — um de: `ADITIVO`, `NOVO` | sim | | | `[].ano` | integer | sim | | | `[].mes` | integer | sim | | | `[].quantidadeMeta` | integer | sim | Nº de vendas desejadas no mês. | | `[].valorSaasMeta` | number | sim | Meta de MRR do mês, em reais. | | `[].valorServicoMeta` | number | sim | Meta de valor de serviço do mês, em reais. | Exemplo (dados fictícios): ```json [ { "id": "a3e5c7b9-1d20-4f46-8b68-0c2e4a6d8f10", "produtoId": "5a1e9c47-2b60-4f8d-8e13-6d0a4b92c7f1", "produtoNome": "Produto Exemplo", "classe": "NOVO", "ano": 2026, "mes": 1, "quantidadeMeta": 2, "valorSaasMeta": 3000, "valorServicoMeta": 10000 }, { "id": "b4f6d8ca-2e31-4057-9c79-1d3f5b7e9a21", "produtoId": "5a1e9c47-2b60-4f8d-8e13-6d0a4b92c7f1", "produtoNome": "Produto Exemplo", "classe": "NOVO", "ano": 2026, "mes": 2, "quantidadeMeta": 2, "valorSaasMeta": 3000, "valorServicoMeta": 10000 } ] ``` #### 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 /metas/realizados **Vendas realizadas que contam para as metas (lista paginada)** · operationId `listarMetasRealizados` Autenticação: `Authorization: Bearer `. Lista, uma a uma, as **vendas realizadas** que alimentam o "realizado" das metas. É o detalhe do que `GET /metas/dashboard` soma, com os mesmos filtros e a mesma regra, de modo que os totais reconciliam. ## De onde vem o realizado Fonte exclusiva: os **formulários comerciais** (`formulario_comercial`). Um formulário entra quando: - está `CONCLUIDO`; - é **venda do Comercial** (foto gravada na conclusão; sem foto, vale o cadastro atual do vendedor: vendedor não comissionado fica de fora); - tem **produto principal** (o produto marcado como principal; ele define `produtoId`/`produtoNome`); - o tipo de projeto o classifica como `ADITIVO` (tipo "Aditivo") ou `NOVO` ("Implantação e Consultoria" ou "CS"; nomes comparados sem acento, sem diferenciar maiúsculas e sem espaços nas pontas). Formulário sem classe (ex.: "Serviço de base") nunca entra, mesmo sem filtro de `classe`. Cada formulário pode gerar até **dois eventos**, conforme `metrica`: `MRR` (usa a data de assinatura do SaaS e o valor mensal do SaaS, `valorSaasMensal` > 0) e `SERVICO` (usa a data de assinatura do serviço e o valor de serviço, `valorServico` > 0). O evento só existe se o valor correspondente for positivo. O ano/mês é o da **data de assinatura de cada evento** (janela UTC `[início, fim)`), não da data de criação. Nesta lista, um formulário com os dois eventos aparece **uma única vez** (o evento de data mais recente; em empate, `MRR`). `total` conta formulários distintos. No dashboard, os mesmos formulários são contados em cada métrica (podem entrar em MRR e em serviço). ## Unidades `valorSaasMensal`, `valorServico`, `valorComissao` e `os.valorTotal` são **números JSON em reais**, `null` quando ausentes. As datas são instantes ISO 8601 em UTC (datas de assinatura normalmente à meia-noite UTC). `dataVenda` e `dataAssinatura` são iguais e correspondem à data de referência do evento exibido. `os` é a primeira ordem de serviço paga ligada à comissão da venda (`null` se não houver); `os.pagamentoStatus`: `PAGA`, `PARCIAL`, `PENDENTE` ou `SEM_RECEBIVEL`. ## Filtros e paginação `ano` obrigatório; `mes`, `vendedorId`, `produtoId`, `classe`, `metrica` opcionais. Ordenação fixa: `dataVenda` desc, `id` desc. `page` a partir de 1, `pageSize` padrão 20, máximo 200; `pagination.totalPages = ceil(total / pageSize)`. ## Permissão e escopo Exige a capacidade **`LER_METAS`**: na regra legada, `type` do usuário igual a `ADMIN` ou `COMERCIAL` (papel de dono/admin ou gestor de outro setor não basta); com o acesso por setor ligado, dono/administrador ou membro/gestor do setor Comercial. Sem ela: `403`. Além disso, o **escopo de dados** (`META`) restringe as linhas: escopo total vê todos os vendedores; usuário vinculado a um vendedor vê **somente as vendas dele** (`vendedorId` do filtro só reduz ainda mais); sem escopo, a lista vem vazia. Escopo total: administrador, gestor e qualquer usuário fora do Comercial (com `ESCOPO_COMERCIAL` desligado, o padrão); só o usuário do Comercial que não é gestor fica restrito ao seu vendedor. Com `ACESSO_POR_SETOR` e `ESCOPO_COMERCIAL` ligados: Dono/Admin e gestor do Comercial veem tudo; membro do Comercial vê só o seu vendedor. #### Parâmetros | Nome | Onde | Tipo | Obrigatório | Descrição | |---|---|---|---|---| | `ano` | query | integer; mín. 1 | sim | Ano das assinaturas (inteiro maior ou igual a 1). Ex.: `2026` | | `mes` | query | integer; mín. 1; máx. 12 | não | Mês das assinaturas (1 a 12). Sem ele, o ano inteiro. Ex.: `11` | | `vendedorId` | query | string (uuid) | não | UUID do vendedor da venda. Ex.: `0b8f6a52-3c41-4d0e-9a77-5f2d1c6e8a10` | | `produtoId` | query | string (uuid) | não | UUID do produto principal da venda. Ex.: `5a1e9c47-2b60-4f8d-8e13-6d0a4b92c7f1` | | `classe` | query | string — um de: `ADITIVO`, `NOVO` | não | Classe da venda. Ex.: `NOVO` | | `metrica` | query | string — um de: `MRR`, `SERVICO` | não | Só eventos desta métrica: `MRR` (recorrente) ou `SERVICO` (implantação/serviço). Ex.: `MRR` | | `page` | query | integer; padrão `1`; mín. 1 | não | Número da página, a partir de 1. Ex.: `1` | | `pageSize` | query | integer; padrão `20`; mín. 1; máx. 200 | não | Itens por página (1 a 200). Ex.: `20` | #### Resposta 200 Página de vendas realizadas. Tipo da raiz: MetasRealizadosLista | Campo | Tipo | Sempre presente | Descrição | |---|---|---|---| | `data` | MetasRealizadoVenda[] | sim | | | `data[].id` | string (uuid) | sim | Id do formulário comercial. | | `data[].origem` | string — um de: `FORMULARIO` | sim | | | `data[].status` | string — um de: `CONCLUIDO` | sim | | | `data[].classe` | string — um de: `ADITIVO`, `NOVO` | sim | | | `data[].vendedorId` | string (uuid) | sim | | | `data[].vendedorNome` | string \| null | sim | | | `data[].clienteNome` | string \| null | sim | | | `data[].projetoId` | string (uuid) \| null | sim | | | `data[].projetoNome` | string \| null | sim | | | `data[].produtoId` | string (uuid) | sim | Produto principal. | | `data[].produtoNome` | string \| null | sim | | | `data[].tipoProjetoId` | string (uuid) \| null | sim | | | `data[].tipoProjetoNome` | string \| null | sim | | | `data[].metrica` | string — um de: `MRR`, `SERVICO` | sim | Evento exibido (o de data mais recente quando o formulário tem os dois). | | `data[].dataVenda` | string (date-time) | sim | Data de referência do evento (igual a `dataAssinatura`). | | `data[].dataAssinatura` | string (date-time) | sim | | | `data[].dataAssinaturaSaas` | string (date-time) \| null | sim | | | `data[].dataAssinaturaServico` | string (date-time) \| null | sim | | | `data[].valorSaasMensal` | number \| null | sim | MRR do SaaS, em reais. | | `data[].valorServico` | number \| null | sim | Valor de serviço, em reais. | | `data[].valorComissao` | number \| null | sim | Comissão da venda, em reais. | | `data[].os` | MetasRealizadoOs \| qualquer \| null | sim | Primeira OS paga ligada à comissão. | | `pagination` | object | sim | | | `pagination.page` | integer | sim | | | `pagination.pageSize` | integer | sim | | | `pagination.total` | integer | sim | | | `pagination.totalPages` | integer | sim | | Exemplo (dados fictícios): ```json { "data": [ { "id": "c5a7e9d1-3f42-4168-8a8a-2e4c6b8d0f32", "origem": "FORMULARIO", "status": "CONCLUIDO", "classe": "NOVO", "vendedorId": "0b8f6a52-3c41-4d0e-9a77-5f2d1c6e8a10", "vendedorNome": "Vendedor Exemplo", "clienteNome": "Empresa Alfa Ltda", "projetoId": "4f0a8c36-1e92-4d7b-8c05-9a3e6b1d2f70", "projetoNome": "Implantação Alfa", "produtoId": "5a1e9c47-2b60-4f8d-8e13-6d0a4b92c7f1", "produtoNome": "Produto Exemplo", "tipoProjetoId": "8e2d4a60-9b13-4c75-a0f8-1b6c3d7e9f42", "tipoProjetoNome": "Implantação", "metrica": "MRR", "dataVenda": "2026-11-05T00:00:00.000Z", "dataAssinatura": "2026-11-05T00:00:00.000Z", "dataAssinaturaSaas": "2026-11-05T00:00:00.000Z", "dataAssinaturaServico": "2026-11-03T00:00:00.000Z", "valorSaasMensal": 1500, "valorServico": 8000, "valorComissao": 450.75, "os": { "id": 1234, "numOs": "000123", "valorTotal": 8000, "dataFaturamento": "2026-11-10T00:00:00.000Z", "dataVencimento": "2026-11-25T00:00:00.000Z", "pagamentoStatus": "PENDENTE", "dataPagamento": null } } ], "pagination": { "page": 1, "pageSize": 20, "total": 1, "totalPages": 1 } } ``` #### 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. - **403**: O usuário do token não tem a permissão exigida pela rota. --- ### GET /metas/dashboard **Painel anual de metas x realizado, por produto e classe** · operationId `obterMetasDashboard` Autenticação: `Authorization: Bearer `. Para um ano, cruza as **metas** cadastradas com o **realizado** e devolve uma linha por (produto, classe), cada uma com os 12 meses e os 4 trimestres. É a pergunta "estamos batendo as metas?". ## Como é calculado - **Meta** (por mês): de `GET /metas`. `quantidadeMrr` e `quantidadeServico` recebem a mesma `quantidadeMeta`; `saas` = `valorSaasMeta` (MRR em reais); `servico` = `valorServicoMeta` (reais). Mês sem meta cadastrada = tudo 0. - **Realizado** (por mês): mesma fonte e regras de `GET /metas/realizados` (formulários comerciais concluídos, venda do Comercial, produto principal, classe ADITIVO/NOVO; ver aquela rota). `quantidadeMrr` = nº de vendas com assinatura SaaS no mês e MRR positivo; `saas` = soma do MRR mensal dessas vendas (reais); `quantidadeServico` = nº de vendas com assinatura de serviço no mês e valor de serviço positivo; `servico` = soma desses valores (reais). O mês é o da data de assinatura (UTC). Não há ligação com faturamento ou pagamento: venda assinada e não paga também conta. - **`atingimento`**: percentual **de 0 a 100 ou mais** (não é fração 0-1) = `realizado / meta × 100`, arredondado a 1 casa decimal, em cada uma das 4 medidas. **Meta 0 resulta em 0** (não nulo, não infinito), mesmo que haja realizado. - **Trimestres** (`tri` 1 a 4 = jan-mar, abr-jun, jul-set, out-dez): soma dos 3 meses de meta e de realizado; o `atingimento` é recalculado sobre as somas (não é média dos meses). - Só aparecem linhas (produto, classe) que tenham meta **ou** realizado no ano; cada linha sempre traz os 12 meses e 4 trimestres (zerados quando vazios). Ordenação: nome do produto, depois classe. Realizado de formulário sem classe ou sem produto principal não aparece. ## Permissão e escopo Qualquer usuário autenticado: não há checagem de capacidade nesta rota (diferente de `/metas/realizados`, que dá `403`). As **metas** são globais do tenant e aparecem para todos; o **realizado** respeita o escopo de dados `META`: escopo total soma todos os vendedores; usuário vinculado a um vendedor soma só as vendas dele; sem escopo, o realizado vem zerado (não há erro). Dois usuários podem, portanto, ver o mesmo `meta` com `realizado` e `atingimento` diferentes. Escopo total do realizado: administrador, gestor e qualquer usuário fora do Comercial (com `ESCOPO_COMERCIAL` desligado, o padrão). Só o usuário do Comercial que não é gestor fica restrito ao seu vendedor. Com `ACESSO_POR_SETOR` e `ESCOPO_COMERCIAL` ligados: Dono/Admin e gestor do Comercial veem tudo; membro do Comercial vê só o seu vendedor; os demais, nada. #### Parâmetros | Nome | Onde | Tipo | Obrigatório | Descrição | |---|---|---|---|---| | `ano` | query | integer; mín. 1 | sim | Ano do painel (inteiro maior ou igual a 1). Ex.: `2026` | #### Resposta 200 Painel do ano. Tipo da raiz: MetasDashboard | Campo | Tipo | Sempre presente | Descrição | |---|---|---|---| | `ano` | integer | sim | | | `linhas` | object[] | sim | | | `linhas[].produtoId` | string (uuid) | sim | | | `linhas[].produtoNome` | string | sim | Vazio se o produto não tem nome. | | `linhas[].classe` | string — um de: `ADITIVO`, `NOVO` | sim | | | `linhas[].meses` | MetasDashboardMes[] | sim | | | `linhas[].meses[].mes` | integer | sim | | | `linhas[].meses[].meta` | MetasDashboardValores | sim | Quatro medidas. Em `meta` e `realizado` são quantidades e reais; em `atingimento` são percentuais (0 a 100 ou mais, 1 casa decimal; 0 quando a meta é 0). | | `linhas[].meses[].meta.quantidadeMrr` | number | sim | Nº de vendas com MRR (inteiro em meta/realizado). | | `linhas[].meses[].meta.saas` | number | sim | MRR em reais. | | `linhas[].meses[].meta.quantidadeServico` | number | sim | Nº de vendas com valor de serviço. | | `linhas[].meses[].meta.servico` | number | sim | Valor de serviço em reais. | | `linhas[].meses[].realizado` | MetasDashboardValores | sim | Quatro medidas. Em `meta` e `realizado` são quantidades e reais; em `atingimento` são percentuais (0 a 100 ou mais, 1 casa decimal; 0 quando a meta é 0). | | `linhas[].meses[].realizado.quantidadeMrr` | number | sim | Nº de vendas com MRR (inteiro em meta/realizado). | | `linhas[].meses[].realizado.saas` | number | sim | MRR em reais. | | `linhas[].meses[].realizado.quantidadeServico` | number | sim | Nº de vendas com valor de serviço. | | `linhas[].meses[].realizado.servico` | number | sim | Valor de serviço em reais. | | `linhas[].meses[].atingimento` | MetasDashboardValores | sim | Quatro medidas. Em `meta` e `realizado` são quantidades e reais; em `atingimento` são percentuais (0 a 100 ou mais, 1 casa decimal; 0 quando a meta é 0). | | `linhas[].meses[].atingimento.quantidadeMrr` | number | sim | Nº de vendas com MRR (inteiro em meta/realizado). | | `linhas[].meses[].atingimento.saas` | number | sim | MRR em reais. | | `linhas[].meses[].atingimento.quantidadeServico` | number | sim | Nº de vendas com valor de serviço. | | `linhas[].meses[].atingimento.servico` | number | sim | Valor de serviço em reais. | | `linhas[].trimestres` | MetasDashboardTrimestre[] | sim | | | `linhas[].trimestres[].tri` | integer | sim | | | `linhas[].trimestres[].meta` | MetasDashboardValores | sim | Quatro medidas. Em `meta` e `realizado` são quantidades e reais; em `atingimento` são percentuais (0 a 100 ou mais, 1 casa decimal; 0 quando a meta é 0). | | `linhas[].trimestres[].meta.quantidadeMrr` | number | sim | Nº de vendas com MRR (inteiro em meta/realizado). | | `linhas[].trimestres[].meta.saas` | number | sim | MRR em reais. | | `linhas[].trimestres[].meta.quantidadeServico` | number | sim | Nº de vendas com valor de serviço. | | `linhas[].trimestres[].meta.servico` | number | sim | Valor de serviço em reais. | | `linhas[].trimestres[].realizado` | MetasDashboardValores | sim | Quatro medidas. Em `meta` e `realizado` são quantidades e reais; em `atingimento` são percentuais (0 a 100 ou mais, 1 casa decimal; 0 quando a meta é 0). | | `linhas[].trimestres[].realizado.quantidadeMrr` | number | sim | Nº de vendas com MRR (inteiro em meta/realizado). | | `linhas[].trimestres[].realizado.saas` | number | sim | MRR em reais. | | `linhas[].trimestres[].realizado.quantidadeServico` | number | sim | Nº de vendas com valor de serviço. | | `linhas[].trimestres[].realizado.servico` | number | sim | Valor de serviço em reais. | | `linhas[].trimestres[].atingimento` | MetasDashboardValores | sim | Quatro medidas. Em `meta` e `realizado` são quantidades e reais; em `atingimento` são percentuais (0 a 100 ou mais, 1 casa decimal; 0 quando a meta é 0). | | `linhas[].trimestres[].atingimento.quantidadeMrr` | number | sim | Nº de vendas com MRR (inteiro em meta/realizado). | | `linhas[].trimestres[].atingimento.saas` | number | sim | MRR em reais. | | `linhas[].trimestres[].atingimento.quantidadeServico` | number | sim | Nº de vendas com valor de serviço. | | `linhas[].trimestres[].atingimento.servico` | number | sim | Valor de serviço em reais. | Exemplo (dados fictícios): ```json { "ano": 2026, "linhas": [ { "produtoId": "5a1e9c47-2b60-4f8d-8e13-6d0a4b92c7f1", "produtoNome": "Produto Exemplo", "classe": "NOVO", "meses": [ { "mes": 1, "meta": { "quantidadeMrr": 2, "saas": 3000, "quantidadeServico": 2, "servico": 10000 }, "realizado": { "quantidadeMrr": 2, "saas": 3200, "quantidadeServico": 1, "servico": 9000 }, "atingimento": { "quantidadeMrr": 100, "saas": 106.7, "quantidadeServico": 50, "servico": 90 } }, { "mes": 2, "meta": { "quantidadeMrr": 2, "saas": 3000, "quantidadeServico": 2, "servico": 10000 }, "realizado": { "quantidadeMrr": 1, "saas": 1500, "quantidadeServico": 2, "servico": 12000 }, "atingimento": { "quantidadeMrr": 50, "saas": 50, "quantidadeServico": 100, "servico": 120 } }, { "mes": 3, "meta": { "quantidadeMrr": 2, "saas": 3000, "quantidadeServico": 2, "servico": 10000 }, "realizado": { "quantidadeMrr": 3, "saas": 4500, "quantidadeServico": 3, "servico": 15000 }, "atingimento": { "quantidadeMrr": 150, "saas": 150, "quantidadeServico": 150, "servico": 150 } }, { "mes": 4, "meta": { "quantidadeMrr": 2, "saas": 3000, "quantidadeServico": 2, "servico": 10000 }, "realizado": { "quantidadeMrr": 0, "saas": 0, "quantidadeServico": 0, "servico": 0 }, "atingimento": { "quantidadeMrr": 0, "saas": 0, "quantidadeServico": 0, "servico": 0 } }, { "mes": 5, "meta": { "quantidadeMrr": 2, "saas": 3000, "quantidadeServico": 2, "servico": 10000 }, "realizado": { "quantidadeMrr": 0, "saas": 0, "quantidadeServico": 0, "servico": 0 }, "atingimento": { "quantidadeMrr": 0, "saas": 0, "quantidadeServico": 0, "servico": 0 } }, { "mes": 6, "meta": { "quantidadeMrr": 2, "saas": 3000, "quantidadeServico": 2, "servico": 10000 }, "realizado": { "quantidadeMrr": 0, "saas": 0, "quantidadeServico": 0, "servico": 0 }, "atingimento": { "quantidadeMrr": 0, "saas": 0, "quantidadeServico": 0, "servico": 0 } }, { "mes": 7, "meta": { "quantidadeMrr": 2, "saas": 3000, "quantidadeServico": 2, "servico": 10000 }, "realizado": { "quantidadeMrr": 0, "saas": 0, "quantidadeServico": 0, "servico": 0 }, "atingimento": { "quantidadeMrr": 0, "saas": 0, "quantidadeServico": 0, "servico": 0 } }, { "mes": 8, "meta": { "quantidadeMrr": 2, "saas": 3000, "quantidadeServico": 2, "servico": 10000 }, "realizado": { "quantidadeMrr": 0, "saas": 0, "quantidadeServico": 0, "servico": 0 }, "atingimento": { "quantidadeMrr": 0, "saas": 0, "quantidadeServico": 0, "servico": 0 } }, { "mes": 9, "meta": { "quantidadeMrr": 2, "saas": 3000, "quantidadeServico": 2, "servico": 10000 }, "realizado": { "quantidadeMrr": 0, "saas": 0, "quantidadeServico": 0, "servico": 0 }, "atingimento": { "quantidadeMrr": 0, "saas": 0, "quantidadeServico": 0, "servico": 0 } }, { "mes": 10, "meta": { "quantidadeMrr": 2, "saas": 3000, "quantidadeServico": 2, "servico": 10000 }, "realizado": { "quantidadeMrr": 0, "saas": 0, "quantidadeServico": 0, "servico": 0 }, "atingimento": { "quantidadeMrr": 0, "saas": 0, "quantidadeServico": 0, "servico": 0 } }, { "mes": 11, "meta": { "quantidadeMrr": 2, "saas": 3000, "quantidadeServico": 2, "servico": 10000 }, "realizado": { "quantidadeMrr": 0, "saas": 0, "quantidadeServico": 0, "servico": 0 }, "atingimento": { "quantidadeMrr": 0, "saas": 0, "quantidadeServico": 0, "servico": 0 } }, { "mes": 12, "meta": { "quantidadeMrr": 2, "saas": 3000, "quantidadeServico": 2, "servico": 10000 }, "realizado": { "quantidadeMrr": 0, "saas": 0, "quantidadeServico": 0, "servico": 0 }, "atingimento": { "quantidadeMrr": 0, "saas": 0, "quantidadeServico": 0, "servico": 0 } } ], "trimestres": [ { "tri": 1, "meta": { "quantidadeMrr": 6, "saas": 9000, "quantidadeServico": 6, "servico": 30000 }, "realizado": { "quantidadeMrr": 6, "saas": 9200, "quantidadeServico": 6, "servico": 36000 }, "atingimento": { "quantidadeMrr": 100, "saas": 102.2, "quantidadeServico": 100, "servico": 120 } }, { "tri": 2, "meta": { "quantidadeMrr": 6, "saas": 9000, "quantidadeServico": 6, "servico": 30000 }, "realizado": { "quantidadeMrr": 0, "saas": 0, "quantidadeServico": 0, "servico": 0 }, "atingimento": { "quantidadeMrr": 0, "saas": 0, "quantidadeServico": 0, "servico": 0 } }, { "tri": 3, "meta": { "quantidadeMrr": 6, "saas": 9000, "quantidadeServico": 6, "servico": 30000 }, "realizado": { "quantidadeMrr": 0, "saas": 0, "quantidadeServico": 0, "servico": 0 }, "atingimento": { "quantidadeMrr": 0, "saas": 0, "quantidadeServico": 0, "servico": 0 } }, { "tri": 4, "meta": { "quantidadeMrr": 6, "saas": 9000, "quantidadeServico": 6, "servico": 30000 }, "realizado": { "quantidadeMrr": 0, "saas": 0, "quantidadeServico": 0, "servico": 0 }, "atingimento": { "quantidadeMrr": 0, "saas": 0, "quantidadeServico": 0, "servico": 0 } } ] } ] } ``` #### 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. ## Comissões Comissões de venda por vendedor e seu ciclo de liberação. - `GET /comissoes` — Lista paginada de comissões de venda - `GET /comissoes/resumo` — Totais de comissões por etapa do fluxo - `GET /comissoes/filter-options` — Opções para os filtros de comissões - `GET /comissoes/{id}` — Detalhe de uma comissão ### GET /comissoes **Lista paginada de comissões de venda** · operationId `listarComissoes` Autenticação: `Authorization: Bearer `. Lista as comissões de venda (padrão e especiais, misturadas) visíveis ao usuário, com paginação feita em memória sobre o resultado filtrado. **Ordenação:** só comissões padrão: `dataElegibilidade` decrescente (as ainda sem data, isto é, pendentes, vêm primeiro) e depois criação decrescente. Com comissões especiais no resultado, tudo é reordenado por data decrescente usando `dataElegibilidade`, senão `dataVendaConclusao`, senão criação. **Paginação:** `page` e `pageSize` fora do intervalo não geram erro: são ajustados (page mínimo 1; pageSize entre 1 e 500; valor não numérico vale o padrão). Resposta: `{ data: [...], pagination: { page, pageSize, total, totalPages } }`. Cada item é do formato padrão ou especial (`tipoComissao`). Valores monetários em **reais** (número JSON), percentuais de 0 a 100, datas ISO 8601 (UTC). **Sem efeito colateral:** é só leitura. Comissões `PENDENTE_REGRA` passam a `PENDENTE_CONFERENCIA` quando uma regra de comissão vigente é criada ou alterada, não numa consulta. **Permissão:** capacidade `LER_COMISSOES`. Sem ela, 403 ("Usuário sem permissão para acessar o módulo de comissões."). Em geral: usuário de tipo `ADMIN` ou `COMERCIAL`; com acesso por setor ligado, administrador pleno ou membro/gestor do setor Comercial. **Escopo de registros:** modo legado: `type ADMIN` ou usuário com `is_admin` vê todas as comissões da organização; usuário `COMERCIAL` sem `is_admin` vê só as do vendedor vinculado ao seu usuário. Com acesso por setor ligado: Dono/Admin e gestor do Comercial veem todas; membro do Comercial vê só as do vendedor vinculado. Sem o interruptor `ESCOPO_COMERCIAL`, um membro do Comercial que também pertence a outro setor pode ver todas. Sem vínculo de vendedor, ou com mais de um, **não há erro: lista vazia / zeros** (404 no detalhe). ### Ciclo de status da comissão padrão 1. `PENDENTE_REGRA`: o vendedor é comissionado, mas não há regra de comissão ativa e vigente (vendedor + produto + tipo de projeto) na data da venda. Vira `PENDENTE_CONFERENCIA` assim que a regra existe. 2. `PENDENTE_CONFERENCIA`: regra resolvida; percentuais congelados (`percentualPrimeiraParcelaSnapshot`, `percentualServicoSnapshot`); aguardando a **conferência da venda**, feita manualmente por gestão ou automaticamente na aprovação do formulário. 3. `PENDENTE_ELEGIBILIDADE`: venda conferida (`conferidaEm` preenchido); aguarda a **primeira OS paga**. 4. `ELEGIVEL`: liberada para pagamento. Acontece quando a **primeira OS do projeto/contrato muda para `PAGO` no Omie** (`primeiraOsPagaId` e `dataElegibilidade` são gravados e os valores calculados). Atalhos: sem projeto (importada, ou formulário que não gera projeto) ou projeto não faturável pelo financeiro, a conferência já libera a comissão sem depender de OS; o mesmo vale para venda sem comissão de serviço cujo SaaS foi conferido. Se a OS foi paga antes da conferência, a conferência já a encontra. Se a OS vinculada for cancelada/excluída e outra OS for paga, a comissão é revinculada à nova. 5. `FECHADA`: congelada em um fechamento de período (`fechamentoId`); continua apta a pagamento. 6. `PAGA_PARCIAL`: um componente pago (`SERVICO` ou `PRIMEIRA_PARCELA`) e o outro não. 7. `PAGA`: todos os componentes aplicáveis pagos (`pagaEm` = o pagamento mais recente). 8. `CANCELADA`: a OS ligada à comissão foi cancelada ou excluída no Omie antes da elegibilidade. Se uma OS for paga depois, a comissão é revivida ao estágio em que estava. Não há cancelamento manual. O pagamento exige conferência final registrada (feita por administrador); esses passos são escritas e não fazem parte desta documentação de leitura. **Componentes:** `SERVICO` (comissão sobre o serviço/implantação) e `PRIMEIRA_PARCELA` (comissão sobre a primeira parcela do SaaS/mensalidade). Cada um tem conferência e pagamento próprios em `componentes`. `valorComissao` é a soma dos dois; percentuais são de 0 a 100; valores em **reais**, números JSON (a API converte os decimais do banco para número). **Comissões especiais** (`tipoComissao = ESPECIAL`) são comissões adicionais para outro vendedor beneficiário, derivadas de uma comissão padrão por uma regra especial. Têm ciclo próprio: `ABERTA` -> `CONFERIDA` -> `PAGA` (ou `CANCELADA`), um único componente cada, e não pertencem a fechamento. **Campos expostos:** a resposta tem forma explícita. Não saem colunas internas (organização, arquivo de importação, conferência final) nem o e-mail do vendedor. Os campos `*PorId` são ids (UUID) de usuários do AgendAI. #### Parâmetros | Nome | Onde | Tipo | Obrigatório | Descrição | |---|---|---|---|---| | `page` | query | integer; padrão `1`; mín. 1 | não | Página, base 1. Não numérico ou menor que 1 vira 1; valor fracionário não é arredondado (envie inteiro). Ex.: `1` | | `pageSize` | query | integer; padrão `20`; mín. 1; máx. 500 | não | Itens por página; limitado a 1-500. Não numérico ou 0 vale 20. Ex.: `20` | | `status` | query | lista de string — um de: `PENDENTE_REGRA`, `PENDENTE_CONFERENCIA`, `PENDENTE_ELEGIBILIDADE`, `ELEGIVEL`, `FECHADA`, `PAGA_PARCIAL`, `PAGA`, `CANCELADA`, `ABERTA`, `CONFERIDA` | não | Filtra por status. Aceita parâmetro repetido ou lista separada por vírgula. Valores de comissão padrão: `PENDENTE_REGRA`, `PENDENTE_CONFERENCIA`, `PENDENTE_ELEGIBILIDADE`, `ELEGIVEL`, `FECHADA`, `PAGA_PARCIAL`, `PAGA`, `CANCELADA`; de comissão especial: `ABERTA`, `CONFERIDA`, `PAGA`, `CANCELADA`. Valores desconhecidos são **ignorados sem erro**; cada tipo de comissão só é filtrado pelos valores que reconhece (se nenhum valor da lista é válido para um tipo, esse tipo some do resultado). Ex.: `["ELEGIVEL","PAGA_PARCIAL"]` | | `vendedorId` | query | string (uuid) | não | Id (UUID) do vendedor; para comissão especial, o beneficiário. UUID inválido (inclusive texto vazio) responde 400. Ex.: `2d4f6a80-1b3c-4e5f-8a7b-9c0d1e2f3a4b` | | `fechamentoId` | query | string (uuid) | não | Id (UUID) de um fechamento. Comissões especiais não pertencem a fechamento: com este filtro elas são excluídas. | | `dataVendaInicio` | query | string (date) | não | Início do período, inclusivo. Prefira `YYYY-MM-DD` (vira 00:00:00.000Z do dia, UTC); qualquer texto que `new Date()` entenda é aceito, e texto não interpretável responde 400 ("Data inválida para o domínio de comissões."). **Sem `componente`**, compara com a data da venda/conclusão (`dataVendaConclusao`; nas especiais, `dataReferenciaVenda`). **Com `componente`**, comissões de formulário passam a ser comparadas com a data de assinatura do contrato do componente (serviço ou SaaS) do formulário, e as sem formulário (`formularioId` nulo) com a data da venda. Ex.: `2026-10-01` | | `dataVendaFim` | query | string (date) | não | Fim do período, inclusivo até 23:59:59.999Z do dia. Mesmas regras de `dataVendaInicio`. Ex.: `2026-10-31` | | `componente` | query | string — um de: `SERVICO`, `PRIMEIRA_PARCELA` | não | Mostra só comissões em que o componente é aplicável (valor > 0): `SERVICO` ou `PRIMEIRA_PARCELA` (SaaS/mensalidade). Comissões ligadas a formulário só entram se o formulário tiver a data de assinatura do contrato desse componente (`contratoDataAssinaturaServico` / `contratoDataAssinaturaSaas`), mesmo sem filtro de data; comissões sem formulário entram sempre. Entre as que passam por esse corte, as `PENDENTE_REGRA` aparecem mesmo sem valor. Nas especiais, filtra pelo `componente` da especial. Texto vazio é ignorado; outro valor responde 400. Também muda a base do filtro de data (ver `dataVendaInicio`). Ex.: `SERVICO` | | `produtoId` | query | string (uuid) | não | Id (UUID) do produto. UUID inválido responde 400. | | `classe` | query | string — um de: `NOVO`, `ADITIVO` | não | `ADITIVO` quando o tipo de projeto se chama "Aditivo" (sem diferenciar acento/caixa); `NOVO` para todos os demais. Outro valor responde 400. Ex.: `NOVO` | #### Resposta 200 Página de comissões. Tipo da raiz: object | Campo | Tipo | Sempre presente | Descrição | |---|---|---|---| | `data` | lista de ComissoesComissaoPadrao \| ComissoesComissaoEspecial | sim | | | `pagination` | object | sim | | | `pagination.page` | integer | sim | Página atual. | | `pagination.pageSize` | integer | sim | Itens por página (após ajuste). | | `pagination.total` | integer | sim | Total de comissões (padrão + especiais) que passam nos filtros. | | `pagination.totalPages` | integer | sim | Total de páginas (`0` quando `total = 0`). | Exemplo (dados fictícios): ```json { "data": [ { "id": "c1a2b3c4-d5e6-4f70-8a91-b2c3d4e5f607", "tipoComissao": "PADRAO", "origem": "FORMULARIO", "formularioId": "f0e1d2c3-b4a5-4968-8776-5a4b3c2d1e0f", "projetoId": "7e3d1c20-9a54-4d3b-8f21-0c6b5a4d3e22", "vendedorId": "2d4f6a80-1b3c-4e5f-8a7b-9c0d1e2f3a4b", "produtoId": "5f0c1e7a-8d2b-4a39-b0c4-1e2d3c4b5a69", "tipoProjetoId": "a1b2c3d4-0000-4000-8000-000000000001", "regraId": "d9c8b7a6-5f4e-4d3c-8b2a-1f0e9d8c7b6a", "importacaoReferenciaExterna": null, "clienteSnapshot": null, "projetoSnapshot": null, "vendedorNomeSnapshot": null, "produtoSnapshot": null, "tipoProjetoSnapshot": null, "contratoId": 912, "primeiraOsPagaId": 48213, "fechamentoId": null, "status": "ELEGIVEL", "dataVendaConclusao": "2026-09-28T00:00:00.000Z", "dataBaseRegra": "2026-09-28T00:00:00.000Z", "percentualPrimeiraParcelaSnapshot": 50, "percentualServicoSnapshot": 5, "regraVigenciaInicio": "2026-01-01T00:00:00.000Z", "regraVigenciaFim": null, "baseCalculoPrimeiraParcela": 1200, "valorComissaoPrimeiraParcela": 600, "baseCalculoServico": 10000, "valorComissaoServico": 500, "baseCalculo": 11200, "valorComissao": 1100, "dataElegibilidade": "2026-10-20T13:05:00.000Z", "conferidaEm": "2026-10-05T14:20:00.000Z", "conferidaPorId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "observacaoConferencia": null, "primeiraParcelaConferidaEm": "2026-10-05T14:20:00.000Z", "primeiraParcelaConferidaPorId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "observacaoConferenciaPrimeiraParcela": null, "primeiraParcelaPagaEm": null, "primeiraParcelaPagaPorId": null, "servicoPagoEm": null, "servicoPagoPorId": null, "observacaoPagamentoServico": null, "observacaoPagamentoPrimeiraParcela": null, "pagaEm": null, "pagaPorId": null, "observacaoOperacional": "Comissão elegível pela primeira OS paga confirmada no Omie.", "createdAt": "2026-09-28T17:00:00.000Z", "updatedAt": "2026-10-20T13:05:00.000Z", "vendedor": { "id": "2d4f6a80-1b3c-4e5f-8a7b-9c0d1e2f3a4b", "nome": "Vendedor Exemplo" }, "produto": { "id": "5f0c1e7a-8d2b-4a39-b0c4-1e2d3c4b5a69", "produto": "Plataforma Exemplo" }, "tipoProjeto": { "id": "a1b2c3d4-0000-4000-8000-000000000001", "tipoProjeto": "Implantação", "status": true }, "fechamento": null, "cliente": { "id": "0b6a1d52-3f0e-4c6a-9d0e-5a1f2f6d7a11", "nomeEmpresa": "Empresa Alfa Ltda" }, "projeto": { "nomeProjeto": "Implantação Alfa", "quantidadeHoras": 100, "valorHora": 100 }, "contrato": { "id": 912, "tipoContrato": "SERVICO", "numCtr": "CTR-0091", "codStatus": "ATIVO", "descStatus": "Ativo", "valorTotalMensal": 1200, "diaFaturamento": 10, "vigenciaInicio": "2026-09-01T00:00:00.000Z", "vigenciaFim": "2027-08-31T00:00:00.000Z", "pagamentoStatus": "PAGA", "dataPrimeiroRecebimento": "2026-10-20T00:00:00.000Z" }, "primeiraOsPaga": { "id": 48213, "numOs": "000412", "codStatus": "PAGO", "descStatus": "Pago", "valorTotal": 10000, "dataFaturamento": "2026-10-01T00:00:00.000Z", "dataVencimento": "2026-10-10T00:00:00.000Z", "quantidadeParcelas": 1, "pagamentoStatus": "PAGA", "dataPrimeiroRecebimento": "2026-10-20T00:00:00.000Z" }, "primeiraOsPendente": null, "componentes": { "SERVICO": { "aplicavel": true, "aptoPagamento": true, "conferencia": { "status": "CONFERIDO", "em": "2026-10-05T14:20:00.000Z", "porId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "observacao": null }, "pagamento": { "status": "PENDENTE", "em": null, "porId": null, "observacao": null } }, "PRIMEIRA_PARCELA": { "aplicavel": true, "aptoPagamento": true, "conferencia": { "status": "CONFERIDO", "em": "2026-10-05T14:20:00.000Z", "porId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "observacao": null }, "pagamento": { "status": "PENDENTE", "em": null, "porId": null, "observacao": null } } } } ], "pagination": { "page": 1, "pageSize": 20, "total": 1, "totalPages": 1 } } ``` #### 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. - **403**: O usuário do token não tem a permissão exigida pela rota. --- ### GET /comissoes/resumo **Totais de comissões por etapa do fluxo** · operationId `obterResumoComissoes` Autenticação: `Authorization: Bearer `. Soma quantidade e valor das comissões visíveis por etapa do fluxo, com os mesmos filtros de `GET /comissoes` (sem paginação). Resposta: `{ cards: { pendenteRegra, pendenteConferencia, pendenteElegibilidade, prontaPagamento, paga } }`. Mapeamento dos cartões: `pendenteRegra` = `PENDENTE_REGRA`; `pendenteConferencia` = `PENDENTE_CONFERENCIA` + especiais `ABERTA`; `pendenteElegibilidade` = `PENDENTE_ELEGIBILIDADE`; `prontaPagamento` = `ELEGIVEL` + `FECHADA` + `PAGA_PARCIAL` + especiais `CONFERIDA`; `paga` = `PAGA` + especiais `PAGA`. `CANCELADA` não entra em nenhum. Valores em **reais**. Armadilhas: em `prontaPagamento`, comissões `PAGA_PARCIAL` contribuem só com o valor ainda não pago. Em `pendenteRegra`, o valor é a base de serviço estimada do projeto (horas x valor da hora), pois ainda não há comissão calculada. O filtro `status` limita o que entra, mas os cartões continuam sendo por etapa (um cartão fora do filtro fica zerado). **Sem efeito colateral:** é só leitura. Comissões `PENDENTE_REGRA` passam a `PENDENTE_CONFERENCIA` quando uma regra de comissão vigente é criada ou alterada, não numa consulta. **Permissão:** capacidade `LER_COMISSOES`. Sem ela, 403 ("Usuário sem permissão para acessar o módulo de comissões."). Em geral: usuário de tipo `ADMIN` ou `COMERCIAL`; com acesso por setor ligado, administrador pleno ou membro/gestor do setor Comercial. **Escopo de registros:** modo legado: `type ADMIN` ou usuário com `is_admin` vê todas as comissões da organização; usuário `COMERCIAL` sem `is_admin` vê só as do vendedor vinculado ao seu usuário. Com acesso por setor ligado: Dono/Admin e gestor do Comercial veem todas; membro do Comercial vê só as do vendedor vinculado. Sem o interruptor `ESCOPO_COMERCIAL`, um membro do Comercial que também pertence a outro setor pode ver todas. Sem vínculo de vendedor, ou com mais de um, **não há erro: lista vazia / zeros** (404 no detalhe). ### Ciclo de status da comissão padrão 1. `PENDENTE_REGRA`: o vendedor é comissionado, mas não há regra de comissão ativa e vigente (vendedor + produto + tipo de projeto) na data da venda. Vira `PENDENTE_CONFERENCIA` assim que a regra existe. 2. `PENDENTE_CONFERENCIA`: regra resolvida; percentuais congelados (`percentualPrimeiraParcelaSnapshot`, `percentualServicoSnapshot`); aguardando a **conferência da venda**, feita manualmente por gestão ou automaticamente na aprovação do formulário. 3. `PENDENTE_ELEGIBILIDADE`: venda conferida (`conferidaEm` preenchido); aguarda a **primeira OS paga**. 4. `ELEGIVEL`: liberada para pagamento. Acontece quando a **primeira OS do projeto/contrato muda para `PAGO` no Omie** (`primeiraOsPagaId` e `dataElegibilidade` são gravados e os valores calculados). Atalhos: sem projeto (importada, ou formulário que não gera projeto) ou projeto não faturável pelo financeiro, a conferência já libera a comissão sem depender de OS; o mesmo vale para venda sem comissão de serviço cujo SaaS foi conferido. Se a OS foi paga antes da conferência, a conferência já a encontra. Se a OS vinculada for cancelada/excluída e outra OS for paga, a comissão é revinculada à nova. 5. `FECHADA`: congelada em um fechamento de período (`fechamentoId`); continua apta a pagamento. 6. `PAGA_PARCIAL`: um componente pago (`SERVICO` ou `PRIMEIRA_PARCELA`) e o outro não. 7. `PAGA`: todos os componentes aplicáveis pagos (`pagaEm` = o pagamento mais recente). 8. `CANCELADA`: a OS ligada à comissão foi cancelada ou excluída no Omie antes da elegibilidade. Se uma OS for paga depois, a comissão é revivida ao estágio em que estava. Não há cancelamento manual. O pagamento exige conferência final registrada (feita por administrador); esses passos são escritas e não fazem parte desta documentação de leitura. **Componentes:** `SERVICO` (comissão sobre o serviço/implantação) e `PRIMEIRA_PARCELA` (comissão sobre a primeira parcela do SaaS/mensalidade). Cada um tem conferência e pagamento próprios em `componentes`. `valorComissao` é a soma dos dois; percentuais são de 0 a 100; valores em **reais**, números JSON (a API converte os decimais do banco para número). **Comissões especiais** (`tipoComissao = ESPECIAL`) são comissões adicionais para outro vendedor beneficiário, derivadas de uma comissão padrão por uma regra especial. Têm ciclo próprio: `ABERTA` -> `CONFERIDA` -> `PAGA` (ou `CANCELADA`), um único componente cada, e não pertencem a fechamento. #### Parâmetros | Nome | Onde | Tipo | Obrigatório | Descrição | |---|---|---|---|---| | `status` | query | lista de string — um de: `PENDENTE_REGRA`, `PENDENTE_CONFERENCIA`, `PENDENTE_ELEGIBILIDADE`, `ELEGIVEL`, `FECHADA`, `PAGA_PARCIAL`, `PAGA`, `CANCELADA`, `ABERTA`, `CONFERIDA` | não | Filtra por status. Aceita parâmetro repetido ou lista separada por vírgula. Valores de comissão padrão: `PENDENTE_REGRA`, `PENDENTE_CONFERENCIA`, `PENDENTE_ELEGIBILIDADE`, `ELEGIVEL`, `FECHADA`, `PAGA_PARCIAL`, `PAGA`, `CANCELADA`; de comissão especial: `ABERTA`, `CONFERIDA`, `PAGA`, `CANCELADA`. Valores desconhecidos são **ignorados sem erro**; cada tipo de comissão só é filtrado pelos valores que reconhece (se nenhum valor da lista é válido para um tipo, esse tipo some do resultado). Ex.: `["ELEGIVEL","PAGA_PARCIAL"]` | | `vendedorId` | query | string (uuid) | não | Id (UUID) do vendedor; para comissão especial, o beneficiário. UUID inválido (inclusive texto vazio) responde 400. Ex.: `2d4f6a80-1b3c-4e5f-8a7b-9c0d1e2f3a4b` | | `fechamentoId` | query | string (uuid) | não | Id (UUID) de um fechamento. Comissões especiais não pertencem a fechamento: com este filtro elas são excluídas. | | `dataVendaInicio` | query | string (date) | não | Início do período, inclusivo. Prefira `YYYY-MM-DD` (vira 00:00:00.000Z do dia, UTC); qualquer texto que `new Date()` entenda é aceito, e texto não interpretável responde 400 ("Data inválida para o domínio de comissões."). **Sem `componente`**, compara com a data da venda/conclusão (`dataVendaConclusao`; nas especiais, `dataReferenciaVenda`). **Com `componente`**, comissões de formulário passam a ser comparadas com a data de assinatura do contrato do componente (serviço ou SaaS) do formulário, e as sem formulário (`formularioId` nulo) com a data da venda. Ex.: `2026-10-01` | | `dataVendaFim` | query | string (date) | não | Fim do período, inclusivo até 23:59:59.999Z do dia. Mesmas regras de `dataVendaInicio`. Ex.: `2026-10-31` | | `componente` | query | string — um de: `SERVICO`, `PRIMEIRA_PARCELA` | não | Mostra só comissões em que o componente é aplicável (valor > 0): `SERVICO` ou `PRIMEIRA_PARCELA` (SaaS/mensalidade). Comissões ligadas a formulário só entram se o formulário tiver a data de assinatura do contrato desse componente (`contratoDataAssinaturaServico` / `contratoDataAssinaturaSaas`), mesmo sem filtro de data; comissões sem formulário entram sempre. Entre as que passam por esse corte, as `PENDENTE_REGRA` aparecem mesmo sem valor. Nas especiais, filtra pelo `componente` da especial. Texto vazio é ignorado; outro valor responde 400. Também muda a base do filtro de data (ver `dataVendaInicio`). Ex.: `SERVICO` | | `produtoId` | query | string (uuid) | não | Id (UUID) do produto. UUID inválido responde 400. | | `classe` | query | string — um de: `NOVO`, `ADITIVO` | não | `ADITIVO` quando o tipo de projeto se chama "Aditivo" (sem diferenciar acento/caixa); `NOVO` para todos os demais. Outro valor responde 400. Ex.: `NOVO` | #### Resposta 200 Totais por etapa. Tipo da raiz: ComissoesResumoResponse | Campo | Tipo | Sempre presente | Descrição | |---|---|---|---| | `cards` | object | sim | | | `cards.pendenteRegra` | object | sim | Sem regra de comissão vigente. `valorComissaoTotal` aqui é a base de serviço estimada do projeto (horas x valor da hora) quando existe, não uma comissão calculada. | | `cards.pendenteRegra.totalComissoes` | integer | sim | Quantidade de comissões (padrão + especiais) em `PENDENTE_REGRA`. | | `cards.pendenteRegra.valorComissaoTotal` | number | sim | Valor total em reais. | | `cards.pendenteConferencia` | object | sim | Aguardando conferência da venda. | | `cards.pendenteConferencia.totalComissoes` | integer | sim | Quantidade de comissões (padrão + especiais) em `PENDENTE_CONFERENCIA` (mais especiais `ABERTA`). | | `cards.pendenteConferencia.valorComissaoTotal` | number | sim | Valor total em reais. | | `cards.pendenteElegibilidade` | object | sim | Venda conferida, aguardando a primeira OS paga. | | `cards.pendenteElegibilidade.totalComissoes` | integer | sim | Quantidade de comissões (padrão + especiais) em `PENDENTE_ELEGIBILIDADE`. | | `cards.pendenteElegibilidade.valorComissaoTotal` | number | sim | Valor total em reais. | | `cards.prontaPagamento` | object | sim | Comissões `ELEGIVEL`, `FECHADA` ou `PAGA_PARCIAL` (mais especiais `CONFERIDA`). Para `PAGA_PARCIAL` conta só o que ainda falta pagar. | | `cards.prontaPagamento.totalComissoes` | integer | sim | Quantidade de comissões prontas para pagamento. | | `cards.prontaPagamento.valorComissaoTotal` | number | sim | Valor total em reais (saldo a pagar nas parcialmente pagas). | | `cards.prontaPagamento.valorComissaoPrimeiraParcelaPendente` | number | sim | Parte do saldo que é componente primeira parcela/SaaS ainda não pago, em reais. | | `cards.prontaPagamento.valorComissaoServicoPendente` | number | sim | Parte do saldo que é componente serviço ainda não pago, em reais. | | `cards.paga` | object | sim | Totalmente paga. | | `cards.paga.totalComissoes` | integer | sim | Quantidade de comissões (padrão + especiais) em `PAGA` (mais especiais `PAGA`). | | `cards.paga.valorComissaoTotal` | number | sim | Valor total em reais. | Exemplo (dados fictícios): ```json { "cards": { "pendenteRegra": { "totalComissoes": 1, "valorComissaoTotal": 8000 }, "pendenteConferencia": { "totalComissoes": 2, "valorComissaoTotal": 2300 }, "pendenteElegibilidade": { "totalComissoes": 3, "valorComissaoTotal": 4100.5 }, "prontaPagamento": { "totalComissoes": 4, "valorComissaoTotal": 5200, "valorComissaoPrimeiraParcelaPendente": 1800, "valorComissaoServicoPendente": 3400 }, "paga": { "totalComissoes": 6, "valorComissaoTotal": 9100 } } } ``` #### 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. - **403**: O usuário do token não tem a permissão exigida pela rota. --- ### GET /comissoes/filter-options **Opções para os filtros de comissões** · operationId `listarOpcoesFiltroComissoes` Autenticação: `Authorization: Bearer `. Devolve os valores que alimentam os filtros de `GET /comissoes`: status existentes, vendedores, produtos e fechamentos, **somente a partir do que o usuário pode ver** (mesmo escopo de registros da listagem). Não recebe parâmetros. Resposta: `{ statuses, vendedores, produtos, fechamentos }`. **Sem efeito colateral:** é só leitura. Comissões `PENDENTE_REGRA` passam a `PENDENTE_CONFERENCIA` quando uma regra de comissão vigente é criada ou alterada, não numa consulta. **Permissão:** capacidade `LER_COMISSOES`. Sem ela, 403 ("Usuário sem permissão para acessar o módulo de comissões."). Em geral: usuário de tipo `ADMIN` ou `COMERCIAL`; com acesso por setor ligado, administrador pleno ou membro/gestor do setor Comercial. **Escopo de registros:** modo legado: `type ADMIN` ou usuário com `is_admin` vê todas as comissões da organização; usuário `COMERCIAL` sem `is_admin` vê só as do vendedor vinculado ao seu usuário. Com acesso por setor ligado: Dono/Admin e gestor do Comercial veem todas; membro do Comercial vê só as do vendedor vinculado. Sem o interruptor `ESCOPO_COMERCIAL`, um membro do Comercial que também pertence a outro setor pode ver todas. Sem vínculo de vendedor, ou com mais de um, **não há erro: lista vazia / zeros** (404 no detalhe). #### Resposta 200 Opções de filtro. Tipo da raiz: ComissoesFilterOptionsResponse | Campo | Tipo | Sempre presente | Descrição | |---|---|---|---| | `statuses` | lista de string — um de: `PENDENTE_REGRA`, `PENDENTE_CONFERENCIA`, `PENDENTE_ELEGIBILIDADE`, `ELEGIVEL`, `FECHADA`, `PAGA_PARCIAL`, `PAGA`, `CANCELADA`, `ABERTA`, `CONFERIDA` | sim | Status presentes nos dados visíveis ao usuário (padrão e especiais), ordenados alfabeticamente e sem repetição. | | `vendedores` | object[] | sim | Vendedores com ao menos uma comissão visível. | | `vendedores[].id` | string (uuid) | sim | Id do vendedor (UUID). | | `vendedores[].nome` | string \| null | sim | Nome do vendedor. | | `produtos` | object[] | sim | Produtos com comissão visível, em ordem alfabética (pt-BR). | | `produtos[].id` | string (uuid) | sim | Id do produto (UUID). | | `produtos[].produto` | string \| null | sim | Nome do produto. | | `fechamentos` | object[] | sim | Fechamentos de período visíveis, do período mais recente ao mais antigo. Quem vê só as próprias comissões vê só os fechamentos que as contêm. | | `fechamentos[].id` | string (uuid) | sim | Id do fechamento (UUID). | | `fechamentos[].periodoInicio` | string (date-time) | sim | Início do período. | | `fechamentos[].periodoFim` | string (date-time) | sim | Fim do período. | Exemplo (dados fictícios): ```json { "statuses": [ "ELEGIVEL", "PAGA", "PENDENTE_CONFERENCIA" ], "vendedores": [ { "id": "2d4f6a80-1b3c-4e5f-8a7b-9c0d1e2f3a4b", "nome": "Vendedor Exemplo" } ], "produtos": [ { "id": "5f0c1e7a-8d2b-4a39-b0c4-1e2d3c4b5a69", "produto": "Plataforma Exemplo" } ], "fechamentos": [ { "id": "e1f2a3b4-c5d6-4789-8a9b-0c1d2e3f4a5b", "periodoInicio": "2026-09-01T00:00:00.000Z", "periodoFim": "2026-09-30T23:59:59.999Z" } ] } ``` #### Erros - **401**: Token ausente, inválido, expirado ou de usuário desativado. Faça login de novo. - **403**: O usuário do token não tem a permissão exigida pela rota. --- ### GET /comissoes/{id} **Detalhe de uma comissão** · operationId `obterComissao` Autenticação: `Authorization: Bearer `. Devolve uma comissão pelo id, no mesmo formato dos itens de `GET /comissoes` (sem o envelope `data`), já com `primeiraOsPendente` e a situação de recebimento da OS e do contrato (`pagamentoStatus`, `dataPrimeiroRecebimento`) (só na comissão padrão; a especial não tem esses campos). Padrão: procura entre as comissões **padrão**. Para uma comissão especial é obrigatório enviar `tipoComissao=ESPECIAL`; sem isso o id de uma especial responde 404. O usuário só enxerga comissões do seu escopo (ver abaixo); fora dele a resposta é 404, não 403. O `id` precisa ser um UUID (senão 400). **Sem efeito colateral:** é só leitura. Comissões `PENDENTE_REGRA` passam a `PENDENTE_CONFERENCIA` quando uma regra de comissão vigente é criada ou alterada, não numa consulta. **Permissão:** capacidade `LER_COMISSOES`. Sem ela, 403 ("Usuário sem permissão para acessar o módulo de comissões."). Em geral: usuário de tipo `ADMIN` ou `COMERCIAL`; com acesso por setor ligado, administrador pleno ou membro/gestor do setor Comercial. **Escopo de registros:** modo legado: `type ADMIN` ou usuário com `is_admin` vê todas as comissões da organização; usuário `COMERCIAL` sem `is_admin` vê só as do vendedor vinculado ao seu usuário. Com acesso por setor ligado: Dono/Admin e gestor do Comercial veem todas; membro do Comercial vê só as do vendedor vinculado. Sem o interruptor `ESCOPO_COMERCIAL`, um membro do Comercial que também pertence a outro setor pode ver todas. Sem vínculo de vendedor, ou com mais de um, **não há erro: lista vazia / zeros** (404 no detalhe). ### Ciclo de status da comissão padrão 1. `PENDENTE_REGRA`: o vendedor é comissionado, mas não há regra de comissão ativa e vigente (vendedor + produto + tipo de projeto) na data da venda. Vira `PENDENTE_CONFERENCIA` assim que a regra existe. 2. `PENDENTE_CONFERENCIA`: regra resolvida; percentuais congelados (`percentualPrimeiraParcelaSnapshot`, `percentualServicoSnapshot`); aguardando a **conferência da venda**, feita manualmente por gestão ou automaticamente na aprovação do formulário. 3. `PENDENTE_ELEGIBILIDADE`: venda conferida (`conferidaEm` preenchido); aguarda a **primeira OS paga**. 4. `ELEGIVEL`: liberada para pagamento. Acontece quando a **primeira OS do projeto/contrato muda para `PAGO` no Omie** (`primeiraOsPagaId` e `dataElegibilidade` são gravados e os valores calculados). Atalhos: sem projeto (importada, ou formulário que não gera projeto) ou projeto não faturável pelo financeiro, a conferência já libera a comissão sem depender de OS; o mesmo vale para venda sem comissão de serviço cujo SaaS foi conferido. Se a OS foi paga antes da conferência, a conferência já a encontra. Se a OS vinculada for cancelada/excluída e outra OS for paga, a comissão é revinculada à nova. 5. `FECHADA`: congelada em um fechamento de período (`fechamentoId`); continua apta a pagamento. 6. `PAGA_PARCIAL`: um componente pago (`SERVICO` ou `PRIMEIRA_PARCELA`) e o outro não. 7. `PAGA`: todos os componentes aplicáveis pagos (`pagaEm` = o pagamento mais recente). 8. `CANCELADA`: a OS ligada à comissão foi cancelada ou excluída no Omie antes da elegibilidade. Se uma OS for paga depois, a comissão é revivida ao estágio em que estava. Não há cancelamento manual. O pagamento exige conferência final registrada (feita por administrador); esses passos são escritas e não fazem parte desta documentação de leitura. **Componentes:** `SERVICO` (comissão sobre o serviço/implantação) e `PRIMEIRA_PARCELA` (comissão sobre a primeira parcela do SaaS/mensalidade). Cada um tem conferência e pagamento próprios em `componentes`. `valorComissao` é a soma dos dois; percentuais são de 0 a 100; valores em **reais**, números JSON (a API converte os decimais do banco para número). **Comissões especiais** (`tipoComissao = ESPECIAL`) são comissões adicionais para outro vendedor beneficiário, derivadas de uma comissão padrão por uma regra especial. Têm ciclo próprio: `ABERTA` -> `CONFERIDA` -> `PAGA` (ou `CANCELADA`), um único componente cada, e não pertencem a fechamento. #### Parâmetros | Nome | Onde | Tipo | Obrigatório | Descrição | |---|---|---|---|---| | `id` | path | string (uuid) | sim | Id (UUID) da comissão. Ex.: `c1a2b3c4-d5e6-4f70-8a91-b2c3d4e5f607` | | `tipoComissao` | query | string — um de: `PADRAO`, `ESPECIAL`; padrão `"PADRAO"` | não | `PADRAO` (padrão quando omitido) ou `ESPECIAL`. Outro valor responde 400. Ex.: `PADRAO` | #### Resposta 200 Comissão encontrada. Tipo: ComissoesComissaoPadrao | ComissoesComissaoEspecial Exemplo (dados fictícios): ```json { "id": "c1a2b3c4-d5e6-4f70-8a91-b2c3d4e5f607", "tipoComissao": "PADRAO", "origem": "FORMULARIO", "formularioId": "f0e1d2c3-b4a5-4968-8776-5a4b3c2d1e0f", "projetoId": "7e3d1c20-9a54-4d3b-8f21-0c6b5a4d3e22", "vendedorId": "2d4f6a80-1b3c-4e5f-8a7b-9c0d1e2f3a4b", "produtoId": "5f0c1e7a-8d2b-4a39-b0c4-1e2d3c4b5a69", "tipoProjetoId": "a1b2c3d4-0000-4000-8000-000000000001", "regraId": "d9c8b7a6-5f4e-4d3c-8b2a-1f0e9d8c7b6a", "importacaoReferenciaExterna": null, "clienteSnapshot": null, "projetoSnapshot": null, "vendedorNomeSnapshot": null, "produtoSnapshot": null, "tipoProjetoSnapshot": null, "contratoId": 912, "primeiraOsPagaId": 48213, "fechamentoId": null, "status": "ELEGIVEL", "dataVendaConclusao": "2026-09-28T00:00:00.000Z", "dataBaseRegra": "2026-09-28T00:00:00.000Z", "percentualPrimeiraParcelaSnapshot": 50, "percentualServicoSnapshot": 5, "regraVigenciaInicio": "2026-01-01T00:00:00.000Z", "regraVigenciaFim": null, "baseCalculoPrimeiraParcela": 1200, "valorComissaoPrimeiraParcela": 600, "baseCalculoServico": 10000, "valorComissaoServico": 500, "baseCalculo": 11200, "valorComissao": 1100, "dataElegibilidade": "2026-10-20T13:05:00.000Z", "conferidaEm": "2026-10-05T14:20:00.000Z", "conferidaPorId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "observacaoConferencia": null, "primeiraParcelaConferidaEm": "2026-10-05T14:20:00.000Z", "primeiraParcelaConferidaPorId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "observacaoConferenciaPrimeiraParcela": null, "primeiraParcelaPagaEm": null, "primeiraParcelaPagaPorId": null, "servicoPagoEm": null, "servicoPagoPorId": null, "observacaoPagamentoServico": null, "observacaoPagamentoPrimeiraParcela": null, "pagaEm": null, "pagaPorId": null, "observacaoOperacional": "Comissão elegível pela primeira OS paga confirmada no Omie.", "createdAt": "2026-09-28T17:00:00.000Z", "updatedAt": "2026-10-20T13:05:00.000Z", "vendedor": { "id": "2d4f6a80-1b3c-4e5f-8a7b-9c0d1e2f3a4b", "nome": "Vendedor Exemplo" }, "produto": { "id": "5f0c1e7a-8d2b-4a39-b0c4-1e2d3c4b5a69", "produto": "Plataforma Exemplo" }, "tipoProjeto": { "id": "a1b2c3d4-0000-4000-8000-000000000001", "tipoProjeto": "Implantação", "status": true }, "fechamento": null, "cliente": { "id": "0b6a1d52-3f0e-4c6a-9d0e-5a1f2f6d7a11", "nomeEmpresa": "Empresa Alfa Ltda" }, "projeto": { "nomeProjeto": "Implantação Alfa", "quantidadeHoras": 100, "valorHora": 100 }, "contrato": { "id": 912, "tipoContrato": "SERVICO", "numCtr": "CTR-0091", "codStatus": "ATIVO", "descStatus": "Ativo", "valorTotalMensal": 1200, "diaFaturamento": 10, "vigenciaInicio": "2026-09-01T00:00:00.000Z", "vigenciaFim": "2027-08-31T00:00:00.000Z", "pagamentoStatus": "PAGA", "dataPrimeiroRecebimento": "2026-10-20T00:00:00.000Z" }, "primeiraOsPaga": { "id": 48213, "numOs": "000412", "codStatus": "PAGO", "descStatus": "Pago", "valorTotal": 10000, "dataFaturamento": "2026-10-01T00:00:00.000Z", "dataVencimento": "2026-10-10T00:00:00.000Z", "quantidadeParcelas": 1, "pagamentoStatus": "PAGA", "dataPrimeiroRecebimento": "2026-10-20T00:00:00.000Z" }, "primeiraOsPendente": null, "componentes": { "SERVICO": { "aplicavel": true, "aptoPagamento": true, "conferencia": { "status": "CONFERIDO", "em": "2026-10-05T14:20:00.000Z", "porId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "observacao": null }, "pagamento": { "status": "PENDENTE", "em": null, "porId": null, "observacao": null } }, "PRIMEIRA_PARCELA": { "aplicavel": true, "aptoPagamento": true, "conferencia": { "status": "CONFERIDO", "em": "2026-10-05T14:20:00.000Z", "porId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "observacao": null }, "pagamento": { "status": "PENDENTE", "em": null, "porId": null, "observacao": null } } } } ``` #### 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. - **403**: O usuário do token não tem a permissão exigida pela rota. - **404**: Registro não encontrado (ou de outra organização). ## Dashboard e cancelamentos Números consolidados do mês e da semana e índice de cancelamento. - `GET /dashboard/resumo-mes` — Resumo do mês da Visão geral (minutos e contagens) - `GET /dashboard/cardMensal` — Ocupação do mês por status, em percentual e em horas - `GET /dashboard/cardSemanal` — Ocupação da semana ISO por status, em percentual e em horas - `GET /dashboard/indice-cancelamento-mensal` — Horas canceladas e horas agendadas por pessoa no mês - `GET /dashboard/indice-cancelamento-semanal` — Horas canceladas e horas agendadas por pessoa na semana ISO ### GET /dashboard/resumo-mes **Resumo do mês da Visão geral (minutos e contagens)** · operationId `obterResumoMesDashboard` Autenticação: `Authorization: Bearer `. Responde: "como está o mês: quanto foi realizado, quanto está confirmado, o que aguarda aprovação ou conferência?". Devolve sete números do mês. **Cálculo.** Considera as agendas **não excluídas** cujo **`dataInicio`** cai no mês civil (hora de parede gravada como UTC: o mês vai de `AAAA-MM-01T00:00:00Z` inclusive ao dia 1 do mês seguinte exclusivo). Campos em **MINUTOS**: `minutosRealizados` (REALIZADO), `minutosConfirmadosARealizar` (CONFIRMADO), `minutosAgendados` (PRE_RESERVAR + RESERVADO + CONFIRMADO + REALIZADO). Campos em **QUANTIDADE de agendas**: `aguardandoAprovacao` (CONFIRMADO com `statusAprovacao = AGUARDANDO`), `conferenciasPendentes` (REALIZADO com `conferencia = false`), `totalAgendamentos` (todas menos as CANCELADAS; inclui PENDENTE e agendas sem status). **Permissão e escopo.** Exige token válido (JWT). Sem `@Roles`, mas **é a única rota deste grupo com escopo de dados**: o resultado considera só as agendas que o usuário pode ler (o mesmo escopo da lista de agendas): visão total vê tudo; com o acesso por setor ligado, a pessoa vê os ramos do seu setor (ex.: agendas CS, universo comercial, clientes de que é CS) mais as agendas em que é responsável ou convidada; perfil comercial restrito vê as agendas do seu vendedor mais as em que é responsável ou convidado; quem não tem escopo nenhum (e não é participante) recebe os números zerados (não é 403). Não aceita os filtros `usuarioIds`/`setor`. Erros: `mesAno` ausente ou em formato inválido dá 400. #### Parâmetros | Nome | Onde | Tipo | Obrigatório | Descrição | |---|---|---|---|---| | `mesAno` | query | string | sim | Mês de referência. Formatos aceitos: `MM-AAAA` (ex.: `03-2026`, recomendado) ou `Mês AAAA` com o nome do mês em português, capitalizado e com acento (`Março 2026`). Também é lido de `mesAno[mesAno]`. O mês cobre as agendas cujo INÍCIO (`dataInicio`) cai nele. Ex.: `03-2026` | #### Resposta 200 Resumo do mês. Tipo da raiz: DashboardResumoMes | Campo | Tipo | Sempre presente | Descrição | |---|---|---|---| | `mesAno` | string | sim | Eco do parâmetro `mesAno` recebido, sem normalizar. | | `minutosRealizados` | integer | sim | MINUTOS de agendas com status REALIZADO que começam no mês. | | `minutosConfirmadosARealizar` | integer | sim | MINUTOS de agendas com status CONFIRMADO que começam no mês. | | `aguardandoAprovacao` | integer | sim | QUANTIDADE (não minutos) de agendas CONFIRMADAS com aprovação em `AGUARDANDO`. | | `conferenciasPendentes` | integer | sim | QUANTIDADE de agendas REALIZADAS ainda sem conferência (`conferencia = false`). | | `totalAgendamentos` | integer | sim | QUANTIDADE de agendas do mês que NÃO estão canceladas (status nulo ou diferente de CANCELADO; inclui PENDENTE, reservadas, etc.). | | `minutosAgendados` | integer | sim | MINUTOS somados dos status PRE_RESERVAR, RESERVADO, CONFIRMADO e REALIZADO. | Exemplo (dados fictícios): ```json { "mesAno": "03-2026", "minutosRealizados": 7440, "minutosConfirmadosARealizar": 1800, "aguardandoAprovacao": 6, "conferenciasPendentes": 14, "totalAgendamentos": 132, "minutosAgendados": 12600 } ``` #### 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 /dashboard/cardMensal **Ocupação do mês por status, em percentual e em horas** · operationId `obterCardMensalDashboard` Autenticação: `Authorization: Bearer `. Responde: "que fatia da capacidade do mês está confirmada, pré-reservada, reservada, realizada, cancelada ou livre?". **Capacidade (base 100%).** `capacidadeHoras` = horas úteis do mês (calendário de dias úteis, 2023-2025 por tabela fixa, 2026 em diante pela regra de dias úteis; mês desconhecido = 160) x **número de pessoas distintas que têm ao menos uma agenda não excluída no mês** (qualquer status, só agendas com consultor). Ou seja, a capacidade depende de quem tem agenda, não do tamanho cadastrado da equipe. **Como o status é contado.** Soma as durações de TODAS as agendas não excluídas (`deleted_at` nulo) da organização cujo `dataInicio` cai no período, por status, em **horas** (minutos / 60). Uma agenda CANCELADA com `deduzir = true` conta como REALIZADA. Agendas PENDENTE e agendas sem status NÃO aparecem em nenhum campo da resposta e NÃO reduzem o `livre` (`livre` = capacidade menos as cinco categorias exibidas), mas contam para o número de pessoas da capacidade. `livre` nunca é negativo (sobreposição/overbooking é cortada em 0). **Unidades.** `porcentagem*`: percentual 0-100 como **TEXTO** com 2 casas (`"12.50"`); `horasPorStatus.*` e `capacidadeHoras`: **número em horas**, 2 casas. Os percentuais são horas / capacidade x 100 e podem passar de 100 individualmente, mas `porcentagemLivre` é cortado em 0. **Permissão:** exige token válido (JWT) e organização identificada. Não há `@Roles` nem checagem de tela. Os filtros `usuarioIds` e `setor` restringem às pessoas informadas (400 se malformados ou `setor` desconhecido). Nenhum escopo por usuário: qualquer usuário autenticado vê as horas de todas as pessoas da organização. Erros: `mesAno` ausente ou inválido (ex.: `foo`, `13-2026`, nome de mês errado) responde **400**. #### Parâmetros | Nome | Onde | Tipo | Obrigatório | Descrição | |---|---|---|---|---| | `mesAno` | query | string | sim | Mês de referência. Formatos aceitos: `MM-AAAA` (ex.: `03-2026`, recomendado) ou `Mês AAAA` com o nome do mês em português, capitalizado e com acento (`Março 2026`). Também é lido de `mesAno[mesAno]`. O mês cobre as agendas cujo INÍCIO (`dataInicio`) cai nele. Ex.: `03-2026` | | `usuarioIds` | query | lista de string (uuid) | não | Restringe às pessoas (usuário responsável da agenda). UUIDs (maiúsculas são aceitas e convertidas), repetidos ou separados por vírgula; máx. 200; só usuários da organização. UUID malformado dá 400. Ex.: `["5d1b6c2e-8f3a-4c71-9b0e-2a7d4e6f1a10"]` | | `setor` | query | lista de string — um de: `CONSULTORIA`, `PROJETOS`, `COMERCIAL`, `FINANCEIRO`, `MIGRACAO`, `CS` | não | Restringe ao setor efetivo do responsável: CONSULTORIA, PROJETOS, COMERCIAL, FINANCEIRO, MIGRACAO, CS (sem diferenciar maiúsculas). Com `usuarioIds`, vale a interseção. Desconhecido dá 400. Ex.: `["CONSULTORIA"]` | #### Resposta 200 Ocupação do mês. Tipo da raiz: DashboardCard | Campo | Tipo | Sempre presente | Descrição | |---|---|---|---| | `porcentagemConfirmadas` | string | sim | | | `porcentagemPreReservadas` | string | sim | | | `porcentagemReservado` | string | sim | | | `porcentagemRealizadas` | string | sim | | | `porcentagemCanceladas` | string | sim | | | `porcentagemLivre` | string | sim | 100 menos a soma dos demais, nunca negativa. | | `horasPorStatus` | DashboardHorasPorStatus | sim | Horas (NÚMERO, em horas, 2 casas) de cada status; as mesmas que sustentam os percentuais. | | `horasPorStatus.confirmadas` | number | sim | | | `horasPorStatus.preReservadas` | number | sim | | | `horasPorStatus.reservado` | number | sim | | | `horasPorStatus.realizadas` | number | sim | | | `horasPorStatus.canceladas` | number | sim | | | `horasPorStatus.livre` | number | sim | Capacidade que sobra, nunca negativa; 0 sem capacidade. | | `capacidadeHoras` | number | sim | Capacidade do período em HORAS = horas úteis do período x número de pessoas com alguma agenda no período. É a base (100%) dos percentuais. | Exemplo (dados fictícios): ```json { "porcentagemConfirmadas": "12.50", "porcentagemPreReservadas": "3.10", "porcentagemReservado": "8.00", "porcentagemRealizadas": "40.25", "porcentagemCanceladas": "5.00", "porcentagemLivre": "31.15", "horasPorStatus": { "confirmadas": 20, "preReservadas": 4.96, "reservado": 12.8, "realizadas": 64.4, "canceladas": 8, "livre": 49.84 }, "capacidadeHoras": 160 } ``` #### 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 /dashboard/cardSemanal **Ocupação da semana ISO por status, em percentual e em horas** · operationId `obterCardSemanalDashboard` Autenticação: `Authorization: Bearer `. Responde: o mesmo que `cardMensal`, mas para uma **semana ISO** (segunda 00:00 a domingo 23:59:59.999). **Capacidade (base 100%).** `capacidadeHoras` = horas úteis da semana (2023-2025 por tabela fixa, 2026 em diante pela regra de dias úteis; 40 h para semana fora das regras) x número de pessoas distintas com ao menos uma agenda não excluída na semana. **Como o status é contado.** Soma as durações de TODAS as agendas não excluídas (`deleted_at` nulo) da organização cujo `dataInicio` cai no período, por status, em **horas** (minutos / 60). Uma agenda CANCELADA com `deduzir = true` conta como REALIZADA. Agendas PENDENTE e agendas sem status NÃO aparecem em nenhum campo da resposta e NÃO reduzem o `livre` (`livre` = capacidade menos as cinco categorias exibidas), mas contam para o número de pessoas da capacidade. `livre` nunca é negativo (sobreposição/overbooking é cortada em 0). **Unidades.** `porcentagem*`: percentual 0-100 como **TEXTO** com 2 casas; `horasPorStatus.*` e `capacidadeHoras`: **número em horas**. `semanaAno` ecoa o texto recebido sem normalizar (`00-2027` volta `00-2027`); só com os literais `null`/`undefined` vem a semana de hoje no formato `"S AAAA"` (espaço, sem zero à esquerda, ex.: `"7 2026"`). **Permissão:** exige token válido (JWT) e organização identificada. Não há `@Roles` nem checagem de tela. Os filtros `usuarioIds` e `setor` restringem às pessoas informadas (400 se malformados ou `setor` desconhecido). Nenhum escopo por usuário: qualquer usuário autenticado vê as horas de todas as pessoas da organização. Erros: `semanaAno` ausente ou vazio dá 400; formato inválido dá 400. Detalhe: os textos literais `null` e `undefined` passam pelo controller e o serviço os trata como "semana de hoje". #### Parâmetros | Nome | Onde | Tipo | Obrigatório | Descrição | |---|---|---|---|---| | `semanaAno` | query | string | sim | Semana ISO 8601 (segunda a domingo) no formato `SS-AAAA` (também `SS AAAA`), onde AAAA é o ANO ISO (29/12/2025 é a semana 01 de 2026) e SS vai de 0 a 53. `00-2027` é normalizado para a semana ISO real (53-2026). Ausente ou malformado dá 400. Também é lido de `semanaAno[semanaAno]`. Ex.: `11-2026` | | `usuarioIds` | query | lista de string (uuid) | não | Restringe às pessoas (usuário responsável da agenda). UUIDs (maiúsculas são aceitas e convertidas), repetidos ou separados por vírgula; máx. 200; só usuários da organização. UUID malformado dá 400. Ex.: `["5d1b6c2e-8f3a-4c71-9b0e-2a7d4e6f1a10"]` | | `setor` | query | lista de string — um de: `CONSULTORIA`, `PROJETOS`, `COMERCIAL`, `FINANCEIRO`, `MIGRACAO`, `CS` | não | Restringe ao setor efetivo do responsável: CONSULTORIA, PROJETOS, COMERCIAL, FINANCEIRO, MIGRACAO, CS (sem diferenciar maiúsculas). Com `usuarioIds`, vale a interseção. Desconhecido dá 400. Ex.: `["CONSULTORIA"]` | #### Resposta 200 Ocupação da semana. Tipo da raiz: qualquer | Campo | Tipo | Sempre presente | Descrição | |---|---|---|---| | `semanaAno` | string | sim | | Exemplo (dados fictícios): ```json { "semanaAno": "11-2026", "porcentagemConfirmadas": "12.50", "porcentagemPreReservadas": "3.10", "porcentagemReservado": "8.00", "porcentagemRealizadas": "40.25", "porcentagemCanceladas": "5.00", "porcentagemLivre": "31.15", "horasPorStatus": { "confirmadas": 20, "preReservadas": 4.96, "reservado": 12.8, "realizadas": 64.4, "canceladas": 8, "livre": 49.84 }, "capacidadeHoras": 160 } ``` #### 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 /dashboard/indice-cancelamento-mensal **Horas canceladas e horas agendadas por pessoa no mês** · operationId `obterIndiceCancelamentoMensal` Autenticação: `Authorization: Bearer `. Responde: "quanto cada pessoa cancelou em um mês e quanto agendou no total?". É a matéria-prima do índice de cancelamento: **a rota NÃO calcula o percentual**; quem consome divide `Cancelados` por `Total Agendamentos` (ou por Cancelados + Total, conforme a definição de negócio). **Formato.** `resultadoFinal` tem sempre duas séries (`Cancelados` e `Total Agendamentos`). Cada série traz listas paralelas por pessoa (`ordem` = nome, `usuarioIds`, `data` = horas sem arredondar, `horas` = horas com 2 casas). A mesma pessoa ocupa a mesma posição nas duas séries. Pessoas sem nenhuma agenda no período não aparecem. **Cálculo (unidade: HORAS decimais).** Considera agendas não excluídas da organização cujo `dataInicio` cai no período e que tenham consultor. `Cancelados` = horas das agendas com status CANCELADO **exceto** as canceladas com `deduzir = true` (essas contam como REALIZADO). `Total Agendamentos` = horas de TODAS as agendas **não canceladas** (PRE_RESERVAR, RESERVADO, CONFIRMADO, REALIZADO, PENDENTE, sem status e canceladas com `deduzir`), ou seja, NÃO inclui as canceladas. Agrupa por pessoa: o responsável da agenda (reserva no usuário do consultor, depois no consultor). **Permissão:** exige token válido (JWT) e organização identificada. Não há `@Roles` nem checagem de tela. Os filtros `usuarioIds` e `setor` restringem às pessoas informadas (400 se malformados ou `setor` desconhecido). Nenhum escopo por usuário: qualquer usuário autenticado vê as horas de todas as pessoas da organização. Erros: `mesAno` ausente ou com formato inválido responde **400**. #### Parâmetros | Nome | Onde | Tipo | Obrigatório | Descrição | |---|---|---|---|---| | `mesAno` | query | string | sim | Mês de referência. Formatos aceitos: `MM-AAAA` (ex.: `03-2026`, recomendado) ou `Mês AAAA` com o nome do mês em português, capitalizado e com acento (`Março 2026`). Só é lido de `mesAno` (a forma `mesAno[mesAno]` NÃO é aceita nesta rota: sem `mesAno`, responde 400). O mês cobre as agendas cujo INÍCIO (`dataInicio`) cai nele. Ex.: `03-2026` | | `usuarioIds` | query | lista de string (uuid) | não | Restringe às pessoas (usuário responsável da agenda). UUIDs (maiúsculas são aceitas e convertidas), repetidos ou separados por vírgula; máx. 200; só usuários da organização. UUID malformado dá 400. Ex.: `["5d1b6c2e-8f3a-4c71-9b0e-2a7d4e6f1a10"]` | | `setor` | query | lista de string — um de: `CONSULTORIA`, `PROJETOS`, `COMERCIAL`, `FINANCEIRO`, `MIGRACAO`, `CS` | não | Restringe ao setor efetivo do responsável: CONSULTORIA, PROJETOS, COMERCIAL, FINANCEIRO, MIGRACAO, CS (sem diferenciar maiúsculas). Com `usuarioIds`, vale a interseção. Desconhecido dá 400. Ex.: `["CONSULTORIA"]` | #### Resposta 200 Séries do índice de cancelamento do mês. Tipo da raiz: DashboardIndiceCancelamento | Campo | Tipo | Sempre presente | Descrição | |---|---|---|---| | `resultadoFinal` | DashboardSerie[] | sim | Sempre duas séries, nesta ordem: `Cancelados` e `Total Agendamentos`. | | `resultadoFinal[].ordem` | string[] | sim | Nome de cada pessoa (coluna), ordenado por nome. Pode repetir entre homônimos. | | `resultadoFinal[].usuarioIds` | lista de string (uuid) \| null | sim | Usuário de cada coluna, na mesma ordem de `ordem`; nulo quando a agenda está reservada num consultor sem usuário. | | `resultadoFinal[].name` | string — um de: `Cancelados`, `Total Agendamentos` | sim | Nome da série. | | `resultadoFinal[].data` | number[] | sim | Horas (decimais, SEM arredondar) de cada coluna. | | `resultadoFinal[].horas` | number[] | sim | Mesmas horas de `data`, arredondadas a 2 casas. | Exemplo (dados fictícios): ```json { "resultadoFinal": [ { "ordem": [ "Consultor Exemplo", "Outra Consultora Exemplo" ], "usuarioIds": [ "5d1b6c2e-8f3a-4c71-9b0e-2a7d4e6f1a10", "9a3c7e10-2b4d-4f68-a1c5-7e8d9f0a1b22" ], "name": "Cancelados", "data": [ 4, 1.5 ], "horas": [ 4, 1.5 ] }, { "ordem": [ "Consultor Exemplo", "Outra Consultora Exemplo" ], "usuarioIds": [ "5d1b6c2e-8f3a-4c71-9b0e-2a7d4e6f1a10", "9a3c7e10-2b4d-4f68-a1c5-7e8d9f0a1b22" ], "name": "Total Agendamentos", "data": [ 120.5, 98.33333333333333 ], "horas": [ 120.5, 98.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 /dashboard/indice-cancelamento-semanal **Horas canceladas e horas agendadas por pessoa na semana ISO** · operationId `obterIndiceCancelamentoSemanal` Autenticação: `Authorization: Bearer `. Responde: "quanto cada pessoa cancelou em uma semana ISO e quanto agendou no total?". É a matéria-prima do índice de cancelamento: **a rota NÃO calcula o percentual**; quem consome divide `Cancelados` por `Total Agendamentos` (ou por Cancelados + Total, conforme a definição de negócio). **Formato.** `resultadoFinal` tem sempre duas séries (`Cancelados` e `Total Agendamentos`). Cada série traz listas paralelas por pessoa (`ordem` = nome, `usuarioIds`, `data` = horas sem arredondar, `horas` = horas com 2 casas). A mesma pessoa ocupa a mesma posição nas duas séries. Pessoas sem nenhuma agenda no período não aparecem. **Cálculo (unidade: HORAS decimais).** Considera agendas não excluídas da organização cujo `dataInicio` cai no período e que tenham consultor. `Cancelados` = horas das agendas com status CANCELADO **exceto** as canceladas com `deduzir = true` (essas contam como REALIZADO). `Total Agendamentos` = horas de TODAS as agendas **não canceladas** (PRE_RESERVAR, RESERVADO, CONFIRMADO, REALIZADO, PENDENTE, sem status e canceladas com `deduzir`), ou seja, NÃO inclui as canceladas. Agrupa por pessoa: o responsável da agenda (reserva no usuário do consultor, depois no consultor). **Permissão:** exige token válido (JWT) e organização identificada. Não há `@Roles` nem checagem de tela. Os filtros `usuarioIds` e `setor` restringem às pessoas informadas (400 se malformados ou `setor` desconhecido). Nenhum escopo por usuário: qualquer usuário autenticado vê as horas de todas as pessoas da organização. Erros: `semanaAno` ausente ou malformado dá 400. #### Parâmetros | Nome | Onde | Tipo | Obrigatório | Descrição | |---|---|---|---|---| | `semanaAno` | query | string | sim | Semana ISO 8601 (segunda a domingo) no formato `SS-AAAA` (também `SS AAAA`), onde AAAA é o ANO ISO (29/12/2025 é a semana 01 de 2026) e SS vai de 0 a 53. `00-2027` é normalizado para a semana ISO real (53-2026). Ausente ou malformado dá 400. Ex.: `11-2026` | | `usuarioIds` | query | lista de string (uuid) | não | Restringe às pessoas (usuário responsável da agenda). UUIDs (maiúsculas são aceitas e convertidas), repetidos ou separados por vírgula; máx. 200; só usuários da organização. UUID malformado dá 400. Ex.: `["5d1b6c2e-8f3a-4c71-9b0e-2a7d4e6f1a10"]` | | `setor` | query | lista de string — um de: `CONSULTORIA`, `PROJETOS`, `COMERCIAL`, `FINANCEIRO`, `MIGRACAO`, `CS` | não | Restringe ao setor efetivo do responsável: CONSULTORIA, PROJETOS, COMERCIAL, FINANCEIRO, MIGRACAO, CS (sem diferenciar maiúsculas). Com `usuarioIds`, vale a interseção. Desconhecido dá 400. Ex.: `["CONSULTORIA"]` | #### Resposta 200 Séries do índice de cancelamento da semana. Tipo da raiz: DashboardIndiceCancelamento | Campo | Tipo | Sempre presente | Descrição | |---|---|---|---| | `resultadoFinal` | DashboardSerie[] | sim | Sempre duas séries, nesta ordem: `Cancelados` e `Total Agendamentos`. | | `resultadoFinal[].ordem` | string[] | sim | Nome de cada pessoa (coluna), ordenado por nome. Pode repetir entre homônimos. | | `resultadoFinal[].usuarioIds` | lista de string (uuid) \| null | sim | Usuário de cada coluna, na mesma ordem de `ordem`; nulo quando a agenda está reservada num consultor sem usuário. | | `resultadoFinal[].name` | string — um de: `Cancelados`, `Total Agendamentos` | sim | Nome da série. | | `resultadoFinal[].data` | number[] | sim | Horas (decimais, SEM arredondar) de cada coluna. | | `resultadoFinal[].horas` | number[] | sim | Mesmas horas de `data`, arredondadas a 2 casas. | Exemplo (dados fictícios): ```json { "resultadoFinal": [ { "ordem": [ "Consultor Exemplo" ], "usuarioIds": [ "5d1b6c2e-8f3a-4c71-9b0e-2a7d4e6f1a10" ], "name": "Cancelados", "data": [ 2 ], "horas": [ 2 ] }, { "ordem": [ "Consultor Exemplo" ], "usuarioIds": [ "5d1b6c2e-8f3a-4c71-9b0e-2a7d4e6f1a10" ], "name": "Total Agendamentos", "data": [ 31.5 ], "horas": [ 31.5 ] } ] } ``` #### 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.