Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introdução

O Painel de Alocação / Agendamento objetiva exibir a grade de agendamentos previstos para uma determinada Entidade dentro de um determinado Período de Tempo.

[!NOTE] > Só serão exibidos os registros da Entidade que possuam agendamento no período especificado ou que estejam no filtro da Entidade.

Pré-requesitos

São pré-requisitos para esse módulo:

  1. Módulo "Rotas Personalizadas" habilitado e rota personalizada para "/custom/panel/<entidade>" criada (veja como);
  2. Tabela "nfs_generic_panel":
CREATE TABLE `nfs_generic_panel` (
  `SEQ_DB` bigint(20) NOT NULL AUTO_INCREMENT,
  `EMPRESA` int(11) NOT NULL,
  `FILIAL` int(11) NOT NULL,
  `LOCAL` int(11) NOT NULL,
  `PANEL` varchar(100) NOT NULL,
  `TITLE` varchar(100) DEFAULT NULL,
  `MAIN_TABLE` varchar(1000) NOT NULL,
  `OPTIONS` varchar(1000) DEFAULT NULL,
  `QUERY` text,
  `QUERY_STATUS` text,
  `PANEL_DAYS` varchar(1000) DEFAULT NULL,
  `INS_DH` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `ENABLED` int(11) DEFAULT '1',
  `DELETED` int(11) DEFAULT '0',
  UNIQUE KEY `SEQ_DB_UNIQUE` (`SEQ_DB`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;

Configuração Inicial

A configuração inicial segue de acordo com os registros de nfs_generic_panel, com definições a seguir:

1. PANEL: Nome do Ambiente (alias), usado para identificar o Painel;

PANEL='FUNCIONARIO'

2. TITLE: Título do Painel, exibido no topo do Ambiente;

TITLE='Painel de Alocação de Técnico'

3. MAIN_TABLE: QueryBuilder para obter os dados da Entidade do ambiente;

{
  "select": ["fun.SEQ_DB AS ID", "fun.NOME AS DESCRICAO", "fun.FOTO_SEQ_DB"],
  "from": "funcionario fun",
  "where": ["fun.SEQ_DB IN (:seqDb)", "fun.EQUIPE_SEQ_DB IN (:equipe)"],
  "order_by": [["fun.NOME", "ASC"]]
}

4. OPTIONS: Opções usadas em todo o ambiente;

{
  "JORNADA_TRABALHO": 8.25,
  "LABEL": "Funcionários",
  "THUMB": "/assets/img/tecnico/tecnico.png",
  "FEATURED_DAYS": [
    { "DAY": "DOM", "COLOR": "#312312" },
    { "DAY": "SAB", "COLOR": "#312312" }
  ]
}

Onde:

  • JORNADA_TRABALHO: jornada de trabalho (h);
  • LABEL: Label da entidade;
  • THUMB: imagem padrão usada quando a imagem da entidade não estiver disponível.
  • FEATURED_DAYS: Dias que serão destacados com suas cores especificas. Caso não seja especificado a cor será adicionado uma cor automáticamente.

[!NOTE] O parâmentro JORNADA_TRABALHO é importante para a exibição adequada da soma do tempo previsto para cada dia (Entidade vs Período). Se ele não for definido em OPTIONS, será usado o Parêmetro do Sistema "JORNADA_TRABALHO", se definido. Do contrário, será adotada a constante 8 como valor.

INSERT INTO nfs_core_par_parametros (
  EMPRESA, FILIAL, `LOCAL`, NOME, CONTEUDO, TIPO
) VALUES (
  4, 9999, 9999, 'JORNADA_TRABALHO', '8,75', 1
);

Onde:

  • NOME: referência do parâmetro;
  • CONTEUDO: tempo em horas, p. ex.: '8', '8.0', '8,75'.

5. QUERY: QueryBuilder principal;

[!NOTE] Na configuração abaixo LINK_LABEL pode ser configurado com qualquer texto, lembrando que o texto será exibido em todos os cards, logo, foi utilizado anteriormente o texto OS, por este motivo, as bases que foram configuradas estão exibindo o mesmo LINK_LABEL para todos os cards, por isso foi alterado para os.CODIGO, pois assim será exibido o código de cada OS, porém não precisa ser este o padrão e pode ser alterada a exibição conforme a necessidade dos clientes.

2022-09-09_17-59.png

Para configurar os campos a serem exibidos conforme na imagem acima, basta inserir na configuração os respectivos parametros: "TEMPO_APONTADO" "TEMPO_TOTAL" "TEMPO_PADRAO".

[!NOTE] A configuração abaixo é apenas de exemplo, logo, deve ser montada a configuração de acordo com as necessidades e dados disponíveis no ambiente.

{
  "union": {
    "os_tecnico": {
      "select": [
        "os.CODIGO as LINK_LABEL",
        "concat('/t/os_tecnico/edit/', os_t.SEQ_DB) as LINK",
        "sum(os_ser.TEMPO_PADRAO) as TEMPO_TOTAL",
        "sum(os_ser.TEMPO_PADRAO) as TEMPO_APONTADO"
				"sum(os_ser.TEMPO_PADRAO) as TEMPO_PADRAO"
        "os.CODIGO as RELACAO_CODIGO",
        "os_t.SEQ_DB as ENTIDADE_SEQ_DB",
        "os_t.DATA_INICIAL as DATA_INICIAL",
        "os_t.DATA_FINAL as DATA_FINAL",
        "os_t.DIA_INTEIRO as DIA_INTEIRO",
        "os_s.DESCRICAO as STATUS_DESCRICAO",
        "os_s.COR as STATUS_COR",
        "coalesce(cli.DESCRICAO, 'Cliente Indisponível') as RELACAO_DESCRICAO"
      ],
      "from": "os_tecnico os_t",
      "inner_join": [
        ["os_t", "os", "os", "os.SEQ_DB = os_t.OS_SEQ_DB"],
        ["os_t", "status_os", "os_s", "os_s.SEQ_DB = os_t.STATUS_OS_SEQ_DB"]
      ],
      "left_join": [
        ["os", "cliente", "cli", "cli.SEQ_DB = os.CLIENTE_SEQ_DB"],
        ["os", "os_n_servico", "os_ser", "os_ser.OS_SEQ_DB = os.SEQ_DB"]
      ],
      "where": [
        "os_t.DATA_INICIAL IS NOT NULL",
        "os_t.FUNCIONARIO_SEQ_DB = :seqDB",
        "os_s.SEQ_DB in (:status)",
        "date(os_t.DATA_INICIAL) in (:dias) or date(os_t.DATA_FINAL) in (:dias) or :dtIni between date(os_t.DATA_INICIAL) and date(os_t.DATA_FINAL) or :dtFim between date(os_t.DATA_INICIAL) and date(os_t.DATA_FINAL)"
      ],
      "group_by": [
        "RELACAO_CODIGO",
        "ENTIDADE_SEQ_DB",
        "DATA_INICIAL",
        "DATA_FINAL",
        "DIA_INTEIRO",
        "STATUS_DESCRICAO",
        "STATUS_COR",
        "RELACAO_DESCRICAO"
      ],
      "order_by": [["DATA_INICIAL", "ASC"]]
    },
    "agendamento_servico": {
      "select": [
        "'AG' as LINK_LABEL",
        "concat('/t/agendamento_servico/edit/', ags.SEQ_DB) as LINK",
        "0 as TEMPO_TOTAL",
        "ags.ID as RELACAO_CODIGO",
        "ags.SEQ_DB as ENTIDADE_SEQ_DB",
        "ags.INI_DH as DATA_INICIAL",
        "ags.FIM_DH as DATA_FINAL",
        "ags.DIA_INTEIRO as DIA_INTEIRO",
        "os_s.DESCRICAO as STATUS_DESCRICAO",
        "os_s.COR as STATUS_COR",
        "coalesce(cli.DESCRICAO, 'Cliente Indisponível') as RELACAO_DESCRICAO"
      ],
      "from": "agendamento_servico ags",
      "inner_join": [
        ["ags", "status_os", "os_s", "os_s.SEQ_DB = ags.STATUS_SEQ_DB"]
      ],
      "left_join": [
        ["ags", "cliente", "cli", "cli.SEQ_DB = ags.CLIENTE_SEQ_DB"]
      ],
      "where": [
        "ags.INI_DH IS NOT NULL",
        "ags.FUNCIONARIO_SEQ_DB = :seqDB",
        "os_s.SEQ_DB in (:status)",
        "date(ags.INI_DH) in (:dias) or date(ags.FIM_DH) in (:dias) or :dtIni between date(ags.INI_DH) and date(ags.FIM_DH) or :dtFim between date(ags.INI_DH) and date(ags.FIM_DH)"
      ],
      "group_by": [
        "RELACAO_CODIGO",
        "ENTIDADE_SEQ_DB",
        "DATA_INICIAL",
        "DATA_FINAL",
        "DIA_INTEIRO",
        "STATUS_DESCRICAO",
        "STATUS_COR",
        "RELACAO_DESCRICAO"
      ],
      "order_by": [["DATA_INICIAL", "ASC"]]
    }
  }
}

[!IMPORTANT] A estrutura de colunas (Nomes e quantidade de Campos) deve ser mantida!

5. 1. Se o resultado do painel tiver origem em uma única query (sem union), basta remover esse node:

{
  "select": [
    "'OS' as LINK_LABEL",
    "concat('/t/os_tecnico/edit/', os_t.SEQ_DB) as LINK",
    "sum(os_ser.TEMPO_PADRAO) as TEMPO_TOTAL",
    "os.CODIGO as RELACAO_CODIGO",
    "os_t.SEQ_DB as ENTIDADE_SEQ_DB",
    "os_t.DATA_INICIAL as DATA_INICIAL",
    "os_t.DATA_FINAL as DATA_FINAL",
    "os_t.DIA_INTEIRO as DIA_INTEIRO",
    "os_s.DESCRICAO as STATUS_DESCRICAO",
    "os_s.COR as STATUS_COR",
    "coalesce(cli.DESCRICAO, 'Cliente Indisponível') as RELACAO_DESCRICAO"
  ],
  "from": "os_tecnico os_t",
  "inner_join": [
    ["os_t", "os", "os", "os.SEQ_DB = os_t.OS_SEQ_DB"],
    ["os_t", "status_os", "os_s", "os_s.SEQ_DB = os_t.STATUS_OS_SEQ_DB"]
  ],
  "left_join": [
    ["os", "cliente", "cli", "cli.SEQ_DB = os.CLIENTE_SEQ_DB"],
    ["os", "os_n_servico", "os_ser", "os_ser.OS_SEQ_DB = os.SEQ_DB"]
  ],
  "where": [
    "os_t.DATA_INICIAL IS NOT NULL",
    "os_t.FUNCIONARIO_SEQ_DB = :seqDB",
    "os_s.SEQ_DB in (:status)",
    "date(os_t.DATA_INICIAL) in (:dias) or date(os_t.DATA_FINAL) in (:dias) or :dtIni between date(os_t.DATA_INICIAL) and date(os_t.DATA_FINAL) or :dtFim between date(os_t.DATA_INICIAL) and date(os_t.DATA_FINAL)"
  ],
  "group_by": [
    "RELACAO_CODIGO",
    "ENTIDADE_SEQ_DB",
    "DATA_INICIAL",
    "DATA_FINAL",
    "DIA_INTEIRO",
    "STATUS_DESCRICAO",
    "STATUS_COR",
    "RELACAO_DESCRICAO"
  ],
  "order_by": [["DATA_INICIAL", "ASC"]]
}

:::

6. QUERY_STATUS: QueryBuilder do Status do Painel;

{
  "select": ["sts.SEQ_DB as id", "sts.COR as color", "sts.DESCRICAO as text"],
  "from": "status_os sts"
}

[!IMPORTANT] Mesmo que sejam exibidas informações de várias queries usando "union", os status deverão ser os mesmos para que o filtro por status funcione corretamente.

7. PANEL_DAYS: JSON dos filtros Período e Dias:

Para configurar deve-se inserir no campo PANEL_DAYS da tabela nfs_generic_panel o seguinte:

{
  "7": {
    "label": "1 Semana",
    "selected": true,
    "days": [0, 1, 2, 3, 4, 5, 6]
  },
  "10": {
    "label": "10 dias úteis",
    "selected": false,
    "days": [1, 2, 3, 4, 5]
  },
  "20": {
    "label": "20 dias úteis",
    "selected": false,
    "days": [1, 2, 3, 4, 5]
  },
  "30": {
    "label": "30 dias úteis",
    "selected": false,
    "days": [1, 2, 3, 4, 5]
  }
}

Desta forma sera exibida as abas no header da tabela para alternar entre as datas, exibindo de 10 em 10 dias, e na área de filtros será exibido de acordo com a configuração. OBS: Máximo de 30 dias (semana/ 10 dias / 20 dias / 30 dias).

agenda_tecnico.jpeg

Onde:

  • KEY: key principal usada como quantidade de dias do Período;
  • label: label do drop-down de Período;
  • selected: item do Período pré-selecionado ou não;
  • days: dias da semana [0 = DOM, ... 6 = SAB] que serão pré-selecionados do drop-down Dias ao selecionar o item do Período.

8. Filtros opcionais por OS/CHASSI:

É possivel adicionar a tela da agenda técnico filtros por OS e ou por Chassi veja as configurações necessárias nesse tópico.

QUERY

Na tabela nfs_generic_panel adicione a QUERY o JOIN da tabela de equipamento caso queira filtrar por chassi da seguinte forma:

"inner_join": [
				["otd", "os_tecnico", "os_t", "os_t.SEQ_DB = otd.OS_TECNICO_SEQ_DB"],
				["os_t", "os", "os", "os.SEQ_DB = os_t.OS_SEQ_DB"],
				["os", "equipamento", "eqp", "eqp.SEQ_DB = os.EQUIPAMENTO_SEQ_DB"] /* new line */
				...

E na clausula WHERE adicione os itens conforme o exemplo abaixo:

"where": [
				"otd.DATA_INICIAL IS NOT NULL",
				"os.CODIGO = :os", /* new line */
				"eqp.CHASSI = :chassi", /* new line */
				...

Exibição do Chassi no panel

Para exibir o chassi basta incluir no SELECT da query.

"select": [
				"'OS' as LINK_LABEL",
				"eqp.CHASSI as CHASSI", /* new line */
				...

OPTIONS:

Incluir as chaves conforme exemplo para habilitar e desabilitar a exibição dos filtros.

{
	"JORNADA_TRABALHO": 8,
	"LABEL": "Funcionários",
	"CHASSI_FILTER": true, /* new line */
	"OS_FILTER": true /* new line */
	...
}

Indicadores de Tempo de Alocação / Agendamento

Para os indicadores, foram usadas os seguintes dados na ordem a seguir:

  1. Se o campo DIA_INTEIRO for verdadeiro (1), o indicador para a Entidade no Dia será o valor da "JORNADA_TRABALHO";
  2. Se o campo TEMPO_TOTAL for maior > 0 (zero), usa-se esse valor como indicador para a Entidade no Dia (valor expresso em horas). Na query usada em um dos exemplos a coluna "os_n_servico.TEMPO_PADRAO" representa esse valor;
  3. Se os campos DATA_INICIAL E DATA_FINAL estiverem preenchidos, o indicador será o valor compreendido entre essas duas datas. Se o período entre essas duas datas se extender por mais de 1 (hum) dia (p.ex. 01/01/2019 08:00:00 até 05/01/2019 18:00:00), o cálculo será feito diariamente, sendo a data e as horas iniciais e finais (no o exemplo adotado, seriam calculados 10 horas para cada dia, sendo a hora inicial 08:00:00 e a final 18:00:00).
  4. Se apenas o campo DATA_INICIAL estiver preenchido, o indicador será o compreendido entre a hora inicial até as 23:59:59 dessa data.

Scroll automático

Existe um ícone de relógio, que ao ser acionado apresenta uma tela para informação de uma escala de tempo de 1 até 90. Esta escala é o "tempo" que o scroll vai levar para deslizar a tela de uma extremidade até a outra.

Pressione ESC para interromper a rolagem automática.

[!IMPORTANT] O scroll automático não está implementado quando o painel está em fullscreen pelo portlet (ícones na tela)

Dragdrop e Backlog


Os chips de detalhes agora são elementos arrastaveis, onde ao soltar na coluna de uma nova data/funcionário um reagendamento rápido pode ser realizado. O layout dos chips também foi alterado para que ficasse mais compacto, e foi habilitado o scroll horizontal para suportar até 20 colunas.

agenda-técnico-google-chrome-22-september-2023-2.gif

O recurso só está disponível para paineis que usam entrypoint, e pode ser desabilitado e ter algumas configurações que iremos detalhar nessa doc.

Também é possivel configurar o backlog, o backlog são chips de itens que ainda não possuem um técnico definido e que podem ser designados para os técnicos apenas arrastando o chip para a data e funcionario desejado.

Regras de negócio da movimentação de agendamentos

  1. Só é permitido mover agendamentos para o presente ou o futuro

    • Não é possível mover uma atividade para uma data que já passou.

    • Essa regra garante que o agendamento represente ações futuras ou do próprio dia.

  2. Não é permitido duplicar a mesma OS para o mesmo técnico na mesma data

    • Se o técnico já possui aquela OS no dia escolhido, a movimentação é bloqueada.

    • Isso evita que um mesmo serviço apareça duas vezes no mesmo dia para a mesma pessoa.

  3. Se o agendamento for movido para o mesmo técnico e mesma data, nada muda

    • O sistema entende que não houve alteração e, por isso, a movimentação é cancelada.

    • Uma mensagem de aviso será exibida para informar que não há mudanças.

  4. O sistema verifica se há conflito de horários

    • Antes de confirmar a mudança, o sistema confere se a nova posição escolhida está livre.

    • Se já existir outra atividade no mesmo horário, a movimentação será impedida para evitar sobreposição.

  5. Movimentações entre diferentes técnicos ou para o backlog são permitidas

    • É possível mover uma atividade de um técnico para outro, ou ainda, para o backlog (lista geral).

    • Também é permitido mover uma atividade do backlog para um técnico.

  6. Se for uma atividade comum (como uma OS), o sistema pedirá um motivo de remanejamento

    • Nesses casos, ao mover o agendamento, o usuário verá uma janela pedindo que selecione um motivo da alteração.

    • A movimentação só será concluída após a escolha do motivo.

  7. Se for um evento (tipo especial de atividade), a validação é diferente

    • O sistema verifica se o técnico já tem um evento igual na data escolhida.

    • Se tiver, a movimentação é bloqueada e uma mensagem explicativa é exibida.

    • Se não tiver, o evento é salvo automaticamente, sem pedir justificativa.

  8. Se algo der errado no processo, o sistema desfaz a movimentação

    • Caso haja falha ao salvar a nova posição ou algum problema na validação, o agendamento volta automaticamente para a posição anterior.
  9. Todas as ações são acompanhadas de mensagens de aviso ou erro

    • O sistema informa claramente se a movimentação foi concluída, ignorada ou bloqueada, usando mensagens visuais para o usuário.

Layout

design_sem_nome.png

Configurações

Na tabela nfs_core_par_parametros teremos um novo registro com o nome GENERIC_PANEL_TABLE_OPTIONS.

{
  "<ENTIDADE_DO_PAINEL>": {
    "ENTRY_POINT": true,
    "BACKLOG_ENTITY": "backlog",
    "TIME_FORMAT": "H:m",
    "ONLY_SCHEDULES": false,
    "BLOCK_SAME_DAY_EVENT": true,
    "TIME_HIDDEN": true,
    "PROGRESS_VIEW_MODE": "percent" /** new */
  },
  "<ENTIDADE_DO_PAINEL>": {
    "ENTRY_POINT": false
  }
}

<ENTIDADE_DO_PAINEL>: Nome da entidade principal do módulo (EQP, FUNCIONARIO etc.) ;

ENTRY_POINT: Habilita o painel por entrypoint com o valor true. E também habilita do dragdrop;

BACKLOG_ENTITY: Habilita o recurso de backlog.

DRAGDROP: Use DISABLED para desabilitar o dragdrop.

TIME_FORMAT: Use "H:m" para não exibir os segundos nas datas de INI e FIM dos detalhes do CHIP, por padrão usamos "H:m:s".

ONLY_SCHEDULES: Controla a visualização padrão da agenda, se true, exibe apenas os agendados como padrão, se false exibe todos.

BLOCK_SAME_DAY_EVENT: Controla se será permitido que o funcionario tenha dois chips do mesmo evento no mesmo dia, se true, vai bloquear esse cenário, se false poderá ter eventos do mesmo tipo no mesmo dia.

TIME_HIDDEN: Se ativo, oculta os campos de tempo e hora início e fim nos chips.

PROGRESS_VIEW_MODE: Essa configuração define a visualização do texto exibido na barra de progreso, que fica no todo da coluna no dia. Aceita dois diferentes valores, hours ou percent. Por padrão o usado é hours. Abaixo os exemplos.

Percent

ProgressViewModePercent.png

Hours

ProgressViewModeHours.png

[!TIP] Podemos usar os parâmetros show_employees=0 ou show_employees=1 para exibir ou não todos os registros ao abrir o painel.

Entrypoint

Aqui vemos como configurar o painel com entrypoint, basicamente usamos os mesmos parâmetros que passamos no painel por QueryBuilder menos o ID, em entry point vai montar todas as entidades de uma única vez.

Parâmetros

$status = $this->inputValues['status'] ?? NULL;
$dias = $this->inputValues['dias'] ?? NULL;
$dtIni = $this->inputValues['dtIni'] ?? NULL;
$dtFim = $this->inputValues['dtFim'] ?? NULL;
$equipe = $this->inputValues['equipe'] ?? NULL;
$os = $this->inputValues['os'] ?? NULL;
$equipamento = $this->inputValues['equipamento'] ?? NULL;

Tabela nfs_generic_panel passa a receber o termo ENTRY_POINT na coluna QUERY, e receber o ACTION do entrypoint na coluna NAME. E o entrypoint o termo GENERIC_PANEL na coluna FILE_OR_DOMAIN.

O retorno será um array de funcionários cujo o seu ID é o indice, e dentro de cada funcionário está sua respectiva agenda.

Antes enviávamos o SEQDB do técnico e recebíamos um array com seus respectivos agendamentos:

[
    0 => [
        'URL_EDIT' => "concat('/t/os_tecnico_detalhe/edit/', otd.SEQ_DB) as URL_EDIT",
        'ENTIDADE_SEQ_DB' => '...',
        (...)
    ],
    1 => [
        'URL_EDIT' => "concat('/t/os_tecnico_detalhe/edit/', otd.SEQ_DB) as URL_EDIT",
        'ENTIDADE_SEQ_DB' => '...',
        (...)
    ]
]

URL_EDIT é necessário para utilizar o reagendamento usando o drag and drop, é por esse link que iremos salvar a alteração.

Agora, não será mais enviado o SEQDB do técnico nos parâmetros do entrypoint, e precisaremos receber um array de técnicos com essa mesma estrutura dentro de cada técnico, fazendo que todos os que foram contemplados pelo filtro sejam trazidos de uma única vez.

O índice do array precisa ser o SEQDB do técnico, como por exemplo:

9 => [
    0 => [
        'URL_EDIT' => "concat('/t/os_tecnico_detalhe/edit/', otd.SEQ_DB) as URL_EDIT",
        'ENTIDADE_SEQ_DB' => '...',
        (...)
    ],
    1 => [
        'URL_EDIT' => "concat('/t/os_tecnico_detalhe/edit/', otd.SEQ_DB) as URL_EDIT",
        'ENTIDADE_SEQ_DB' => '...',
        (...)
    ]
],
10548 => [
    0 => [
        'URL_EDIT' => "concat('/t/os_tecnico_detalhe/edit/', otd.SEQ_DB) as URL_EDIT",
        'ENTIDADE_SEQ_DB' => '...',
        (...)
    ],
    1 => [
        'URL_EDIT' => "concat('/t/os_tecnico_detalhe/edit/', otd.SEQ_DB) as URL_EDIT",
        'ENTIDADE_SEQ_DB' => '...',
        (...)
    ]
]

E em cada array de técnico teremos os mesmos que antes, nada foi alterado.

E para o backlog o índice dever ser exatamente o texto "backlog", igual a propriedade definida em GENERIC_PANEL_TABLE_OPTIONS.

'backlog' => [
    0 => [
        'URL_EDIT' => "concat('/t/os_tecnico_detalhe/edit/', otd.SEQ_DB) as URL_EDIT",
        'ENTIDADE_SEQ_DB' => '...',
        (...)
    ],
    1 => [
        'URL_EDIT' => "concat('/t/os_tecnico_detalhe/edit/', otd.SEQ_DB) as URL_EDIT",
        'ENTIDADE_SEQ_DB' => '...',
        (...)
    ]
]

Exemplo entrypoint completo

//$_SESSION['DEBUG_MODE'] = 1;
//$this->inputValues = $_SESSION['DEBUG_PARAMS'];

$status = $this->inputValues['status'] ?? NULL;
$dias = $this->inputValues['dias'] ?? NULL;
$dtIni = $this->inputValues['dtIni'] ?? NULL;
$dtFim = $this->inputValues['dtFim'] ?? NULL;
$equipe = $this->inputValues['equipe'] ?? NULL;
$os = $this->inputValues['os'] ?? NULL;
$equipamento = $this->inputValues['equipamento'] ?? NULL;

/*
$status = [1,2,3,4,5,6,7,8,10,12];
$dias = ["2023-12-27 00:00:00","2023-12-28 00:00:00","2023-12-29 00:00:00","2023-12-30 00:00:00","2023-12-31 00:00:00","2024-01-01 00:00:00"];
$dtIni = "2023-12-29 00:00:00";
$dtFim = "2024-01-01 23:59:59";
$equipe = null;
$os = null;
$equipamento = null;
*/

$os_tecnico_detalhe_backlog_list = Dao::table('os_tecnico_detalhe', 'otd')
->select([
	"'os_tecnico_detalhe' as TABELA_CHIP",
	"otd.SEQ_DB as SEQ_DETALHE",
	"'backlog' SEQ_DB",
	"os.CODIGO as LINK_LABEL",
	"concat('/t/os_tecnico_detalhe/edit/', otd.SEQ_DB) as SECONDARY_LINK",
	"0 as TEMPO_TOTAL",
	"concat('/g/OS/dados/', os.SEQ_DB,'?popup=1') as LINK",
	"'fa-pencil-square' as SECONDARY_LINK_ICON",
	"os.OBSERVACAO as OBS_CHIP",
	"cli.DESCRICAO as RELACAO_CODIGO",
	"otd.SEQ_DB as ENTIDADE_SEQ_DB",
	"otd.DATA_INICIAL as DATA_INICIAL",
	"otd.DATA_FINAL as DATA_FINAL",
	"0 as DIA_INTEIRO",
	"os_s.DESCRICAO as STATUS_DESCRICAO",
	"os_s.COR as STATUS_COR",
	"CONCAT(IF(os_s.SEQ_DB = 4,concat('-> ',(select mp.DESCRICAO from app_apontamento_status_os ap inner join app_motivo_pausa mp on mp.SEQ_DB = ap.MOTIVO_PAUSA_SEQ_DB where ap.OS_SEQ_DB = os.SEQ_DB and ap.ATIVO = 1 and mp.ATIVO = 1 order by ap.INI_DH DESC LIMIT 1)),'')) as RELACAO_DESCRICAO"
])
->innerJoin('otd', 'os_tecnico', 'os_t', 'os_t.SEQ_DB = otd.OS_TECNICO_SEQ_DB')
->innerJoin('os_t', 'os', 'os', 'os.SEQ_DB = os_t.OS_SEQ_DB')
->innerJoin('os_t', 'status_os', 'os_s', 'os_s.SEQ_DB = os_t.STATUS_OS_SEQ_DB')
->leftJoin('os', 'cliente', 'cli', 'cli.SEQ_DB = os.CLIENTE_SEQ_DB')
->leftJoin('os', 'os_n_servico', 'os_ser', 'os_ser.OS_SEQ_DB = os.SEQ_DB')
->leftJoin('os', 'agendamento_servico', 'ags', 'ags.SEQ_DB = os.AGENDAMENTO_SERVICO_SEQ_DB')
->whereIsNull('os_t.FUNCIONARIO_SEQ_DB')
->whereIsNotNull('otd.DATA_INICIAL')
->whereIn('os_s.SEQ_DB', $status)
->whereIn('equ.SEQ_DB', $equipe)
->whereIn('os.SEQ_DB', $os)
->whereIn('os.EQUIPAMENTO_SEQ_DB', $equipamento)
->whereRaw('((date(otd.DATA_INICIAL) in (:dias)) or (date(otd.DATA_FINAL) in (:dias)) or (:dtIni between otd.DATA_INICIAL and otd.DATA_FINAL) or (:dtFim between otd.DATA_INICIAL and otd.DATA_FINAL))')
->setParameters([
	'dtIni' => $dtIni,
	'dtFim' => $dtFim,
	'dias' => $dias
])
->orderBy('DATA_INICIAL','ASC')
->get();


$os_tecnico_detalhe_list = Dao::table('os_tecnico_detalhe', 'otd')
->select([
	"'os_tecnico_detalhe' as TABELA_CHIP",
	"otd.SEQ_DB as SEQ_DETALHE",
	"fun.SEQ_DB",
	"os.CODIGO as LINK_LABEL",
	"concat('/t/os_tecnico_detalhe/edit/', otd.SEQ_DB) as SECONDARY_LINK",
	"0 as TEMPO_TOTAL",
	"concat('/g/OS/dados/', os.SEQ_DB,'?popup=1') as LINK",
	"'fa-pencil-square' as SECONDARY_LINK_ICON",
	"os.OBSERVACAO as OBS_CHIP",
	"cli.DESCRICAO as RELACAO_CODIGO",
	"otd.SEQ_DB as ENTIDADE_SEQ_DB",
	"otd.DATA_INICIAL as DATA_INICIAL",
	"otd.DATA_FINAL as DATA_FINAL",
	"0 as DIA_INTEIRO",
	"os_s.DESCRICAO as STATUS_DESCRICAO",
	"os_s.COR as STATUS_COR",
	"CONCAT(IF(os_s.SEQ_DB = 4,concat((select mp.DESCRICAO from app_apontamento_status_os ap inner join app_motivo_pausa mp on mp.SEQ_DB = ap.MOTIVO_PAUSA_SEQ_DB where ap.OS_SEQ_DB = os.SEQ_DB and ap.ATIVO = 1 and mp.ATIVO = 1 order by ap.INI_DH DESC LIMIT 1)),'')) as RELACAO_DESCRICAO"
])
->innerJoin('otd', 'os_tecnico', 'os_t', 'os_t.SEQ_DB = otd.OS_TECNICO_SEQ_DB')
->innerJoin('os_t', 'os', 'os', 'os.SEQ_DB = os_t.OS_SEQ_DB')
->innerJoin('os_t', 'status_os', 'os_s', 'os_s.SEQ_DB = os_t.STATUS_OS_SEQ_DB')
->innerJoin('os_t', 'funcionario', 'fun', 'fun.SEQ_DB = os_t.FUNCIONARIO_SEQ_DB')
->innerJoin('fun', 'equipe', 'equ', 'equ.SEQ_DB = fun.EQUIPE_SEQ_DB')
->leftJoin('os', 'cliente', 'cli', 'cli.SEQ_DB = os.CLIENTE_SEQ_DB')
->leftJoin('os', 'os_n_servico', 'os_ser', 'os_ser.OS_SEQ_DB = os.SEQ_DB')
->leftJoin('os', 'agendamento_servico', 'ags', 'ags.SEQ_DB = os.AGENDAMENTO_SERVICO_SEQ_DB')
->whereIsNotNull('otd.DATA_INICIAL')
->whereIn('os_s.SEQ_DB', $status)
->whereIn('equ.SEQ_DB', $equipe)
->whereIn('os.SEQ_DB', $os)
->whereIn('os.EQUIPAMENTO_SEQ_DB', $equipamento)
->whereRaw('((date(otd.DATA_INICIAL) in (:dias)) or (date(otd.DATA_FINAL) in (:dias)) or (:dtIni between otd.DATA_INICIAL and otd.DATA_FINAL) or (:dtFim between otd.DATA_INICIAL and otd.DATA_FINAL))')
->setParameters([
	'dtIni' => $dtIni,
	'dtFim' => $dtFim,
	'dias' => $dias
])
->orderBy('DATA_INICIAL','ASC')
->get();

$agendamento_servico_detalhe_backlog_list = Dao::table('agendamento_servico_detalhe', 'agd')
->select([
	"'agendamento_servico_detalhe' as TABELA_CHIP",
	"agd.SEQ_DB as SEQ_DETALHE",
	"'backlog' SEQ_DB",
	"agd.ID as LINK_LABEL",
	"concat('/t/agendamento_servico_detalhe/edit/', agd.SEQ_DB) as LINK",
	"0 as TEMPO_TOTAL",
	"concat('/t/agendamento_servico_detalhe/edit/', agd.SEQ_DB) as SECONDARY_LINK",
	"'fa-pencil-square' as SECONDARY_LINK_ICON",
	"'' as OBS_CHIP",
	"ags.DESCRICAO_SOLICITACAO as RELACAO_CODIGO",
	"agd.SEQ_DB as ENTIDADE_SEQ_DB",
	"agd.DATA_INICIAL as DATA_INICIAL",
	"agd.DATA_FINAL as DATA_FINAL",
	"0 as DIA_INTEIRO",
	"os_s.DESCRICAO as STATUS_DESCRICAO",
	"os_s.COR as STATUS_COR",
	"coalesce(cli.DESCRICAO, 'Cliente Indisponível') as RELACAO_DESCRICAO"
])
->innerJoin('agd', 'agendamento_servico', 'ags', 'ags.SEQ_DB = agd.AGENDAMENTO_SERVICO_SEQ_DB')
->innerJoin('ags', 'status_os', 'os_s', 'os_s.SEQ_DB =  10')
->leftJoin('ags', 'cliente', 'cli', 'cli.SEQ_DB = ags.CLIENTE_SEQ_DB')
->whereIsNull('agd.FUNCIONARIO_SEQ_DB')
->whereIsNotNull('agd.DATA_INICIAL')
->whereIn('ags.STATUS_AGENDAMENTO_SERVICO_SEQ_DB', [1,4])
->whereIn('equ.SEQ_DB', $equipe)
->whereIn('os_s.SEQ_DB', $status)
->whereRaw('((date(agd.DATA_INICIAL) in (:dias)) or (date(agd.DATA_FINAL) in (:dias)) or (:dtIni between agd.DATA_INICIAL and agd.DATA_FINAL) or (:dtFim between agd.DATA_INICIAL and agd.DATA_FINAL))')
->setParameters([
	'dtIni' => $dtIni,
	'dtFim' => $dtFim,
	'dias' => $dias
])
->orderBy('DATA_INICIAL','ASC')
->get();


$agendamento_servico_detalhe_list = Dao::table('agendamento_servico_detalhe', 'agd')
->select([
	"'agendamento_servico_detalhe' as TABELA_CHIP",
	"agd.SEQ_DB as SEQ_DETALHE",
	"fun.SEQ_DB",
	"agd.ID as LINK_LABEL",
	"concat('/t/agendamento_servico_detalhe/edit/', agd.SEQ_DB) as LINK",
	"0 as TEMPO_TOTAL",
	"concat('/t/agendamento_servico_detalhe/edit/', agd.SEQ_DB) as SECONDARY_LINK",
	"'fa-pencil-square' as SECONDARY_LINK_ICON",
	"'' as OBS_CHIP",
	"ags.DESCRICAO_SOLICITACAO as RELACAO_CODIGO",
	"agd.SEQ_DB as ENTIDADE_SEQ_DB",
	"agd.DATA_INICIAL as DATA_INICIAL",
	"agd.DATA_FINAL as DATA_FINAL",
	"0 as DIA_INTEIRO",
	"os_s.DESCRICAO as STATUS_DESCRICAO",
	"os_s.COR as STATUS_COR",
	"coalesce(cli.DESCRICAO, 'Cliente Indisponível') as RELACAO_DESCRICAO"
])
->innerJoin('agd', 'agendamento_servico', 'ags', 'ags.SEQ_DB = agd.AGENDAMENTO_SERVICO_SEQ_DB')
->innerJoin('ags', 'status_os', 'os_s', 'os_s.SEQ_DB =  10')
->innerJoin('agd', 'funcionario', 'fun', 'fun.SEQ_DB = agd.FUNCIONARIO_SEQ_DB')
->innerJoin('fun', 'equipe', 'equ', 'equ.SEQ_DB = fun.EQUIPE_SEQ_DB')
->leftJoin('ags', 'cliente', 'cli', 'cli.SEQ_DB = ags.CLIENTE_SEQ_DB')
->whereIsNotNull('agd.DATA_INICIAL')
->whereIn('ags.STATUS_AGENDAMENTO_SERVICO_SEQ_DB', [1,4])
->whereIn('equ.SEQ_DB', $equipe)
->whereIn('os_s.SEQ_DB', $status)
->whereRaw('((date(agd.DATA_INICIAL) in (:dias)) or (date(agd.DATA_FINAL) in (:dias)) or (:dtIni between agd.DATA_INICIAL and agd.DATA_FINAL) or (:dtFim between agd.DATA_INICIAL and agd.DATA_FINAL))')
->setParameters([
	'dtIni' => $dtIni,
	'dtFim' => $dtFim,
	'dias' => $dias
])
->orderBy('DATA_INICIAL','ASC')
->get();


$evento_programacao_backlog_list = Dao::table('evento_programacao', 'eve')
->select([
	"'evento_programacao' as TABELA_CHIP",
	"eve.SEQ_DB as SEQ_DETALHE",
	"'backlog' SEQ_DB",
	"eve.ID as LINK_LABEL",
	"concat('/t/evento_programacao/edit/', eve.SEQ_DB) as LINK",
	"0 as TEMPO_TOTAL",
	"concat('/t/evento_programacao/edit/', eve.SEQ_DB) as SECONDARY_LINK",
	"'fa-pencil-square' as SECONDARY_LINK_ICON",
	"'' as OBS_CHIP",
	"evt.DESCRICAO as RELACAO_CODIGO",
	"eve.SEQ_DB as ENTIDADE_SEQ_DB",
	"eve.DATA_INICIAL as DATA_INICIAL",
	"eve.DATA_FINAL as DATA_FINAL",
	"0 as DIA_INTEIRO",
	"os_s.DESCRICAO as STATUS_DESCRICAO",
	"os_s.COR as STATUS_COR",
	"'' as RELACAO_DESCRICAO"
])
->innerJoin('eve', 'status_os', 'os_s', 'os_s.SEQ_DB = 12')
->innerJoin('eve', 'evento', 'evt', 'evt.SEQ_DB = eve.EVENTO_SEQ_DB')
->whereIsNull('eve.FUNCIONARIO_SEQ_DB')
->whereIsNotNull('eve.DATA_INICIAL')
->whereIn('equ.SEQ_DB', $equipe)
->whereIn('os_s.SEQ_DB', $status)
->whereRaw('((date(eve.DATA_INICIAL) in (:dias)) or  (date(eve.DATA_FINAL) in (:dias)) or  (:dtIni between eve.DATA_INICIAL and eve.DATA_FINAL) or (:dtFim between eve.DATA_INICIAL and eve.DATA_FINAL))')
->setParameters([
	'dtIni' => $dtIni,
	'dtFim' => $dtFim,
	'dias' => $dias
])
->orderBy('DATA_INICIAL','ASC')
->get();

$evento_programacao_list = Dao::table('evento_programacao', 'eve')
->select([
	"'evento_programacao' as TABELA_CHIP",
	"eve.SEQ_DB as SEQ_DETALHE",
	"fun.SEQ_DB",
	"eve.ID as LINK_LABEL",
	"concat('/t/evento_programacao/edit/', eve.SEQ_DB) as LINK",
	"0 as TEMPO_TOTAL",
	"concat('/t/evento_programacao/edit/', eve.SEQ_DB) as SECONDARY_LINK",
	"'fa-pencil-square' as SECONDARY_LINK_ICON",
	"'' as OBS_CHIP",
	"evt.DESCRICAO as RELACAO_CODIGO",
	"eve.SEQ_DB as ENTIDADE_SEQ_DB",
	"eve.DATA_INICIAL as DATA_INICIAL",
	"eve.DATA_FINAL as DATA_FINAL",
	"0 as DIA_INTEIRO",
	"os_s.DESCRICAO as STATUS_DESCRICAO",
	"os_s.COR as STATUS_COR",
	"'' as RELACAO_DESCRICAO"
])
->innerJoin('eve', 'status_os', 'os_s', 'os_s.SEQ_DB = 12')
->innerJoin('eve', 'funcionario', 'fun', 'fun.SEQ_DB = eve.FUNCIONARIO_SEQ_DB')
->innerJoin('fun', 'equipe', 'equ', 'equ.SEQ_DB = fun.EQUIPE_SEQ_DB')
->innerJoin('eve', 'evento', 'evt', 'evt.SEQ_DB = eve.EVENTO_SEQ_DB')
->whereIsNotNull('eve.DATA_INICIAL')
->whereIn('equ.SEQ_DB', $equipe)
->whereIn('os_s.SEQ_DB', $status)
->whereRaw('((date(eve.DATA_INICIAL) in (:dias)) or  (date(eve.DATA_FINAL) in (:dias)) or  (:dtIni between eve.DATA_INICIAL and eve.DATA_FINAL) or (:dtFim between eve.DATA_INICIAL and eve.DATA_FINAL))')
->setParameters([
	'dtIni' => $dtIni,
	'dtFim' => $dtFim,
	'dias' => $dias
])
->orderBy('DATA_INICIAL','ASC')
->get();

$dados_list = array_merge($os_tecnico_detalhe_backlog_list, $os_tecnico_detalhe_list, $agendamento_servico_detalhe_backlog_list, $agendamento_servico_detalhe_list, $evento_programacao_backlog_list, $evento_programacao_list);
$dados_array = [];
if (!empty($dados_list)) {
	foreach ($dados_list as $row) {
		$dados_array[$row['SEQ_DB']][] = $row;
	}
}

$this->queryData['dados'] = $dados_array;
//print_r($this->queryData);

PainelDays

Campo da tabela nfs_generic_panel. Pode ser usado para montar filtros até 20 dias.

{
  "7": {
    "label": "1 Semana",
    "selected": true,
    "days": [0, 1, 2, 3, 4, 5, 6]
  },
  "10": {
    "label": "10 dias úteis",
    "selected": false,
    "days": [1, 2, 3, 4, 5]
  },
  "15": {
    "label": "15 dias",
    "selected": false,
    "days": [0, 1, 2, 3, 4, 5, 6]
  },
  "20": {
    "label": "20 dias úteis",
    "selected": false,
    "days": [1, 2, 3, 4, 5]
  }
}

Holidays (Feriados)

Para o Agenda Técnico (Generic Panel), temos o recurso de destacar colunas de feriados, que consite em pintar o <th> da coluna e exibir um hint com a descrição do feriado ao passar o mouse.

HolidayExample.png

O recurso não precisa ser ativado, desde que se use a tabela FERIADO no padrão esperado:

  • tabela: app_feriado
  • campo data: DATA_FERIADO
  • campo descricao: DESCRICAO

Caso a base não tenha uma tabela com esse padrão, um entrypoint de configuração será necessário, abaixo está o exemplo de preenchimento do campo CODE:

{
  "table": "app_feriado", /** nome da tabela de feriados */
  "fields": {
    "date": "DATA_FERIADO", /** campo onde a data que o feriado ocorre. Ex: 2025-09-04 00:00:00 */
    "description": "DESCRICAO" /** campo com a descricao do feriado. Ex: Dia de São Nunca */
  },
  "defaults": {
    "holiday_color": "violet" /** cor do destaque do feriado. Ex: blue ou #000000 */
  }
}

O exemplo acima também é usado como fallback na falta da config ou de campos obrigatórios.

Entrypoint

INSERT INTO nfs_entry_point (
    FILE_OR_DOMAIN, `ACTION`, XMOVA_INSTALLCODE, XMOVA_INSTALLCODE_VERSION,
    EMPRESA, FILIAL, `LOCAL`, TABELA, ENTRY_NUM, CODE, VERSION, BUILD, ATIVO, CODETYPE,
    FIELD, DESCRIPTION, INS_DH, UPD_DH, NFS_USER, DB_USER
  ) VALUES(
    'CONFIG', 'HOLIDAYS', NULL, NULL, 9999, 9999, 9999, 'FERIADO', NULL,
    '{
      "table": "app_feriado",
      "fields": {
        "date": "DATA_FERIADO",
        "description": "DESCRICAO"
      },
      "defaults": {
        "holiday_color": "violet"
      }
    }',
    1, 1, 1, 'JSON', NULL, NULL, '2025-08-29 17:35:20', '2025-09-02 17:29:37', NULL, NULL);