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:
- Módulo "Rotas Personalizadas" habilitado e rota personalizada para "/custom/panel/<entidade>" criada (veja como);
- 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.
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).
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:
- Se o campo DIA_INTEIRO for verdadeiro (1), o indicador para a Entidade no Dia será o valor da "JORNADA_TRABALHO";
- 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;
- 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).
- 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.
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
-
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.
-
-
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.
-
-
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.
-
-
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.
-
-
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.
-
-
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.
-
-
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.
-
-
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.
-
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
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
Hours
[!TIP] Podemos usar os parâmetros
show_employees=0
oushow_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.
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);