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

Guia NFS

Bem vindo ao guia oficial para configuração de funcionalidades do NFS.

Todas as funcionalidades que foram criadas após o inicio da documentação devem ser escritas pelo time do NFS Core e o que é antes dela é construído sobre demanda, então tem muitas telas que estão em construção e caso não encontre algo que procure por favor notifique-nos.

Atenciosamente Time NFS Core.

Acessibilidade

A funcionalidade 'ALTO CONTRASTE' foi criada com o objetivo de aplicar alterações visuais vizando aumentar à acessibilidade da aplicação WEB.

Configurar 'ALTO CONTRASTE'

Para habilitar a funcionalidade na aplicação do cliente, é preciso definir o parâmetro 'HIGH_CONTRAST_ENABLED' como 'True' (valor = 1 na coluna 'CONTEUDO') na tabela 'nfs_core_par_parametros'.

Abaixo segue um exemplo de SQL INSERT de como configurar a tabela 'nfs_core_par_parametros':

	INSERT INTO nfs_homol_abengoa_construmobil.nfs_core_par_parametros
(EMPRESA, FILIAL, `LOCAL`, NOME, CONTEUDO, TIPO)
VALUES(9999, 9999, 9999, 'HIGH_CONTRAST_ENABLED', '1', 1);

Resultado esperado

hc1.png

hc2.png

Banco de Dados

Introdução

A classe DAO, acrônomo para Data Access Objects / Objetos de Acesso a Dados, é uma classe NFS criada para abstrair o acesso aos dados do NFS que visa simplificar, padronizar, dar clareza e agilizar o desenvolvimento de EntryPoints em PHP. Seu uso é fortemente indicado para manter a integridade e a evolução constante do Framework NFS.

Alguns exemplos


// Obtem lista de registros (array)
$lista = Dao::table('EQP')->get();

// Obtem lista de registros ativos
$listaSomenteAtivos = Dao::table('EQP')->enables()->get();

// Obtem lista de registros inativos
$listaSomenteInativos = Dao::table('EQP')->disables()->get();

// Obtem lista de registros (ativos e inativos)
$listaAtivosEInativos = Dao::table('EQP')->all()->get();

// Obter dados de um registro com seus relacionamentos padrões (trazendo as _FK)
$dadosDoEquipamento = Dao::table('EQP')->seqDb(345);

// modo lazy (sem relacionamentos)
Dao::table('EQP')->seqDb(14, true);
// ou
Dao::table('EQP')->lazy()->seqDb(14, true);

// Se precisar só de um campo da tabela (só funciona em modo lazy):
$descricaoEquipamento = Dao::table('EQP')->select(['DESCRICAO'])->seqDb(14, true);

/*
 * Obter lista de registros agrupados por SEQ_DB
 * (usado com frequência em buscas e for's / montagem de EntryPoint / Relatórios)
 */
$rows = Dao::table('equipamento')
        ->orderBy('DESCRICAO','DESC')
        ->groupBy('SEQ_DB')
        ->toArray()
        ->get();

Métodos Básicos / Padrões

table

/**
* Alias para Dao::from(), só que chamado estaticamente
*
* @param string $table Nome da tabela
* @param string $alias Apelido da tabela (será usado o nome da tabela se for nulo)
*
* @return $this
*/
table($table, $alias = null): self

from

/**
* Origem dos dados da query (sem prefixo APP_)
* ::from('table', 'alias')
*
* @param string $table Nome da tabela
* @param string $alias Apelido da tabela (será usado o nome da tabela se for nulo)
*
* @return $this
*/
from($table, $alias): self

select

/**
* Colunas da Query
* ::select(['col1', 'col2', n])
* ::select('col1', 'col2')
* ::select('*')
*
* @param mixed $fields
*
* @return $this
*/
select($fields): self

Exemplo:

$boletins = Dao::table('boletim')
    ->select('seq_db', 'empresa', 'filial', 'local', 'ini_dh', 'fim_dh')
    ->orderBy('filial', 'asc')
    ->limit(5)
    ->get();
echo '<pre>';
print_r($boletins);
echo '</pre>';

get()

/**
* Executa query e retorna registros
*
* @return array
*/
get(): array

Exemplo get()

$boletins = Dao::table('boletim')->get();
echo '<pre>';
print_r($boletins);
echo '</pre>';

first()

/**
* Executa query e retorna primeiro registro
* de acordo com orderBy
*
* @return array
*/
first(): array

Exemplo first()

$boletins = Dao::table('boletim')->first();
echo '<pre>';
print_r($boletins);
echo '</pre>';

Métodos de Junção

innerJoin

/**
* Registra inner join à consulta
*
* @param string $fromAlias
* @param string $table
* @param string $joinAlias
* @param string $condition
*
* @return $this
*/
innerJoin($fromAlias, $table, $joinAlias, $condition): self

Exemplo innerJoin

$dados = Dao::table('apontamento', 'ap')
    ->select([
        'ap.seq_db as apontamento_seq_sb',
        'cli.descricao',
        'cli.loja',
        'cli.endereco',
        'est.descricao as estado',
        'est.sigla'
    ])
    ->innerJoin('ap', 'cliente', 'cli', 'cli.seq_db = ap.cliente_seq_db')
    ->innerJoin('cli', 'estado', 'est', 'est.seq_db = cli.estado_seq_db')
    ->orderBy('cli.descricao', 'asc')
    ->get();

echo '<pre>';
print_r($dados);
echo '</pre>';

leftJoin

/**
* Registra left join à consulta
*
* @param string $fromAlias
* @param string $table
* @param string $joinAlias
* @param string $condition
*
* @return $this
*/

leftJoin($fromAlias, $table, $joinAlias, $condition): self

Exemplo leftJoin

$boletins = Dao::table('boletim', 'b')
    ->select([
        'b.seq_db boletim_seq_db',
        'b.empresa',
        'b.filial',
        'b.local',
        'b.ini_dh',
        'b.fim_dh',
        'a.seq_db as apontamento_seq_sb',
        'd.km_inicial',
        'd.km_final',
    ])
    ->innerJoin('b', 'apontamento', 'a', 'a.seq_db_device_master_seq_db = b.seq_db')
    ->leftJoin('a', 'apontamento_deslocamento', 'd', 'd.apontamento_seq_db = a.seq_db')
    ->orderBy('b.seq_db', 'desc')
    ->get();

echo '<pre>';
print_r($boletins);
echo '</pre>';

rightJoin

/**
* Registra right join à consulta
*
* @param string $fromAlias
* @param string $table
* @param string $joinAlias
* @param string $condition
*
* @return $this
*/
rightJoin($fromAlias, $table, $joinAlias, $condition): self

Exemplo rightJoin

$boletins = Dao::table('boletim', 'b')
    ->select([
        'b.seq_db boletim_seq_db',
        'b.empresa',
        'b.filial',
        'b.local',
        'b.ini_dh',
        'b.fim_dh',
        'a.seq_db as apontamento_seq_sb',
        'd.km_inicial',
        'd.km_final'
    ])
    ->innerJoin('b', 'apontamento', 'a', 'a.seq_db_device_master_seq_db = b.seq_db')
    ->rightJoin('a', 'apontamento_deslocamento', 'd', 'd.apontamento_seq_db = a.seq_db')
    ->orderBy('b.seq_db', 'desc')
    ->todos()
    ->get();

echo '<pre>';
print_r($boletins);
echo '</pre>';

Métodos de Filtro

where

/**
* Adiciona filtro igual (where x = y) com operador lógico AND
* ::where(['key' => $value, n])
* ::where(['a' => ':a', 'ab' => ':ab', n])
*
* @param array $condition
*
* @return $this
*/
where(array $conditions, $type = 'AND'): self

Exemplo where

$eqps = Dao::table('apontamento_equipamento', 'ae')
    ->where([
    'ae.CLIENTE_SEQ_DB' => 1396,
    ])
    ->get();
echo '<pre>';
print_r($eqps);
echo '</pre>';

orWhere

/**
* Alias para adiciona filtro igual (where x = y) com operador lógico OR
* ::where(['a' => 'b', 'ab' => '3', n])
* ::where(['a' => ':a', 'ab' => ':ab', n])
*
* @param array $condition
*
* @return $this
*/
orWhere(array $conditions): self

Exemplo orWhere

$eqps = Dao::table('apontamento_equipamento', 'ae')
    ->where(['ae.CLIENTE_SEQ_DB' => 1396, 'ae.FUNCIONARIO_SEQ_DB' => 887])
    ->orWhere(['ae.CLIENTE_SEQ_DB' => 98])
    ->get();
echo '<pre>';
print_r($eqps);
echo '</pre>';

whereDiff

/**
* Adiciona filtro Diferente (where x <> y) e especifica operador lógico
*
* @param string $key Coluna da condição
* @param mixed $value Valor
* @param string $type Operador lógico das cláusulas (AND / OR)
*
* @return $this
*/
whereDiff($key, $value, $type = 'AND')

Exemplo whereDiff

$eqps = Dao::table('apontamento_equipamento', 'ae')
    ->where(['ae.CLIENTE_SEQ_DB' => 1396, 'ae.FUNCIONARIO_SEQ_DB' => 887])
    ->whereDiff('SEQ_DB_DEVICE', '1548159687880')
    ->get();
echo '<pre>';
print_r($eqps);
echo '</pre>';

whereGreaterThan

/**
* Adiciona filtro maior que (where x > y) e especifica operador lógico
*
* @param string $key Coluna da condição
* @param mixed $value Valor
* @param string $type Operador lógico das cláusulas (AND / OR)
*
* @return $this
*/
whereGreaterThan($key, $value, $type = 'AND')

Exemplo whereGreaterThan

$eqps = Dao::table('apontamento_equipamento', 'ae')
    ->select('ae.SEQ_DB', 'ae.CLIENTE_SEQ_DB', 'ae.FUNCIONARIO_SEQ_DB', 'ae.HORIMETRO')
    ->where(['ae.CLIENTE_SEQ_DB' => 1396, 'ae.FUNCIONARIO_SEQ_DB' => 887])
    ->whereGreaterThan('HORIMETRO', 125)
    ->get();
echo '<pre>';
print_r($eqps);
echo '</pre>'/

whereGreaterEqualsThan

/**
* Adiciona filtro maior ou igual a (where x >= y) e especifica operador lógico
*
* @param string $key Coluna da condição
* @param mixed $value Valor
* @param string $type Operador lógico das cláusulas (AND / OR)
*
* @return $this
*/
whereGreaterEqualsThan($key, $value, $type = 'AND')

Exemplo whereGreaterEqualsThan

$eqps = Dao::table('apontamento_equipamento', 'ae')
    ->select('ae.SEQ_DB', 'ae.CLIENTE_SEQ_DB', 'ae.FUNCIONARIO_SEQ_DB', 'ae.HORIMETRO')
    ->where(['ae.CLIENTE_SEQ_DB' => 1396, 'ae.FUNCIONARIO_SEQ_DB' => 887])
    ->whereGreaterEqualsThan('HORIMETRO', 125)
    ->get();
echo '<pre>';
print_r($eqps);
echo '</pre>';

whereLessThan

/**
* Adiciona filtro meno que (where x < y) e especifica operador lógico
*
* @param string $key Coluna da condição
* @param string $value Valor
* @param string $type Operador lógico das cláusulas (AND / OR)
*
* @return $this
*/

whereLessThan($key, $value, $type = 'AND')

Exemplo whereLessThan

$eqps = Dao::table('apontamento_equipamento', 'ae')
    ->select('ae.SEQ_DB', 'ae.CLIENTE_SEQ_DB', 'ae.FUNCIONARIO_SEQ_DB', 'ae.HORIMETRO')
    ->where(['ae.CLIENTE_SEQ_DB' => 1396, 'ae.FUNCIONARIO_SEQ_DB' => 887])
    ->whereLessThan('HORIMETRO', 125)
    ->get();
echo '<pre>';
print_r($eqps);
echo '</pre>';

whereLessEqualsThan

/**
* Adiciona filtro menor ou igual a (where x <= y) e especifica operador lógico
*
* @param string $key Coluna da condição
* @param string $value Valor
* @param string $type Operador lógico das cláusulas (AND / OR)
*
* @return $this
*/
whereLessEqualsThan($key, $value, $type = 'AND')

Exemplo whereLessEqualsThan

$eqps = Dao::table('apontamento_equipamento', 'ae')
    ->select('ae.SEQ_DB', 'ae.CLIENTE_SEQ_DB', 'ae.FUNCIONARIO_SEQ_DB', 'ae.HORIMETRO')
    ->where(['ae.CLIENTE_SEQ_DB' => 1396, 'ae.FUNCIONARIO_SEQ_DB' => 887])
    ->whereLessEqualsThan('HORIMETRO', 125)
    ->get();
echo '<pre>';
print_r($eqps);
echo '</pre>';

whereIn

/**
* Adiciona filtro entre valores (where x in (a,b,c)) e especifica operador lógico
*
* @param string $key Coluna da condição
* @param string|array $value Valor ou lista de valores
* @param string $type Operador lógico das cláusulas (AND / OR)
*
* @return $this
*/
whereIn($key, $value, $type = 'AND')

Exemplo whereIn

$eqps = Dao::table('apontamento_equipamento', 'ae')
    ->select('ae.SEQ_DB', 'ae.CLIENTE_SEQ_DB', 'ae.FUNCIONARIO_SEQ_DB', 'ae.HORIMETRO')
    ->where(['ae.CLIENTE_SEQ_DB' => 1396, 'ae.FUNCIONARIO_SEQ_DB' => 887])
    ->whereIn('HORIMETRO', [125, 123])
    ->get();
echo '<pre>';
print_r($eqps);
echo '</pre>';

whereNotIn

/**
* Adiciona filtro não entre valores que (where x not in (a,b,c)) e especifica operador lógico
*
* @param string $key Coluna da condição
* @param string|array $value Valor ou lista de valores
* @param string $type Operador lógico das cláusulas (AND / OR)
*
* @return $this
*/
whereNotIn($key, $value, $type = 'AND')

Exemplo whereNotIn

$eqps = Dao::table('apontamento_equipamento', 'ae')
    ->select('ae.SEQ_DB', 'ae.CLIENTE_SEQ_DB', 'ae.FUNCIONARIO_SEQ_DB', 'ae.HORIMETRO')
    ->where(['ae.CLIENTE_SEQ_DB' => 1396, 'ae.FUNCIONARIO_SEQ_DB' => 887])
    ->whereNotIn('HORIMETRO', '125, 123')
    ->get();
echo '<pre>';
print_r($eqps);
echo '</pre>';

whereIsNull

/**
* Adiciona filtro é nulo (where x is null) e especifica operador lógico
*
* @param string $key Coluna da condição
* @param string $type Operador lógico das cláusulas (AND / OR)
*
* @return $this
*/
whereIsNull($key, $type = 'AND')

Exemplo whereIsNull

$eqps = Dao::table('apontamento', 'ae')
    ->select('ae.SEQ_DB', 'ae.FUNCIONARIO_SEQ_DB', 'ae.FIM_DH')
    ->where(['ae.FUNCIONARIO_SEQ_DB' => 849])
    ->whereIsNull('ae.FIM_DH')
    ->limit(10)
    ->orderBy('INS_DH', 'DESC')
    ->get();

echo '<pre>';
print_r($eqps);
echo '</pre>';

whereIsNotNull

/**
* Adiciona filtro não é nulo (where x > y) e especifica operador lógico
*
* @param string $key Coluna da condição
* @param string $type Operador lógico das cláusulas (AND / OR)
*
* @return $this
*/
whereIsNotNull($key, $type = 'AND')

Exemplo whereIsNotNull

$eqps = Dao::table('apontamento', 'ae')
    ->select('ae.SEQ_DB', 'ae.FUNCIONARIO_SEQ_DB', 'ae.FIM_DH')
    ->where(['ae.FUNCIONARIO_SEQ_DB' => 849])
    ->whereIsNotNull('ae.FIM_DH')
    ->limit(10)
    ->orderBy('INS_DH', 'DESC')
    ->get();

echo '<pre>';
print_r($eqps);
echo '</pre>';

whereLike

/**
* Adiciona condição Like
*
* @param string $key Coluna da condição
* @param mixed $value Valor de filtro
* @param string $type Operador lógico das cláusulas (AND / OR)
*
* @return $this
*/
whereLike($key, $value, $type = 'AND')

Exemplo whereLike


$eqps = Dao::table('cliente', 'cli')
    ->select('cli.SEQ_DB', 'cli.DESCRICAO', 'cli.BAIRRO')
    ->whereLike('cli.BAIRRO', 'RURAL')
    ->get();
echo '<pre>';
print_r($eqps);
echo '</pre>';

whereStartsWith

/**
* Adiciona condição Like que começa com o valor especificado
*
* @param string $key Coluna da condição
* @param mixed $value Valor de filtro
* @param string $type Operador lógico das cláusulas (AND / OR)
*
* @return $this
*/
whereStartsWith($key, $value, $type = 'AND')

Exemplo whereStartsWith

$eqps = Dao::table('cliente', 'cli')
    ->select('cli.SEQ_DB', 'cli.DESCRICAO', 'cli.BAIRRO')
    ->whereStartsWith('cli.BAIRRO', 'RURAL')
    ->get();
echo '<pre>';
print_r($eqps);
echo '</pre>';

whereEndsWith

/**
* Adiciona condição Like que termina com o valor especificado
*
* @param string $key Coluna da condição
* @param mixed $value Valor de filtro
* @param string $type Operador lógico das cláusulas (AND / OR)
*
* @return $this
*/
whereEndsWith($key, $value, $type = 'AND')

Exemplo whereEndsWith

$eqps = Dao::table('cliente', 'cli')
    ->select('cli.SEQ_DB', 'cli.DESCRICAO', 'cli.BAIRRO')
    ->whereEndsWith('cli.BAIRRO', 'RURAL')
    ->get();
echo '<pre>';
print_r($eqps);
echo '</pre>';

whereBetween

/**
* Adiciona condição por range de dois valores
*
* @param string $key Coluna da condição
* @param mixed $ini Valor inicial
* @param mixed $fim Valro final
* @param string $type Operador lógico das cláusulas (AND / OR)
*
* @return $this
*/
whereBetween($key, $ini, $fim, $type = 'AND')

Exemplo whereBetween

$eqps = Dao::table('apontamento_equipamento', 'ae')
    ->select('ae.SEQ_DB', 'ae.CLIENTE_SEQ_DB', 'ae.FUNCIONARIO_SEQ_DB', 'ae.HORIMETRO')
    ->where(['ae.CLIENTE_SEQ_DB' => 1396, 'ae.FUNCIONARIO_SEQ_DB' => 887])
    ->whereBetween('ae.HORIMETRO', 100, 200)
    ->get();
echo '<pre>';
print_r($eqps);
echo '</pre>';

whereDate

/**
* Adiciona condição de filtro por data específica
*
* @param string $key Coluna da condição
* @param string $date Valor Data inicial
* @param string $type Operador lógico das cláusulas (AND / OR)
*
* @return $this
*/
whereDate($key, $date, $type = 'AND')

Exemplo whereDate

$eqps = Dao::table('apontamento', 'ae')
    ->select('ae.SEQ_DB', 'ae.INI_DH', 'ae.FIM_DH')
    ->whereDate('ae.INI_DH', '16/01/2019')
    ->limit(100)
    ->get();
echo '<pre>';
print_r($eqps);
echo '</pre>';

whereDateBetween

/**
* Adiciona condição por range de duas datas
*
* @param string $key
* @param string $ini Valor Data inicial
* @param string $fim
* @param string $type
*
* @return $this
*/
whereDateBetween($key, $ini, $fim, $type = 'AND')

Exemplo whereDateBetween

$eqps = Dao::table('apontamento', 'ae')
    ->select('ae.SEQ_DB', 'ae.INI_DH', 'ae.FIM_DH')
    ->whereDateBetween('ae.INI_DH', '20/01/2019', '31/01/2019')
    ->limit(100)
    ->get();
echo '<pre>';
print_r($eqps);
echo '</pre>';

whereGroup e orWhereGroup

/**
* Adiciona condição where agrupada
*
* @param string $key
* @param string $ini Valor Data inicial
* @param string $fim
* @param string $type
*
* @return $this
*/
whereGroup(array $conditions)|| orWhereGroup(array $conditions)

Exemplo whereGroup

$eqps = Dao::table('apontamento_equipamento', 'ae')
    ->select(['ae.CLIENTE_SEQ_DB', 'ae.OS_SEQ_DB'])
    ->where([
        'ae.CLIENTE_SEQ_DB' => 17503,
    ])
    ->whereGroup([
        'OR' => [
            ['ae.OS_SEQ_DB', '=', 18067],
            ['ae.OS_SEQ_DB', '=', 18025],
        ],
    ])
    ->get();
echo '<pre>';
print_r($eqps);
echo '</pre>';

whereRaw

/**
 * Adiciona filtro usando termos SQL.
 *
 * @param string $sql     Cláusula condicional SQL
 * @param mixed  $values  Valores da Cláusula
 * @param string $boolean Operador lógico das cláusulas (AND / OR)
 *                        'AND' se não especificado
 */
public function whereRaw($sql, $values = [], $boolean = 'AND'): self

Exemplo whereRaw

$eqps = Dao::table('apontamento_equipamento', 'ae')
    ->where(['ae.CLIENTE_SEQ_DB' => 1396, 'ae.FUNCIONARIO_SEQ_DB' => 887])
    ->whereRaw('SEQ_DB_DEVICE = 1548159687880')
    ->whereRaw('INI_DH between :ini and :fim', [
        'ini' => '2021-10-01 08:00:00',
        'fim' => '2021-10-01 23:59:59'
    ])
    ->get();

echo '`<pre>`';
print_r($eqps);
echo '`</pre>`';

// usando ::setParameters
$eqps = Dao::table('apontamento_equipamento', 'ae')
    ->where(['ae.CLIENTE_SEQ_DB' => 1396, 'ae.FUNCIONARIO_SEQ_DB' => 887])
    ->whereRaw('INI_DH between :ini and :fim')
    ->setParameters([
        'ini' => '2021-10-01 08:00:00',
        'fim' => '2021-10-01 23:59:59'
    ])
    ->get();

echo '`<pre>`';
print_r($eqps);
echo '`</pre>`';

orWhereRaw

/**
 * Alias para adicionar filtro usando termos SQL e operador ‘OR’.
 *
 * @param string $sql    Cláusula condicional SQL
 * @param mixed  $params Valores da Cláusula
 */
public function orWhereRaw($sql, $params = []): self

Exemplo orWhereRaw

$eqps = Dao::table('apontamento_equipamento', 'ae')
    ->where([
        'ae.CLIENTE_SEQ_DB' => 1396,
        'ae.FUNCIONARIO_SEQ_DB' => 887
    ])
    ->orWhereRaw('SEQ_DB_DEVICE = 1548159687880')
    ->get();

echo '`<pre>`';
print_r($eqps);
echo '`</pre>`';

Método de Ordenação, Limite e filtros rápidos

orderBy

/**
* Ordenação do resultado da consulta
* ::orderBy('coluna', 'ASC|DESC')
*
* @param string $field
* @param string $order
*
* @return $this
*/
orderBy($field, $order = ''): self

limit

/**
* Define número de registros retornados
*
* @param integer $limit
*
* @return $this
*/
limit($limit): self

enables()

/**
* Define que a consulta deve retornar apenas registros ativos
*
* @return $this
*/
enables(): self

disables()

/**
* Define que a consulta deve retornar apenas registros inativos
*
* @return $this
*/
disables(): self

all()

/**
* Define que a consulta deve retornar registros ativos e inativos
*
* @return $this
*/
all(): self

corporate()

/**
* Se deve aplicar filtros de filial/local na consulta ou não.
*/
public function corporate(?bool $corporate = true): self

groupBy

/**
* Agrupamento da query ou do array resultante (Legacy)
* ::groupBy('coluna')
* @calegarifabio @todo fazer/testar
* @param string $field
* @param string $order
*
* @return $this
*/
groupBy($field): self

getDS()

/**
* Retorna a estrtura de dados (DS - Data Structure) da tabela
* @calegarifabio
*
* @return object
*/
getDS(): object

getSql()

/**
* Retorna o sql/statemnt que foi gerado
*/
getSql()

echoSql()

/**
* Imprie o sql/statemnt que foi gerado e faz a consulta
*/
echoSql(): self

Metódos Legado

toArray()

/**
* Agrupamento dos registros pela CHAVE informada no GroupBy
* @tabelaBO
* @legacy
*
* @return $this
*/
toArray(): self

updateRow

/**
* Agrupamento dos registros pela CHAVE informada no GroupBy
* @tabelaBO
* @param array $values
*
* @return Retorno
*/
updateRow($aValues)

updateGpsPoint

/**
* Agrupamento dos registros pela CHAVE informada no GroupBy
* @tabelaBO
* @param float $latitude
* @param float $longitude
* @param string $posicaoDh
* @param string $fieldUpdate (INI ou FIM para tabelas Boletim)
*
* @return Retorno
*/
updateGpsPoint($latitude, $longitude, $posicaoDh = '', $fieldUpdate = 'POSICAO')

getSqlUpdateFklFields

/**
* Método que cria os SQL de update dos Campos FKL que usam essa tabela do primeiro parametro
* @param Tabela $tabela Recebe o objeto tabela (MOBILE_TABLE > 0)
* @param int $rowSeqDb É o SEQ_DB do registro que acabou de ser inserido ou do registro que estásendo alterado
* @param array $valores Array com os valores para insert/update que essa função usará para montaros updates
* @param boolean $insert para verificar se a tabela informada está com esse recurso desativado
*
* Tratado para compreender dois ou mais campos que apontam para a mesma entidade, por exemplo:
* TECNICO e AUXILIAR
* EQUIPAMENTO e IMPLEMENTO
*/
getSqlUpdateFklFields($tabela, $rowSeqDb, $valores, $insert = true)

getSeqDbFromSeqDbDevice

/**
* Metodo que pega o SEQ_DB de uma master ou mobile a partir do SEQ_DB_DEVICE
*
*/
getSeqDbFromSeqDbDevice($seqDbDevice): int

Outros

Tabela Upload:

getUploadContent

/**
	* Retorna o campo conteudo da tabela NFS_UPLOAD em formato BASE64
	* ->getUploadContent($fotoSeqDb, 'FOTO')
  * ->getUploadContent($fotoSeqDb).
*/
public function getUploadContent(mixed $fieldSeqDb, string $fieldAlias = null): self

Acesso à tabela

$dados = Dao::table('upload')
   ->where(['SEQ_DB' => 1234])
   ->first();

Filtro Private User

private

/** executa a consulta ignorando o filtro de private user */
$disablePrivate = Dao::table('TABLE')->select(['SEQ_DB'])->private(false)->get();

Link para Private User Docs

Configurações

Vídeos

Parte 1 | Parte 2 | Parte 3 |

Exibir registros Inativos em Relatórios e Gráficos

Para a exibição de dados de registros inativos onde se faz necessário, foi criada a chave inativos nas configurações de QueryBuilder. Assim, caso seja necessários selecionar também os registros inativos em determinados processos, basta adicionar essa chave com valor true; o valor padrão é ausente/false (somente registros onde ATIVO é igual a 1).

{
	"locais":{
		"dump": "",
		"inativos": true,
		"main": true,
		"select":[
			"sum(if(e.FLAG_PROPRIO=1, 1, 0)) FIBRIA",
			"sum(if(e.FLAG_PROPRIO=0, 1, 0)) PROVEDOR",
			"(select l.NOME from nfs_org_local l where e.LOCAL = l.SEQ_DB)'FILIAL'"
		],
		"from":"equipamento e",
		"group_by":[
			"e.LOCAL"
		]
	},
  ...
}

Obter Data Atual

Para obter a data atual de acordo com o fuso horário da filial, para isso foi criado o seguinte parâmetro:

:NFS_NOW (Necessário os dois pontos)

Obter EMPRESA e/ou FILIAL e/ou LOCAL

Para obter a data atual de acordo com o empresa da filial, para isso foi criado o seguinte parâmetro:

:NFS_EMPRESA (Necessário os dois pontos)

Para obter a data atual de acordo com o filial da filial, para isso foi criado o seguinte parâmetro:

:NFS_FILIAL (Necessário os dois pontos)

Para obter a data atual de acordo com o local da filial, para isso foi criado o seguinte parâmetro:

:NFS_LOCAL (Necessário os dois pontos)

Como usar

Eles devem ser usados no select e/ou where como no exemplo abaixo para o :NFS_NOW:

"secondary_table": {
      "select": [
        "1 as funcionarioApontando",
        "BOLETIM.FIM_DH FIM_DH",
        "BOLETIM.FUNCIONARIO_SEQ_DB",
        "BOLETIM.SEQ_DB BOLETIM_SEQ_DB",
        ":NFS_NOW AGORA",
        "(select sec_to_time(sum(time_to_sec(TIMEDIFF(if(a.FIM_DH is null,:NFS_NOW,a.FIM_DH),a.INI_DH)))) from app_apontamento a inner join app_boletim b on b.SEQ_DB = a.SEQ_DB_DEVICE_MASTER_SEQ_DB where a.TIPO_APONTAMENTO = 1 and b.FIM_DH is null and a.SEQ_DB_DEVICE_MASTER_SEQ_DB = BOLETIM.SEQ_DB) as WORKING"
      ],
      "from": "BOLETIM",
  ...
 }

Como é possível observar foi criar valor com o apelido AGORA onde é usado o parâmetro :NFS_NOW, também é usando para calcular as horas trabalhadas onde verifica se a data final é nulla e caso verdadeiro usa a data atual.

Obter Imagem da tabela NFS_UPLOAD

Para obter o conteúdo da imagem e usa-lo em algum relatório, é necessário utilizar o seguinte parâmetro:

~upload:fk_foto:apelido
  • ~upload: é uma constante, funciona com uma palavra chave.
  • fk_foto: Aqui é o nome da coluna que é usada no query builder que faz relacionamento com a imagem salva na NFS_UPLOAD.
  • apelido: É o apelido atribuido a coluna, onde será usada no relatório.

O retorno é a coluna CONTEUDO da tabela NFS_UPLOAD.

Exemplo:

{
  "os_list": {
    "select": [
      "distinct os.SEQ_DB SEQ_DB",
      "os.CODIGO CODIGO",
      "os.FILIAL FILIAL",
      "st.DESCRICAO STATUS_OS",
      "os.EQUIPAMENTO_SEQ_DB",
      "osf.FUNCIONARIO_SEQ_DB",
      "f.NOME",
      "f.CRACHA",
      "~upload:f.foto_seq_db:foto"
    ],
    "from": "os os",
    "with": ["cliente:descricao,codigo", "funcionario:nome"],
    "left_join": [
      ["os", "os_tecnico", "osf", "osf.OS_SEQ_DB = os.SEQ_DB"],
      ["osf", "funcionario", "f", "f.SEQ_DB = osf.FUNCIONARIO_SEQ_DB"],
      [
        "os",
        "apontamento",
        "a",
        "a.OS_SEQ_DB = os.SEQ_DB AND osf.FUNCIONARIO_SEQ_DB = a.FUNCIONARIO_SEQ_DB"
      ],
      ["os", "cliente", "c", "c.SEQ_DB  = os.CLIENTE_SEQ_DB"],
      ["osf", "status_os", "st", "osf.status_os_seq_db  = st.SEQ_DB"]
    ],
    "where": ["os.SEQ_DB in (:os)", "osf.FUNCIONARIO_SEQ_DB in (:funcionario)"]
  }
}

No caso acima na tabela de FUNCIONARIO tem a coluna FOTO_SEQ_DB que é uma fk para a tabela NFS_UPLOAD, e o apelido foto que o seu valor será a imagem em base64.

Opção HAVING

Para utilizar a opção having do sql basta colocar a seguinte opção no json:

"having" : ["condicao_1" , "condicao_2"]

Opção Union

Para utilizar o Union no Nfs Query Builder é bem simples, basicamento é a união de duas nfs query builder com apenas um resultado, a sua estrutura é

{
  "union": {
    "query_1": {
      // select, from ,where...
    },
    "query_2": {
      // select, from ,where...
    }
  }
}

Para entrar na condição é necessário ter primeiro a chave union e dentro dela uma ou mais nfs query builder, no caso acima tem a query_1 e a query_2.

ATENÇÃO É importante dizer que o resultado das duas consultas devem ser iguais.

Opção group_result_by e group_result_by_unique

A funcionalidade group_result_by recebe um único parâmetro e deve ser o nome do campo que deseja com chave do array resultante. Veja abaixo um exemplo (use o recurso no admin/testCode do NFS para ver o resultado):

$json = '{"eqp_apontamentos":{
		"select":["a.SEQ_DB","a.EQP_SEQ_DB"],
		"from":"eqp_apt a",
		"where":["DATE(a.INI_DH) = DATE(NOW())"],
		"order_by": [["a.SEQ_DB", "ASC"]]
		}}';
$array = json_decode($json, true);
var_dump(\core\NFSQueryBuilder::getQuery($array));

Nesse caso acima serão listados todos os apontamentos do dia com SEQ_DB do apontamento e SEQ_DB do EQP e CHAVE do array é um sequencial que começa em 0.

Se você precisar agrupar o resultado do array pelo SEQ_DB equipamento (para usar num código ou twig):

$json = '{"eqp_apontamentos":{
		"select":["a.SEQ_DB","a.EQP_SEQ_DB"],
		"from":"eqp_apt a",
		"where":["DATE(a.INI_DH) = DATE(NOW())"],
    "order_by": [["a.SEQ_DB", "ASC"]],
    "group_result_by": "EQP_SEQ_DB"
		}}';
$array = json_decode($json, true);
var_dump(\core\NFSQueryBuilder::getQuery($array));

Pronto, veja que o próprio CORE criou um array ordenado.

ATENÇÃO Alguns códigos/entryPoint/Report estão fazendo uso de campo no group_result_by que NÃO ESTÃO NOS CAMPOS DO SELECT. E também não deve ser usado o alias da tabela, exemplo de uso errado: a.EQP_SEQ_DB. ATENÇÃO, o campo do group_result_by deve ser único, portanto, use um ALIAS se for preciso. Exemplo abaixo:

$json = '{"eqp_apontamentos":{
		"select":["a.SEQ_DB","a.EQP_SEQ_DB GROUP_EQP"],
		"from":"eqp_apt a",
		"where":["DATE(a.INI_DH) = DATE(NOW())"],
    "order_by": [["a.SEQ_DB", "ASC"]],
    "group_result_by": "GROUP_EQP"
		}}';
$array = json_decode($json, true);
var_dump(\core\NFSQueryBuilder::getQuery($array));

SOBRE O group_result_by_unique

Utilize group_result_by_unique no lugar de group_result_by quando o campo que será usado como índice é o próprio SEQ_DB da tabela. Esse uso é muito comum para recuperar um cadastro e usar para impressão dos dados. Veja o exemplo abaixo:

Com group_result_by você precisaria colocar equipamento[1][0].CODIGO e usando group_result_by_unique você só precisa do primeiro índice equipamento[1].CODIGO

Caso esse agrupamento seja usado em um campo com muitos registros (Exemplo: EQP_CLASSE_SEQ_DB) somente UM registro será retornado em cada cahve do array.

$json = '{	"equipamentos": {
		"select": ["eqp.SEQ_DB","eqp.EQP_CLASSE_SEQ_DB",
			"eqp.CODIGO",
			"eqp.DESCRICAO"
		],
		"from": "eqp eqp",
		"group_result_by_unique": "SEQ_DB",
		"order_by": [
			["eqp.SEQ_DB", "ASC"]
		]
	}
}';
$array = json_decode($json, true);
var_dump(\core\NFSQueryBuilder::getQuery($array));

Exemplo - Porcentagem Total de Atividades Produtivas e o Detalhado por Atividades Improdutivas

No exemplo abaixo o resulta da primeira query é a porcentagem de todas as atividades produtivas e a segunda a porcentagem por atividades improdutivas, passando um período como parâmetro.

{
  "union": {
    "produtiva": {
      "select": [
        "concat ('produtiva') name",
        "sum(a.INI_FIM_DIFF_SEC) y",
        "concat('HORAS: ',(round((select sum(a2.INI_FIM_DIFF_SEC) total from app_apontamento_maquina a2 inner join app_grupo_atividade g2 on g2.SEQ_DB = a2.GRUPO_ATIVIDADE_SEQ_DB inner join app_atividade ati2 on a2.ATIVIDADE_SEQ_DB = ati2.SEQ_DB where g2.ativo = 1 and g2.DELETED = 0 and a2.ATIVO = 1 and a2.DELETED = 0 and a2.filial = a.filial and g2.filial = g.filial and date(a2.INI_DH) >= min(date(a.INI_DH)) and date(a2.INI_DH) <= max(date(a.INI_DH)))/ 3600, 2))) legend"
      ],
      "from": "apontamento_maquina a",
      "inner_join": [
        ["a", "grupo_atividade", "g", "g.SEQ_DB = a.GRUPO_ATIVIDADE_SEQ_DB"],
        ["a", "atividade", "ati", "a.ATIVIDADE_SEQ_DB = ati.SEQ_DB"]
      ],
      "where": [
        "date(a.INI_DH) >= date_format(STR_TO_DATE(:ini, '%d/%m/%Y'), '%Y-%m-%d')",
        "date(a.INI_DH) <= date_format(STR_TO_DATE(:fim, '%d/%m/%Y'), '%Y-%m-%d')",
        "ati.FLAG_PRODUTIVA = 1"
      ],
      "group_by": ["name"]
    },
    "paradas": {
      "select": [
        "concat(g.DESCRICAO,'::', ati.DESCRICAO) name",
        "sum(a.INI_FIM_DIFF_SEC) y",
        "concat('HORAS: ',(round((select sum(a2.INI_FIM_DIFF_SEC) total from app_apontamento_maquina a2 inner join app_grupo_atividade g2 on g2.SEQ_DB = a2.GRUPO_ATIVIDADE_SEQ_DB inner join app_atividade ati2 on a2.ATIVIDADE_SEQ_DB = ati2.SEQ_DB where g2.ativo = 1 and g2.DELETED = 0 and a2.ATIVO = 1 and a2.DELETED = 0 and a2.filial = a.filial and g2.filial = g.filial and date(a2.INI_DH) >= min(date(a.INI_DH)) and date(a2.INI_DH) <= max(date(a.INI_DH)))/ 3600, 2))) legend"
      ],
      "from": "apontamento_maquina a",
      "inner_join": [
        ["a", "grupo_atividade", "g", "g.SEQ_DB = a.GRUPO_ATIVIDADE_SEQ_DB"],
        ["a", "atividade", "ati", "a.ATIVIDADE_SEQ_DB = ati.SEQ_DB"]
      ],
      "where": [
        "date(a.INI_DH) >= date_format(STR_TO_DATE(:ini, '%d/%m/%Y'), '%Y-%m-%d')",
        "date(a.INI_DH) <= date_format(STR_TO_DATE(:fim, '%d/%m/%Y'), '%Y-%m-%d')",
        "ati.FLAG_PRODUTIVA = 0"
      ],
      "group_by": ["a.GRUPO_ATIVIDADE_SEQ_DB", "a.ATIVIDADE_SEQ_DB, a.FILIAL"]
    }
  }
}

É possível ver o resultado acessando como simova.admin@simova.com.br na base da SuzanoSA de homologação, clicando no menu do Usuário entre na opção Admin Console e na opção NFS ENTRY POINT clique no item Test Code, ao abrir apague os exemplos que são carregados, por fim copie e cole o código abaixo e aberte o botão Testar.

$json = '{"union":{"produtiva":{"select":["concat (\'produtiva\') name","sum(a.INI_FIM_DIFF_SEC) y","concat(\'HORAS: \',(round((select sum(a2.INI_FIM_DIFF_SEC) total from app_apontamento_maquina a2 inner join app_grupo_atividade g2 on g2.SEQ_DB = a2.GRUPO_ATIVIDADE_SEQ_DB inner join app_atividade ati2 on a2.ATIVIDADE_SEQ_DB = ati2.SEQ_DB where g2.ativo = 1 and g2.DELETED = 0 and a2.ATIVO = 1 and a2.DELETED = 0 and a2.filial = a.filial and g2.filial = g.filial and date(a2.INI_DH) >= min(date(a.INI_DH)) and date(a2.INI_DH) <= max(date(a.INI_DH)))/ 3600, 2))) legend"],"from":"apontamento_maquina a","inner_join":[["a","grupo_atividade","g","g.SEQ_DB = a.GRUPO_ATIVIDADE_SEQ_DB"],["a","atividade","ati","a.ATIVIDADE_SEQ_DB = ati.SEQ_DB"]],"where":["date(a.INI_DH) >= date_format(STR_TO_DATE(\'01/02/2019\', \'%d/%m/%Y\'), \'%Y-%m-%d\')","date(a.INI_DH) <= date_format(STR_TO_DATE(\'28/02/2019\', \'%d/%m/%Y\'), \'%Y-%m-%d\')","ati.FLAG_PRODUTIVA = 1"],"group_by":["name"]},"paradas":{"select":["concat(g.DESCRICAO,\'::\', ati.DESCRICAO) name","sum(a.INI_FIM_DIFF_SEC) y","concat(\'HORAS: \',(round((select sum(a2.INI_FIM_DIFF_SEC) total from app_apontamento_maquina a2 inner join app_grupo_atividade g2 on g2.SEQ_DB = a2.GRUPO_ATIVIDADE_SEQ_DB inner join app_atividade ati2 on a2.ATIVIDADE_SEQ_DB = ati2.SEQ_DB where g2.ativo = 1 and g2.DELETED = 0 and a2.ATIVO = 1 and a2.DELETED = 0 and a2.filial = a.filial and g2.filial = g.filial and date(a2.INI_DH) >= min(date(a.INI_DH)) and date(a2.INI_DH) <= max(date(a.INI_DH)))/ 3600, 2))) legend"],"from":"apontamento_maquina a","inner_join":[["a","grupo_atividade","g","g.SEQ_DB = a.GRUPO_ATIVIDADE_SEQ_DB"],["a","atividade","ati","a.ATIVIDADE_SEQ_DB = ati.SEQ_DB"]],"where":["date(a.INI_DH) >= date_format(STR_TO_DATE(\'01/02/2019\', \'%d/%m/%Y\'), \'%Y-%m-%d\')","date(a.INI_DH) <= date_format(STR_TO_DATE(\'28/02/2019\', \'%d/%m/%Y\'), \'%Y-%m-%d\')","ati.FLAG_PRODUTIVA = 0"],"group_by":["a.GRUPO_ATIVIDADE_SEQ_DB","a.ATIVIDADE_SEQ_DB, a.FILIAL"]}}}';

$array = json_decode($json, true);
var_dump(\core\NFSQueryBuilder::getQuery($array));

Opção dump

Os valores possíveis são:

  • sql Vai apresentar na tela a query montada e os parâmetros disponíveis para uso.
  • result Retorna os dados DESSA query em formato JSON (antes do método de tratamento de dados)
  • resultData Retorna os dados DESSA query em formato JSON (após método de tratamento de dados)
  • resultAll Retorna os dados de TODAS as queries em formato JSON.
"dump" : "sql|result|resultData|resultAll"

Opção DEBUG_INFO

Os dados disponíveis são:

$_SESSION['DEBUG_MODE'] = 1;
print_r($_SESSION['DEBUG_CONFIG']);
print_r($_SESSION['DEBUG_PARAMS']);
print_r($_SESSION['DEBUG_SQL']);

Legendas

Hoje é possível todas as cores e descrições das legendas serem dinânmicas, isso ajuda em situações onde quer o painel não deve ser o padrão, por exemplo por padrão boletim fechado é sempre preto, mas há situação onde o cliente quer configurar um cor caso o último boletim fechado já faz mais de 3 dias, então é possível fazer esse cálculo na quer e definir uma cor.

Legenda - Main Table

Legenda - Secondary Table

Legenda - Additional Tables

Provedor em Nuvem

Provedor em Nuvem ou Cloud Provider que vamos abordar nessa documentação é muito usado em grandes empresas pela sua fácil integração e amplos serviços, como exemplo Azure, Google Cloud, e outras.

Azure

Hoje no NFS usamos criamos a integração com o OneDrive/Sharepoint com a Azure:

Azure

Hoje no NFS usamos criamos a integração com o OneDrive/Sharepoint com a Azure:

Configuração e Permissões

Configuração

Passo 1 - Microsoft Entra ID

A primeira coisa é com o acesso a Azure criar um novo App Registration, para isso é necessário ir em Microsoft Entra ID como na imagem abaixo

MS Entra ID

Se for a primera vez, vá em Create a Resource e crie um novo Microsoft Entra ID.

Passo 2 - App Registration

MS Entra ID

Com o seu resource criado, criar um novo App Registration, como na imagem acima. Ele vai ser o serviço de configuração entre a Simova e o Cliente.

Register an Application

MS Entra ID

Registre o nome do app, que será a ponte entre a simova e o sistema do cliente, e pode escolher a primeira opção, que é Single Tenant. Isso pode depender da organização.

Redirect URI, nesse primeiro passo não é necessário.

Por fim, clique em Register no fim da tela.

Passo 3 - Client ID e Tenant ID

Agora com o App criado, a simova precisa de 2 informações importantes dessa nova tela, que são:

  • Client ID: O Identificador da Organização
  • Tenant ID: O Identificador do Locatário, ou em outras palavras o ID do app criado.

MS Entra ID

Passo 4 - Certificates & Secrets

Nesse último passo é necessário criar um Client Secrets, para isso vá no menu em Manage > Certificates & Secrets, que será um segredo entre o NFS e o Client, para não ser necessário realizar um login com algum usuário.

Seguinte a imagem clique em New client secret

MS Entra ID

Em Add a client secret, adicione uma Description e escolha o melhor tempo de acordo com a necessidade do cliente para expirar o segredo.

MS Entra ID

Agora copie e salve o Value do seu novo Client secrets, porque você não vai conseguir vê-lo novamente.

MS Entra ID

Passo 5

Agora as três informações necessárias para o NFS autenticar com a Azure são:

  • Client ID
  • Tenant ID
  • Secret Value

OBS: Compartilhe o Secret Value com o menor número de pessoas possíveis, não compartilhando em e-mails e/ou em grupos de conversa.

Permissões Sharepoint/onedrive

Para gravar arquivos no sharepoint/onedrive é necessário que o cliente dê permissões de escritas pela sua API, então vá no menu Manage > API permissions

Vamos precisar das seguintes permissões:

  • Application.Read.All
  • Files.ReadWrite.All
  • Sites.Read.All
  • Sites.ReadWrite.All
  • User.Read.All

Passos para Adicionar Permissão

Primeiro navege no menu Manage até o item API permissions como é possível observar na imagem abaixo e clique em Add a permission

MS Entra ID

Selecione Microsoft Graph

MS Entra ID

Depois Application permissions

MS Entra ID

Busque pelas permissions, marcando uma a uma, são elas:

  • Application.Read.All
  • Files.ReadWrite.All
  • Sites.Read.All
  • Sites.ReadWrite.All
  • User.Read.All

E no fim cliquem em Add permissons

MS Entra ID

Uma observação muito importante nesse passo é que o usuário que está realizando a configuração seja um admin ou requisite para o admin realizar um Grant admin consent for ... como é possível na imagem, na coluna Status está Granted for ..., porque assim vai ser possível do NFS escrever um arquivo no onedrive/sharepoint.

MS Entra ID

Configuração - NFS

Para configurar o provedor em nuvem (Cloud Provider) é necessário que o cliente execute os passos descritos na o

Após isso é necessário configurar na nfs_core_par_parametros o parâmetro CLOUD_PROVIDER.

Azure

Para configurar o cloud provider da Azure hoje é necessário:

{
    "azure": {
        "client_id": "",
        "client_secret": "",
        "tenant_id": ""
    }
}
  • azure: nome da azure
    • client_id: client id configurado na azure
    • client_secret: client secret configurado na azure
    • tenant_id: tenant id configurado na azure.

Sharepoint

Salvar Relatório

Hoje é possível de após o cliente ter feito a configuração e realizada as permissões no Azure de salvar um relatório em seu sharepoint.

Hoje por entrypoint basta fazer:

$param = new StorageParams();
$param->setReportName('assistencia_tecnica');
$param->setReportParams(array('os' => 1));
$param->setTargetPath('LOCAL_0101');
$param->setRemoteFileName('assist_tecnica_01.pdf');
$param->setReportFileType('pdf');
$param->setType('azure');
$result = $this->storageService->uploadReport($param);

Como é possível observar no exemplo a primeira coisa é instanciar um novo StorageParams

$params = new StorageParams();

Criada a sua variável StorageParams o próximo passo é popular os métodos:

$param->setReportName('assistencia_tecnica');
$param->setReportParams(array('os' => 1));
$param->setTargetPath('LOCAL_0101');
$param->setRemoteFileName('assist_tecnica_01.pdf');
$param->setReportFileType('pdf');
$param->setType('azure');
  • setReportName(string): Nome do relatório que está na nfs_reports;
  • setReportParams(array): Os parâmetros do relatório em formato de array, no exemplo, estamos passando os com o seq_db 1;
  • setTargetPath(string): Uma string com o diretório onde vai ser salvo o arquivo no storage do cliente, exemplo, 'nfs/reports/assit_tec', cada barra é um nível do diretório, se não exister ele será criado.
  • setRemoteFileName(string): Nome do arquivo que será salvo no destino, é necessária a extensão;
  • setReportFileType(string): Tipo do relatório, hoje somente é possível pdf que é o criado a partir do html.
  • setType(string): Tipo do Storage, hoje o padrão é azure, se mas pode ser que amanhã seja tenhamos mais uma opção como google cloud.

Por fim, é realizado o upload:

$result = $this->storageService->uploadReport($param);

Em casos de erro vai retornar um array com message e type:

array(2) {
  ["message"]=>
  string(33) "Cloud Provider not found: azureee"
  ["type"]=>
  string(5) "error"
}

Logo se tiver o type igual a erro você pode realizar alguma ação.

Console

DS/DDL para Gestão de Banco de Dados

Esta documentação aborda a funcionalidade dos botões DS/DDL usados para gerenciar a criação e atualização de estruturas em um banco de dados. Cada botão possui um propósito específico, facilitando operações distintas como a criação de tabelas e a atualização da estrutura do banco de dados. A seguir, descrevemos cada um dos botões e suas funções correspondentes.

Botões DS/DDL

Botão "Full"

  • Objetivo: Criar a estrutura completa do banco de dados e atualizar o banco com novas tabelas.
  • Funcionalidade: Ao clicar no botão "Full", o sistema executa um script que gera todas as tabelas necessárias para o banco de dados, além de atualizar estruturas existentes com novos elementos (colunas, índices, etc.). É útil para garantir que o banco de dados esteja completamente sincronizado com o modelo de dados atual.

Botão "App"

  • Objetivo: Criar a estrutura básica das tabelas do aplicativo.
  • Funcionalidade: Este botão foca na criação de tabelas e estruturas que são especificamente utilizadas pelo aplicativo. Ao selecioná-lo, apenas as tabelas essenciais para o funcionamento do aplicativo são criadas ou atualizadas.

Botão "NFS"

  • Objetivo: Criar tabelas e estruturas básicas do Core do sistema.
  • Funcionalidade: O botão "NFS" é usado para configurar ou atualizar as tabelas que formam o núcleo do sistema, ou seja, as estruturas fundamentais sobre as quais outras funcionalidades são construídas.

Tela de Execução

Ao clicar em qualquer um dos botões descritos acima, abrirá uma tela de execução que contém vários botões de controle e monitoramento da execução dos scripts. Abaixo, detalhamos cada um dos botões presentes nesta tela:

Botão de Fechar Detalhes

  • Funcionalidade: Oculta os detalhes de cada execução, permitindo uma visão mais compacta das operações realizadas.

Botão de Scroll Automático

  • Funcionalidade: Ativa o deslocamento automático da tela para acompanhar a última ação executada, garantindo que o usuário possa visualizar em tempo real as operações mais recentes sem necessidade de scroll manual.

Botão Erro

  • Funcionalidade: Filtra e mostra apenas as execuções que resultaram em erros, facilitando a identificação e correção de problemas.

Botão Warning

  • Funcionalidade: Filtra e exibe apenas as execuções que geraram alertas (warnings), que não são críticos como erros, mas que indicam potenciais questões que necessitam atenção.

Botão Sucesso

  • Funcionalidade: Este botão filtra e exibe as execuções que foram completadas com sucesso, sem erros ou warnings.

Botão Copiar

  • Funcionalidade: Permite copiar todas as queries executadas para a 
    área de transferência, facilitando o compartilhamento ou análise das operações realizadas.

     

Execução via Terminal


Para administradores e desenvolvedores que preferem a linha de comando, o sistema permite a execução de migrações e atualizações do banco de dados via terminal. O script **Migrate.php** é utilizado para este propósito, e pode ser configurado para operar em diferentes escopos e funcionalidades de acordo com os parâmetros fornecidos. Abaixo está a descrição de como usar o script e exemplos práticos de uso.

Uso do Script

O script é executado da seguinte forma:

php command/Migrate.php [sub-domain] [empresa] [tipo] [tabela-nome]

  • sub-domain: Pode ser especificado como **all** para executar a operação em todos os subdomínios disponíveis.
  • empresa: Similar ao sub-domain, pode ser **all** para abranger todas as empresas registradas no sistema.
  • tipo: Define o tipo de operação a ser realizada, como criação de tabelas, verificação de campos, entre outros.
  • tabela-nome: Especifica uma tabela específica para a operação. Se deixado em branco, o script atuará em todas as tabelas relevantes para o tipo de operação selecionado.

Tipos de Operação

Os tipos de operação que podem ser especificados são:

  • full: Execução completa do sistema.
  • nfs: Executa operações relacionadas ao núcleo do sistema.
  • app: Foca na criação e atualização de estruturas necessárias para o aplicativo.
  • core: Verificação de tabelas do núcleo do sistema.
  • types: Inserção de tipos de campo no banco de dados.
  • always: Modificação na estrutura de tabelas do núcleo.
  • verifyTabelaCampoFK: Verificação de campos tipo FK sem link correspondente.
  • creatTables: Verificação da existência de tabelas e criação com estrutura básica.
  • verifyTableFields: Verificação e criação de campos de acordo com o design especificado.
  • afterScript: Execução de scripts após a criação de tabelas e campos.
  • checkPremissas: Verificação de premissas de tabela e tabela_campo.
  • validateDisplayFields: Verificação de existência de campos de display no design.
  • verifySeqDbUnsigned: Verificação de colunas SEQ_DB como UNSIGNED.
  • verifyFks: Verificação de foreign keys e mudanças em campo FK_SEQ_DB para UNSIGNED.
  • criarIndicesPadraoMobileTable: Criação de índices padrão nas tabelas especificadas.
  • verifyUpdDh: Verificação de coluna UPD_DH com ação ON UPDATE.
  • processImage: Processamento de campos de imagem na tabela NFS_UPLOAD.
  • createIndexesCore: Criação de índices para o núcleo do sistema.

Exemplos de Uso


Abaixo estão alguns exemplos de como o script pode ser usado na prática:
php command/Migrate.php all all full

Verificar e criar campos em todas as tabelas de uma empresa específica:

php command/Migrate.php all empresaX verifyTableFields

Como acessar

como_acessar_test_code.gif

O test code foi criado para que os desenvolvedores possam testar os Entry Points criados no banco de dados e funcionalidades do sistema, pois anteriormente era necessário inserir os códigos e configurações no Banco de Dados e executar pelo sistema, com o Test Code é possível centralizar a execução e testes para validação dos códigos e configurações.

Códigos de Exemplo

(acesse aqui para mais informações sobre DAO)

//Obter um único registro do banco pelo SEQ_DB:
$operacao = Dao::table('EQP_OPER')->seqDb(10);

// Criar um array com a chave sendo o SEQ_DB:
$records = Dao::table('EQP')->select('SEQ_DB')->get();
$seqDbList = array_column($records, null,'SEQ_DB');
print_r($seqDbList);

Exemplo de Alerts

(acesse aqui para mais informações sobre Alerts)

$$reportsService = new ReportsService();
$reportExportService = new ReportExportService();

$html = $reportsService->getHtmlReport('test_reports',['inicio'=>'03/10/2020','fim'=>'04/10/2020']);

/** 
 * O ultimo parametro no metodo generatePdfFile com o valor "true", 
 * define que o retorno serão metadados do arquivo ('filePath' e 'fileName'), 
 * necessario para enviar o pdf anexo no alerts
*/
$pdf = $reportExportService->generatePdfFile($html,[],'report1.pdf', true);
$pdf2 = $reportExportService->generatePdfFile($html,[],'report2.pdf', true);

$imagem = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAI4AAABACAYAAADMH0/MAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QMcEzoVGrfXywAAH1RJREFUeNrtnXmcVNW1779rn1NDNzQ9MDSjSIOCIA6AxhmNMfGqMdEIRq+J0SQmN8bkY3xqBm/KTlQck2d85l7JTXxmcKATk1yNswIiCjggAg3SDDI23U1302MN55y97h/VTF2nCjB5nzyl95916uzae+3fmn5rnVPCR3i0vHVnqYd/kSIT1Ng6F/texE2uLZ0caxOptvSN/2dDPsqLb3jrp2NB7lXRswRJCzSoZZkg84jIAs9Pbxk+rTopoH1H3QecPcBZkehv0vGzLMHZih5hkAkgQxRNq9W3HCNPqvLU4Km3bBfpA08fcIAND18ZdwbpwLgbt35lzI/5Q+ISjYz2vOAUNXq2WD0eERV4STGPWDeycNhxN3b1HfkhDBzVGc6Wl4pPV9//thHJKPIegS6RqCzzXNMVGzCuUqx81ghXIEwWlY2I3qsxb07l0dWdfcd+qAJn7nR3W/rwCyz2NtChImJQ3YHIElXzZ1eCBSY6ttuUxo5StdeCnK+qrQL/Jxp1fld2zA9a+47+EHVVTX+5uiQdz0y21h4ljjlFlJMtjBboEOEpsA+OaPNqm44+diBJ+aqq/TYYH/SO9JCihw877IZk3/F/+OF8FBedSGAqjbHjK1NbG31/RVxLFyj6GiLtCmOAT4iY8e3xSFM61bLK+AOXG0ejYjhJhCOiKbvmrv+cuam6+um+lP1QAY4q4i6aMMVLBldv3to9qbOxNdqxbXtz0mtd19XmLiguj60UZLCInIHaEyXQzZ5ZWhuNj11ukXKF6YodltqefOfuh15p6oPAIQKcM5numkj3ucC3BTlXlE+pMM5Peuqmtm8+6ugJdcm0LrdqByLyCWCcE5TUDQvKVnXFnc0CRyOcANJ29y3nv1n98xf8Phgc/DAfOeDcOj8wDi+hVAv8QaBJ4fOq/O9ujd386tx5Iytbu2o9z5slVv4icAzi3LS1e31V5ZRBq0TcX2MlDTpze3NysqpKHwwOlawKhLk4i+eNK870j440vn4e5HIVHY7yR/DvPG3scRu3lkYmW5wHDTpZVO4Ybst+vnFIZUUR/BzlAkXurPTi98kpfYHyx97i9KBd5Sz8k6rXtp9xY22tl5JfBCrfU1gJfEGMe92CuqUVI6JbV6D212o1aeHireycuKHDa8LwjBg6EP1Ug9s+rA8Ghwhweo+zqms7G5aufNkN9BaQD1S5TEzks3XvbXXwnOdReVnQI9TRc8sz62JxIm+oZQ3okarOND7ipZc+4PwdY2YNQTqTXCyi/4WiiFzcmI6NbE8mmxD7rAoecMqguFvR0tbdpKLvgvQ3jnPMhrmJWB8UDgHgqCLP3D8u9vw9x/Sbm5ju7rE8G1OK/E2Q11GdAnp6LQTiOotRtooyPuj2Du8aTDdWVoJaUTtxYAn9+6DwMQeOgrx55+QxpSn3ymLfuzEab5yse7kav7KrEZgHElGfE/uvXNovTapBjNmoUOG6ZtSkSZMC45otgnQhMtinqLgPCh9z4MxLTHcyTnC2qrkVuB7RC+YlRu92NfM+2JjBBCsUbVejRxUVMcBsKU5j7QdAzAYMoaYGa20LkEK0v5VMv7445+PO4zDfqpVmRa0I/azKNCcaKdl1/dZq1DVOg6i2CTIgFonGmyfHAlXaQERE+79dVW6MtWkEX5VoIES1r1vn4w0cqcb6QbAGZSOIIyITiMSH7bIYAuqrpBGTEQVHrZkKiCMBolYh4G3AGEFBxGQcAq/P3hwCwbF11RMhBdql2FZxVNirPTTiBztFdJ5V+3I3toWnZgciLBOV5y369tRrZvsmQ4MIL6I8n7YljX3tpQc33I/agucmprvRoGGKhSpEnnSc4KF0ReX7e3/nmfTq1vNiE+4BeKZjTetZ1dhNr6desG3RJRpzW0VQVa++fmm02vpif/VMR19/zsFa/n/2AubMwKk8dsIQJ2oqfRsEnRrdcv73l+/M0yMsL94x4Yi4yP3AZAvXnjF21dMyk6DvKA8hizM3Md01kYZjxOFbavUYR403QL1XFt4z7v/C2vX0ch8LZk0us/iXA59Q0bdjjn2PmfT11BxqMU48vn2kGLlB4EuCnCAip4iR72Aj//buPcfsw608PWtyuRB8xUGuEUiD8+SmTndrX2xyCAInsIwR4XgRE939oUoJykltqdSQXR+9dtf4klL8LyP6HUUHWeUNm7EvzayuzfQd4aHnqgQXwhyNghg3JprALIqPP8yzzpdF9Fsgg0HrCGS2BoM3wKq+EzwEgaOeZUNEWaroOBGJZD/VLgyLnWSwc378qGMduEFEz5Ns22czyMOBz2tnVc8P9gru9QASAP3QAP/77s83px7g74bK7h+YBOmH2e+HB86MOQ6sLCIaKUHtIHAGYE0MxwqYACSFF3Ri3Ba66aCtO8n86n3bNFPvbzFFE+/3rXVRJiN4IrwaqDxN3F5iRL4kKicAsa4gYpd1Dl77aNOk+nc7K0/jMpPdmLGKyloeTX1AAqgnTmpAOX5mKNYvxUgM6/qI34bVemzLDmp+ngoVzDXXREgP60fKHUjEGUQmKMGNRAhsgGvbyWgD+E3U3NoFcmCHl0i4bC4uokPLifqDsFKCEu05mhSOsxMv2URHRSvPfTcDKJfdOwgJJmOD3Kq9qoertfzhlu0HBL5LZ40G/0jEMSFzdWEzK6mpbgGEL941DPxJOCKgSVQ7CLSZZHQnsTHd1MwMPnw6PmNOFPP+KJApOJyKylHAKIRyVKJZXVJFJIXSBrYeMWuBhai/CD/YSE11Zo9cMWdHxw20QWSkIwzEyFgR/bQKZyJSLoBvpfOPzePto02Toi1ePI0Y3Wvz3UhwA+q/jMSPA/spVKaBjgFKAYdsat8JrEP1JRzzBOmlG6ipyQpixn1FxLsOx4+cCvYM0CNBRoAUgxoERelCdTNiXgc7h6j3Ho9Up/LK6cpEHBs7DLUnEcjJwESEEbvXlI3nPJQWRNdhdD5iX2DshFXUrTsJqw9hpDL3sG0bqt+j3nsqRxFzzur2wUS4C8uFGNMLOBqAPol1q3n85m3MSFTgxm5G7FUgLio+wk6UrajUIrxC3L7Kw5lmqLYHDpwZMxyixw4hkLMRuQT0eJAhQBQpFGSrgngozai+jtqf8cQtb+ytsavuGl/SjDkOyxUI56oyTEQiCopq/fZMyfPfXvepkxv9/uM1F+wbEHsbasaiXAhUAfE8a7Kgbag8ie/cQ2P3OkYWDUXtBaheBBwHUo6oCyKhe1G6QRZhuJvM0pd3g28fOU0dRhD8C8KFwDSECiASPiegWNB2YAnq3IvxFJxfA4eFfLkVyzepzzxZEDhXPhwnve0ShHsQGZrze0IdwnfYkn6FIRgi8c+hdhYiY3PPjwzKVuBJ4Nc89sP33QO2MpG6U1H9JiJnAQMROcCMTCQLLi3LZnGmc9fZz01Md+PFjWOalasVLgKqBInIHi/bqfDgTzedvLrB7/eZXAupClqMmhuxejhG4vvPIqUcuBRXUwx338AGM4EzgFJk1y9L/r0I/YDpqArRaRthzprdSnBBohg3diY2+FqPnAYUVqrddt+AlKF8ChMMxpqnEYL8y8BwJjA/r7IK/k9OQNxrgcqQ641Y+SVBbAHzfxhw2W1TsPZ6DIfnOb8YQhXKt4ChXHrbD/cPnHPvj+GuORM1PwJORPgw3XI+yGvAPYxP1wK6IjExujO641Tf8nWEfwEpFdlHVFZh5ZausmdXdA05E6EsV5AiqAxGdAhGDtx6ivZD7SXgnAc6kl2B+cElFVOx/pmc+4sPeI40FySK6Rf7LMLNwET4EHISDMrRQDGipXkBrPvZ61d/Vk7S/SIweY8y7FZGD5EXkOCvPH5DkisTZWTkXxGmgDj7WV8xyjk47suFgTM94VLRdQ5qqoHjQqyMZts0sejuXMEgKrvNsuIDb2Dsj/GOfJPqmcHSn48ua03az4mYG4EjhdyDU6XFYOcktk5vs8ipQLyAth7sCQmySxM/bNVFShA9naLMn7n6riTd/hcRvRmlKuewDm5pEeAIVCV0aYpgVKivl7yxVTJ9PspFiBT3EqpFZCmWXxK8t4UZ9xWRTl6MMAMk0uPC9mchB6N6jlsQX0NjVahe24Pc3sFVGuUDRFahuh3FRyWCYSAwGtUqkFLUrsU4D5Dx3qFmZvDWnVWlXcn41QjfRBgr5Lo8VVSV2kBkXl2m9DBEJxbckKqCJEHbAB+RKGg5SPTAyQH1ENqAZI9LK0W1XwEQGFQPo8jvT7ecAFwHMoaw41YNENpRuhA1qAwAKc67p/0pg2BgaviVdPRIxF4BUpmjFSJNoL8lllnGIzUBl06pRMx40FdBQXUgyFhgRFaGea3i8PzAmZHoh9HLQc5EiPQSRBsij6LyK5K6idJMFw0VSmWL4MWLMEEZNnI8as9FWELcPMej1d7iByYMTHaZa8To9YKUqGo3IsWSy2AnEX3+jbaBH6D268CIcEGqotKB8hqG5wlYitUODANxOA/l0p5MprAbVV0F8ixqXyNjtxBxozh2KipfAk4g7xOvEgczEeNf3+OecjMX5QOsPodx5qJmI4Hn4uoxWP0imNNyZHsgIaPmQdXls8pQexXIKTmKrqQQ+ySR6J945EfZbDCe2k7Q/z6czgzdRQqmCJOuwsjVwOUgRXnAY/MDJxIfj9oLkd4uQgOQ5wmC+3ji39eF3JkCWkkkNlIXn4uJZfjNjd0v3jGhwuuSbwpcq0inqL4EjEX1+F6bVIPWWz949faNZzsgn0C0Xx7eahvobzHmv9ia2rRXliFccc9y/EwAcl3uHvYSJizAcBeNkUW8eGP3bm7k3OveZeCwLVh9AJFRIT5Ns+bduxTMSUgOuHyUpRgzi+7UXP56S9vuua956G3ad2xA+A/QqrzZVrhlFDQkMZl6TQQTfBJrvgC95KVYVN9CnV9xeFfj7s+zdMLelEIbqg38651pNDgZ4ajctakC9eGm8pqHIlk+gzEhV5NYeRnrby64wepqy6M/aOX3N3a9lRhWFHOcS63K1ap8IKo/VmShwChE3N5EhQq1Jh5f0+nERwATQoM2pROVh/Ejv+DRH2zolZoq6SXNiPMy6M78NIEuwiXBlsx8Xryxax9C7bkH0jj2bYQ1aAjRpgpKJZhPIxSHoHoDIvdRUfwMf63euc/cs7/hobHVKOtBDr66rzk0inDk2MMJ5GsoI3IOW2jCMQ9Rt2EF1ft5qaaIgr8D2ImGkZzSBbo8HDit20pQPQnCHhsRg9FKnGg5MxLRLIOc3yE/c/+4WLq47NNi7dcFXSJqbsCyEbhUkaHS615V6VbVhbPqTupE9TjEhrgaDUCXgP9bam5qCGVQa+ZYRJPh1TAAmlGZTdHAt/LyITbmYfHyCDgbYAuDQ9bXhejv6Uo/zwPfTYffHwRApgD5q3mLB8YIrdv2yG3GnQMQeyXIaTlxk2oHah9F+Rtvz/YOAJWC41YgUh4SgylQj8qicFcVL+qPb0eEmF9Ai0CuwsgwTOw9dF0Tl9/eAqYdtV342gFeG0xKJibO1AEd0aOs4SuIrjSu3hmT2PZur/s2EZ0qISZa0SajLH26Y1gx2KlgikOcRAfCC7SUbsor4KnfcAlGjcaRohBcB6gswwSvM/sbXgHNLsfokGxMIXmC1LAD1zoc+Qt/pT1/gdfEsH4pKrnhdDZQ7wYpyYmbFNnH4iQShjX+Gahcikiui4I3cOQ3/P77O+EHwow5hvJWsw/w9h47742AHg0MDSVQVVfhR/IQgH4mhjjxcEMigjAG9JpsjKBeljq33RjtwDWtEN2G1C1+bsu1K84YO//8qASNqnpv6WjzQcva5BfEcLEgYc8yWRHWqJjVmOLDCLxpmJB6msgm4DWey6PNAOPH9EfttB7h9z6ZJJhX8fz6gjWmtXYimMOQg8jZlTRqniU5bjXMtHm95BWzBqEMDQWf0ISVLRiOA3KzGyPS0/Mo1Lqjich3UKnqZfkV2IiVB3Cp5/KfHg23VcG6kXTqIJxoP8Tk7muwF8FnGkhpyK6TCAtp7NoRDpyI6caT9p4aTx6hidPDou5FMIr2kKjWwZ5XHPG2r0uVbzMaVH919Yh1C53V44wEV6nKIEIJfQ0UWbFditoI7BmIjM75fcUi+j7W3bCfOKAyS2qpk0s40wL6FjW3elAdfv/S4iKKg2kI5QdF9YhuR8xcHp+Zv1fozFsdRsSqEKkIuRqArENoAD02RPyC7QHbhTf2x3EvRvXEUBcl8hyOFBHYH4JzMkhVNnBWJ/t9G+4LsyWXkKIoTYh9h/nVfjhwkkUtRJLzUXM8QtnBle8FwBkcTZWMiHeV/HLb1JJVyYFHN4ybW3u6Bl8V9FTJx40I3cby7u2rTxM0OAkxFaHWQmURse6W/XCwx2AZF8rDCLV4kVUFK9wDgkoCPS03eC84AtBFSLCy4LdGUkygUzGUhFRR0oi+iUg8r9oaFYIBDv2csxH7Veg9j2rWalGF6AXA0H3ZcdkP7xl60QJL8fw1kK8DsOaGJCI1wAvZot7B9X84KKOiHaxLlrMyOWhwxspF4F4oqhchebiBrKPaGVjqVntlFYgJN9OwA3GWcjiZApXpGFamIpSGXM2AvEO0s7ngJqyd1EOGHcxIYuV1KkpaCs9dXIlhKmFtLaI7sLIYkc68wLYYTPdwcL4CjAtN55XhiH4SYdSHKKmEuYMkIkuI9W+GQv04mXffxxz/74itBc5BzDiUEkRj+6tpxE1AZxBlc6YET40zOJKcMtBNjyT7VlDZ5YBzjbBu6/LcbekgMhljxyE5rQCKsBrL+wXTSs+tBJ0WyhxbmnGcxWyhuwDw4nhyKqIVB1WSUG3C8g4PfDdTsMtA/BN6aAYJYcCX47jLUHs0YjVPUF6CkUtQpkMYKEQQikIjMCWNaDcqQQhoo6EBeXbUA28wtjNZGDg1NQHUrGHGfXfTr+tRUuYoHKpQqcRSnu2/sRU9fnoQyJBdRJunhg3pUlLWISqWEwdsKx0Z6yjNeg31VVmrmDWiHKeio3bnFSLr/rvz2AzCiYiUhweespige0f+oFYN62YdRaBH5DC+ikXsWoTagi0JqfhQRE/EHFShUhG2opF1BS10ZMoArP0MwuAQzUki+jKxHdtIlqWQ0Md+DJiTQMcjUnYQq9uJ6OvAQgK7Dusks8DcXbY1KGcj8jXoDTpVYDXGXb1LYffvv2tuSAJrmTFnPYPrXdItDhRFaErGiLoxxMaRyGjUXo1yMSKxjJrdohsW7eSTpRspddM9aTDvGDE/9j1qrRNcIyrXIVKqYFVlw2MN4/shMi1PnakVw5s8Xp1fo+tnxwl0CsKgEI3yQZZBpFA2ZVgTjEfkiNAAkYK0y0Zi0lGQI+GOiQinhbsPrQfzKsvGpRi/I9nTTNXb2sRRew5qig7YGKp2gMzG2P8kXbydxnaPIeP3pTX7r+9Pp38WaDQkmfAReYuxnS17twccmFSybYPhD77NmLOeyPoINjgdYeTeLmt66WYmFe8AtQHIm4r779veXj53Zg321buOmodlBlAqqt2+Sl0bZhzIpPAcVuogsrzgStsaB2LMyXmq6e3gvMHvb8zvpmoHxHDTJ6JhFqEgb6ZAIzui+S3Z52dVoHIZEtqg5SG8jHFXU1VuoTkJYeSjOEgYxZB3YQHKPALzII/9cFPer11x+yiQE0PDkGygvZjqPVbahIbUVybKuOr2wSQSB6ZxjSsFCfph9miRAMMjnZw+YDPFTgZgkzr8rKM4uWBmDUEWx06TwA5VsCJtHUF8AwEnhLKxig/6Dt5+glrHHYMyPk/9ZzP4tQVdSTRZhugJoUqlPXX7DzOuuz9GkT0fuCB8brZg7Z+zoK6hJxYJDhgcaDoPoDsQfY7y8vqCMZ1lOnBEyLosyhqiQV0vf7k32/pQhJm3TyIT+TFpcxdrIydyZaJwV10iYRgVOxwrn0O1Yo+18Tm9dDNj4q2Iaosx8qBfzLPnfXft7g2aiHapsFPQQGD9szurkiin5vrYHjelLITa/G8InZGIYu0JSFivrnqovINjNhfMQ9WZBEwIIf0UpBG0Jc+dgjCRkmQl7KVwiYThc4kymrsvRMxNwKiQtXUh8gRB5A1AmThRs7QD/oH4IZCVqK7rYYpzMz3HrGf2N/y8MguipwNXAeVhFAnCQpJ+w94fu/tMENlxKnA9KmeCdVEzmmTs93zxziXEklsZ0JJke0VW44oxpKOl1NnxiL0CkfP2jvAPi7Vz8oAtxCRIg77oZ8ycs25euc8/t3iBH7g4XlZjeP/55sNKe9xUWNa2ASdYzuM1+bXQLRkA6alAcUh804nyJqlM/hjk3PujkJyS7SrMzdVQfbunUj4wD/lxApHgW8yM/AWZtR18hzpGUGw+idVLEY4MIeoUlbcR8yg132/fa71Jsp2T+0NOE/A4mEvCGW4twnIUV9+1kI63upk4UamdJLDSwSkehPFPwsp1wLGhclfbgjWLmLhvFpoFzhWJIfjRz/X0lE7aK3CbjssxaLCZTGwDzUNbcOkEDB4lCMNQMw4Yme1NyY6YCTijdDNVsZ0qsNRa/uMMf+XWHK/gG1UHxdBlMcu2ZAZMQbQyJDiziCyiX1C4Ik+qCjXHh5YplM0E7pvU/Ch/baosNRC1p+zDiO+5vw14DvgMMDnPDBXAtThyEQRNiImgMhh0CIbicBac9zHcR7/y1btdaHW1cukdSUS9gv142aD3Dxh9DeSKUOZOZACq3yHpH4k7ZTV1miayrh82OhwTTEI5GtGhBSiW9yFY1pv+cLnijnH49t8QLgcqezGtDjAIkUHAcXulnT3GWXJoRgGGRzs5tWSLFhmvWZCHizNtb0p1rhl14hGTCTwX1ea61IBNGet+OU/DeScqC5h9azJviWDGHAfqjkd0VCjwYAXp7vUF4hvB+GNQJkMYW6xbMLoAK5XAZ3qeQw2bpj/CkSBH7iFh8zCxwhqM3IMz7AVmX+Xts2ITdIPjFY5rmIv6v8ZELTYICH9JvEHkcNCvo9brkYXBEOm5JgVSogAxb2LTDbmTBsElGPkyIpX76ZXN9hGLmGyami1O5YBBLNP6b2dkrN0DeVlN7Olp1fWhcUnGy0REiSmy9m87J4DRPJqsmzHeioIlgvSGYsR8IqfPdg9bvIRobUcBYs4AxyEMI7Rpi/fo8j7AsS+B3dTDbXzY4YNdgQ1+gvb7E49clQopZPpgbd64RnUzqrMZULmGlGnFsjFPjLN3NhZHpDjLt0nBdpged9mGyiIm5sZaBtG/AfNRuvi7HnNVBe0YGe1Yd3bphs6YCVaLI7859aal9fnmjbhuXETivpqVC1qGVoEMC82mRN6G0vqCP1/CcNCpeYTRAP6SnGeg9s0M+veko0WhASbOYqK1HWT8txBm9zCpBykvDUBbEPkTEvkeA4b8kT98tz1P1ctHxMtjCVoR+R1B2fxsW0hXC8izoAfzgqieANxuB82EZ1O6CmFlGEtv8JbX4pufovpbVOsLozYfYOhEZFlE+OU3hr7z5wnx5p2o1sRtcnGhP1HNWBtXa2lIl6xtDYqPCT00sR2gSxjX0l0ws8OfjOqIUAFgV6F2Y0Fr6mRGhvNHKKrbkeA9amoCaqo78bxHEB5CdWuPyzgAGdkOkMXAXWT8H7B63asFe4EMPuGNXj7KXJzgMWq+nf2P0ZrqDL55GuG/QTv3C2jFR3UrymMoT2QVI8fapIEleKnG0Dwkq4Vz3uNLs36MZ18COQd0UjbwlWKUGEYNuitM0wAkA3SDbAfWonYRkfjin1S92HpafPPdgqzEMY9NvWl9e0FLGNgicWXH4s4RjYGlFMMGcv7NRepwdR/yKZctHh6HpjEgjWhvrdMUIi8wqKK5APAc6twxqHqga3uvEpEFRNJ7+qtrqhu4MvEL/PhyAj0fYSrooGxFW92eN7t7PQfShPA+KgswugA3s55HCzw6vHvZkS7U24AxFfvKROoxPEhVULcPQObcvJkv3FZN3KlF9ZOIjEO1P9qT6Ij6CF3ZJzLlTVTn4motgVyOlXpEm3qBqwl4OfucfHUIcHpm5Xc0Mz3xFEOKXiGaGQqRYVgZgrEVWIoQ40IA1iRxtAXfNBKx24jHt9Pe3snjP/LOuPNXJwdqDhdHful1rdxY6KVHqsjCu00pqvVRk1oOkVlgi3N4Oy9op8RbU1jKb3vY0U8hZnGuyRcf199QsPAIFs8uJSL/C0yv+pZYPGniCHbsI9ZHqncy9ZqnOXLYAkxsDNlHgiqzHYsagG0HGvDZTH/ZSmemLdv/c4AvKyjp3kp7dBZI2b4yCdqJeKtzFElEgU3MuO9BYt1/AncMVoeClvY8GtEJug2jm+mIbSM2rptgpUM0+mdUFuR4+ECTqLMm33plv+Re/XCH1m1CR4XAWjjsaEtruaVmpt0b8XMT010Tb7zeMTrSk+Sss27auL3Q1HMT012nuPEyrJQFqa5fnVW9McVHeeyS1aaUoaRFacQy79ag50D/GWNPmygQdmZ/zyhcq8oGRQcU85hIw2BXZJT4zhNnZjY27lehhndKqg31rV19Jl/J5E2zPyojTFbyT91T4fri3znMP0bZMMY1YwNrVhdHnXfDOJveo2PbDscEps2NOOtlf49s9I3/78Y/BDgzmOhaJRpE5ZXjbnyv60DuKe9f6gSwqaxLG/qO4RAFDoDv6zo6Oj440O9vi3b7UQ221NbW9v2t4aE8NHHQIJRE4uPzR2uH2vgfFTZZy1avCWIAAAAASUVORK5CYII=";


$content = [];
$content['css'] = '';
$content['js'] = '';
$content['html'] = $html;
$content['image_base64'] = $imagem;
$additionalValues = [];
$additionalValues['CONTENT_JSON'] = json_encode($content);

ou 

$additionalValues['HTML'] = $html;
$additionalValues['IMAGE'] = $imagem;

....
$additionalValues['POSICAO_PLAT'] = -27.5527;
$additionalValues['POSICAO_PLON'] = -48.7296;
$additionalValues['SEND_EMAIL'] = 1;
$additionalValues['SEND_ALERTS'] = 0;
$additionalValues['SEND_EMAIL_PDF'] = 1;
$additionalValues['SEND_EMAIL_PDF_NAME'] = 'indicadores.pdf';
$additionalValues['SEND_EMAIL_IMAGE'] = 1;
$additionalValues['EMAIL_ATTACHMENTS'] = [$pdf,$pdf2];

$envio = Alerts::sendToUser('fabio.calegari@simova.com.br','Teste alerts '.date('d/m/Y H:i:s'), 'Teste',$additionalValues);
print_r($envio);

//sendToGroup
//sendToTopic

print_r($envio);

--> EXEMPLO COM INLINE FILES


// No template do test_reports tem: Logo com CID:<img src="cid:simova.png"> Esse nome simova.png é o criado na imagem abaixo e passado do 'file' para o array de in_line_files.

$reportsService = new ReportsService();
$html = $reportsService->getHtmlReport('test_reports',['inicio'=>'03/10/2020','fim'=>'04/10/2020']);

$exportUtils = new ExportUtils();

$pdf = $exportUtils->generatePdfFile($html,[],'report1.pdf');
$pdf2 = $exportUtils->generatePdfFile($html,[],'report2.pdf');

$imagem = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAI4AAABACAYAAADMH0/MAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QMcEzoVGrfXywAAH1RJREFUeNrtnXmcVNW1779rn1NDNzQ9MDSjSIOCIA6AxhmNMfGqMdEIRq+J0SQmN8bkY3xqBm/KTlQck2d85l7JTXxmcKATk1yNswIiCjggAg3SDDI23U1302MN55y97h/VTF2nCjB5nzyl95916uzae+3fmn5rnVPCR3i0vHVnqYd/kSIT1Ng6F/texE2uLZ0caxOptvSN/2dDPsqLb3jrp2NB7lXRswRJCzSoZZkg84jIAs9Pbxk+rTopoH1H3QecPcBZkehv0vGzLMHZih5hkAkgQxRNq9W3HCNPqvLU4Km3bBfpA08fcIAND18ZdwbpwLgbt35lzI/5Q+ISjYz2vOAUNXq2WD0eERV4STGPWDeycNhxN3b1HfkhDBzVGc6Wl4pPV9//thHJKPIegS6RqCzzXNMVGzCuUqx81ghXIEwWlY2I3qsxb07l0dWdfcd+qAJn7nR3W/rwCyz2NtChImJQ3YHIElXzZ1eCBSY6ttuUxo5StdeCnK+qrQL/Jxp1fld2zA9a+47+EHVVTX+5uiQdz0y21h4ljjlFlJMtjBboEOEpsA+OaPNqm44+diBJ+aqq/TYYH/SO9JCihw877IZk3/F/+OF8FBedSGAqjbHjK1NbG31/RVxLFyj6GiLtCmOAT4iY8e3xSFM61bLK+AOXG0ejYjhJhCOiKbvmrv+cuam6+um+lP1QAY4q4i6aMMVLBldv3to9qbOxNdqxbXtz0mtd19XmLiguj60UZLCInIHaEyXQzZ5ZWhuNj11ukXKF6YodltqefOfuh15p6oPAIQKcM5numkj3ucC3BTlXlE+pMM5Peuqmtm8+6ugJdcm0LrdqByLyCWCcE5TUDQvKVnXFnc0CRyOcANJ29y3nv1n98xf8Phgc/DAfOeDcOj8wDi+hVAv8QaBJ4fOq/O9ujd386tx5Iytbu2o9z5slVv4icAzi3LS1e31V5ZRBq0TcX2MlDTpze3NysqpKHwwOlawKhLk4i+eNK870j440vn4e5HIVHY7yR/DvPG3scRu3lkYmW5wHDTpZVO4Ybst+vnFIZUUR/BzlAkXurPTi98kpfYHyx97i9KBd5Sz8k6rXtp9xY22tl5JfBCrfU1gJfEGMe92CuqUVI6JbV6D212o1aeHireycuKHDa8LwjBg6EP1Ug9s+rA8Ghwhweo+zqms7G5aufNkN9BaQD1S5TEzks3XvbXXwnOdReVnQI9TRc8sz62JxIm+oZQ3okarOND7ipZc+4PwdY2YNQTqTXCyi/4WiiFzcmI6NbE8mmxD7rAoecMqguFvR0tbdpKLvgvQ3jnPMhrmJWB8UDgHgqCLP3D8u9vw9x/Sbm5ju7rE8G1OK/E2Q11GdAnp6LQTiOotRtooyPuj2Du8aTDdWVoJaUTtxYAn9+6DwMQeOgrx55+QxpSn3ymLfuzEab5yse7kav7KrEZgHElGfE/uvXNovTapBjNmoUOG6ZtSkSZMC45otgnQhMtinqLgPCh9z4MxLTHcyTnC2qrkVuB7RC+YlRu92NfM+2JjBBCsUbVejRxUVMcBsKU5j7QdAzAYMoaYGa20LkEK0v5VMv7445+PO4zDfqpVmRa0I/azKNCcaKdl1/dZq1DVOg6i2CTIgFonGmyfHAlXaQERE+79dVW6MtWkEX5VoIES1r1vn4w0cqcb6QbAGZSOIIyITiMSH7bIYAuqrpBGTEQVHrZkKiCMBolYh4G3AGEFBxGQcAq/P3hwCwbF11RMhBdql2FZxVNirPTTiBztFdJ5V+3I3toWnZgciLBOV5y369tRrZvsmQ4MIL6I8n7YljX3tpQc33I/agucmprvRoGGKhSpEnnSc4KF0ReX7e3/nmfTq1vNiE+4BeKZjTetZ1dhNr6desG3RJRpzW0VQVa++fmm02vpif/VMR19/zsFa/n/2AubMwKk8dsIQJ2oqfRsEnRrdcv73l+/M0yMsL94x4Yi4yP3AZAvXnjF21dMyk6DvKA8hizM3Md01kYZjxOFbavUYR403QL1XFt4z7v/C2vX0ch8LZk0us/iXA59Q0bdjjn2PmfT11BxqMU48vn2kGLlB4EuCnCAip4iR72Aj//buPcfsw608PWtyuRB8xUGuEUiD8+SmTndrX2xyCAInsIwR4XgRE939oUoJykltqdSQXR+9dtf4klL8LyP6HUUHWeUNm7EvzayuzfQd4aHnqgQXwhyNghg3JprALIqPP8yzzpdF9Fsgg0HrCGS2BoM3wKq+EzwEgaOeZUNEWaroOBGJZD/VLgyLnWSwc378qGMduEFEz5Ns22czyMOBz2tnVc8P9gru9QASAP3QAP/77s83px7g74bK7h+YBOmH2e+HB86MOQ6sLCIaKUHtIHAGYE0MxwqYACSFF3Ri3Ba66aCtO8n86n3bNFPvbzFFE+/3rXVRJiN4IrwaqDxN3F5iRL4kKicAsa4gYpd1Dl77aNOk+nc7K0/jMpPdmLGKyloeTX1AAqgnTmpAOX5mKNYvxUgM6/qI34bVemzLDmp+ngoVzDXXREgP60fKHUjEGUQmKMGNRAhsgGvbyWgD+E3U3NoFcmCHl0i4bC4uokPLifqDsFKCEu05mhSOsxMv2URHRSvPfTcDKJfdOwgJJmOD3Kq9qoertfzhlu0HBL5LZ40G/0jEMSFzdWEzK6mpbgGEL941DPxJOCKgSVQ7CLSZZHQnsTHd1MwMPnw6PmNOFPP+KJApOJyKylHAKIRyVKJZXVJFJIXSBrYeMWuBhai/CD/YSE11Zo9cMWdHxw20QWSkIwzEyFgR/bQKZyJSLoBvpfOPzePto02Toi1ePI0Y3Wvz3UhwA+q/jMSPA/spVKaBjgFKAYdsat8JrEP1JRzzBOmlG6ipyQpixn1FxLsOx4+cCvYM0CNBRoAUgxoERelCdTNiXgc7h6j3Ho9Up/LK6cpEHBs7DLUnEcjJwESEEbvXlI3nPJQWRNdhdD5iX2DshFXUrTsJqw9hpDL3sG0bqt+j3nsqRxFzzur2wUS4C8uFGNMLOBqAPol1q3n85m3MSFTgxm5G7FUgLio+wk6UrajUIrxC3L7Kw5lmqLYHDpwZMxyixw4hkLMRuQT0eJAhQBQpFGSrgngozai+jtqf8cQtb+ytsavuGl/SjDkOyxUI56oyTEQiCopq/fZMyfPfXvepkxv9/uM1F+wbEHsbasaiXAhUAfE8a7Kgbag8ie/cQ2P3OkYWDUXtBaheBBwHUo6oCyKhe1G6QRZhuJvM0pd3g28fOU0dRhD8C8KFwDSECiASPiegWNB2YAnq3IvxFJxfA4eFfLkVyzepzzxZEDhXPhwnve0ShHsQGZrze0IdwnfYkn6FIRgi8c+hdhYiY3PPjwzKVuBJ4Nc89sP33QO2MpG6U1H9JiJnAQMROcCMTCQLLi3LZnGmc9fZz01Md+PFjWOalasVLgKqBInIHi/bqfDgTzedvLrB7/eZXAupClqMmhuxejhG4vvPIqUcuBRXUwx338AGM4EzgFJk1y9L/r0I/YDpqArRaRthzprdSnBBohg3diY2+FqPnAYUVqrddt+AlKF8ChMMxpqnEYL8y8BwJjA/r7IK/k9OQNxrgcqQ641Y+SVBbAHzfxhw2W1TsPZ6DIfnOb8YQhXKt4ChXHrbD/cPnHPvj+GuORM1PwJORPgw3XI+yGvAPYxP1wK6IjExujO641Tf8nWEfwEpFdlHVFZh5ZausmdXdA05E6EsV5AiqAxGdAhGDtx6ivZD7SXgnAc6kl2B+cElFVOx/pmc+4sPeI40FySK6Rf7LMLNwET4EHISDMrRQDGipXkBrPvZ61d/Vk7S/SIweY8y7FZGD5EXkOCvPH5DkisTZWTkXxGmgDj7WV8xyjk47suFgTM94VLRdQ5qqoHjQqyMZts0sejuXMEgKrvNsuIDb2Dsj/GOfJPqmcHSn48ua03az4mYG4EjhdyDU6XFYOcktk5vs8ipQLyAth7sCQmySxM/bNVFShA9naLMn7n6riTd/hcRvRmlKuewDm5pEeAIVCV0aYpgVKivl7yxVTJ9PspFiBT3EqpFZCmWXxK8t4UZ9xWRTl6MMAMk0uPC9mchB6N6jlsQX0NjVahe24Pc3sFVGuUDRFahuh3FRyWCYSAwGtUqkFLUrsU4D5Dx3qFmZvDWnVWlXcn41QjfRBgr5Lo8VVSV2kBkXl2m9DBEJxbckKqCJEHbAB+RKGg5SPTAyQH1ENqAZI9LK0W1XwEQGFQPo8jvT7ecAFwHMoaw41YNENpRuhA1qAwAKc67p/0pg2BgaviVdPRIxF4BUpmjFSJNoL8lllnGIzUBl06pRMx40FdBQXUgyFhgRFaGea3i8PzAmZHoh9HLQc5EiPQSRBsij6LyK5K6idJMFw0VSmWL4MWLMEEZNnI8as9FWELcPMej1d7iByYMTHaZa8To9YKUqGo3IsWSy2AnEX3+jbaBH6D268CIcEGqotKB8hqG5wlYitUODANxOA/l0p5MprAbVV0F8ixqXyNjtxBxozh2KipfAk4g7xOvEgczEeNf3+OecjMX5QOsPodx5qJmI4Hn4uoxWP0imNNyZHsgIaPmQdXls8pQexXIKTmKrqQQ+ySR6J945EfZbDCe2k7Q/z6czgzdRQqmCJOuwsjVwOUgRXnAY/MDJxIfj9oLkd4uQgOQ5wmC+3ji39eF3JkCWkkkNlIXn4uJZfjNjd0v3jGhwuuSbwpcq0inqL4EjEX1+F6bVIPWWz949faNZzsgn0C0Xx7eahvobzHmv9ia2rRXliFccc9y/EwAcl3uHvYSJizAcBeNkUW8eGP3bm7k3OveZeCwLVh9AJFRIT5Ns+bduxTMSUgOuHyUpRgzi+7UXP56S9vuua956G3ad2xA+A/QqrzZVrhlFDQkMZl6TQQTfBJrvgC95KVYVN9CnV9xeFfj7s+zdMLelEIbqg38651pNDgZ4ajctakC9eGm8pqHIlk+gzEhV5NYeRnrby64wepqy6M/aOX3N3a9lRhWFHOcS63K1ap8IKo/VmShwChE3N5EhQq1Jh5f0+nERwATQoM2pROVh/Ejv+DRH2zolZoq6SXNiPMy6M78NIEuwiXBlsx8Xryxax9C7bkH0jj2bYQ1aAjRpgpKJZhPIxSHoHoDIvdRUfwMf63euc/cs7/hobHVKOtBDr66rzk0inDk2MMJ5GsoI3IOW2jCMQ9Rt2EF1ft5qaaIgr8D2ImGkZzSBbo8HDit20pQPQnCHhsRg9FKnGg5MxLRLIOc3yE/c/+4WLq47NNi7dcFXSJqbsCyEbhUkaHS615V6VbVhbPqTupE9TjEhrgaDUCXgP9bam5qCGVQa+ZYRJPh1TAAmlGZTdHAt/LyITbmYfHyCDgbYAuDQ9bXhejv6Uo/zwPfTYffHwRApgD5q3mLB8YIrdv2yG3GnQMQeyXIaTlxk2oHah9F+Rtvz/YOAJWC41YgUh4SgylQj8qicFcVL+qPb0eEmF9Ai0CuwsgwTOw9dF0Tl9/eAqYdtV342gFeG0xKJibO1AEd0aOs4SuIrjSu3hmT2PZur/s2EZ0qISZa0SajLH26Y1gx2KlgikOcRAfCC7SUbsor4KnfcAlGjcaRohBcB6gswwSvM/sbXgHNLsfokGxMIXmC1LAD1zoc+Qt/pT1/gdfEsH4pKrnhdDZQ7wYpyYmbFNnH4iQShjX+Gahcikiui4I3cOQ3/P77O+EHwow5hvJWsw/w9h47742AHg0MDSVQVVfhR/IQgH4mhjjxcEMigjAG9JpsjKBeljq33RjtwDWtEN2G1C1+bsu1K84YO//8qASNqnpv6WjzQcva5BfEcLEgYc8yWRHWqJjVmOLDCLxpmJB6msgm4DWey6PNAOPH9EfttB7h9z6ZJJhX8fz6gjWmtXYimMOQg8jZlTRqniU5bjXMtHm95BWzBqEMDQWf0ISVLRiOA3KzGyPS0/Mo1Lqjich3UKnqZfkV2IiVB3Cp5/KfHg23VcG6kXTqIJxoP8Tk7muwF8FnGkhpyK6TCAtp7NoRDpyI6caT9p4aTx6hidPDou5FMIr2kKjWwZ5XHPG2r0uVbzMaVH919Yh1C53V44wEV6nKIEIJfQ0UWbFditoI7BmIjM75fcUi+j7W3bCfOKAyS2qpk0s40wL6FjW3elAdfv/S4iKKg2kI5QdF9YhuR8xcHp+Zv1fozFsdRsSqEKkIuRqArENoAD02RPyC7QHbhTf2x3EvRvXEUBcl8hyOFBHYH4JzMkhVNnBWJ/t9G+4LsyWXkKIoTYh9h/nVfjhwkkUtRJLzUXM8QtnBle8FwBkcTZWMiHeV/HLb1JJVyYFHN4ybW3u6Bl8V9FTJx40I3cby7u2rTxM0OAkxFaHWQmURse6W/XCwx2AZF8rDCLV4kVUFK9wDgkoCPS03eC84AtBFSLCy4LdGUkygUzGUhFRR0oi+iUg8r9oaFYIBDv2csxH7Veg9j2rWalGF6AXA0H3ZcdkP7xl60QJL8fw1kK8DsOaGJCI1wAvZot7B9X84KKOiHaxLlrMyOWhwxspF4F4oqhchebiBrKPaGVjqVntlFYgJN9OwA3GWcjiZApXpGFamIpSGXM2AvEO0s7ngJqyd1EOGHcxIYuV1KkpaCs9dXIlhKmFtLaI7sLIYkc68wLYYTPdwcL4CjAtN55XhiH4SYdSHKKmEuYMkIkuI9W+GQv04mXffxxz/74itBc5BzDiUEkRj+6tpxE1AZxBlc6YET40zOJKcMtBNjyT7VlDZ5YBzjbBu6/LcbekgMhljxyE5rQCKsBrL+wXTSs+tBJ0WyhxbmnGcxWyhuwDw4nhyKqIVB1WSUG3C8g4PfDdTsMtA/BN6aAYJYcCX47jLUHs0YjVPUF6CkUtQpkMYKEQQikIjMCWNaDcqQQhoo6EBeXbUA28wtjNZGDg1NQHUrGHGfXfTr+tRUuYoHKpQqcRSnu2/sRU9fnoQyJBdRJunhg3pUlLWISqWEwdsKx0Z6yjNeg31VVmrmDWiHKeio3bnFSLr/rvz2AzCiYiUhweespige0f+oFYN62YdRaBH5DC+ikXsWoTagi0JqfhQRE/EHFShUhG2opF1BS10ZMoArP0MwuAQzUki+jKxHdtIlqWQ0Md+DJiTQMcjUnYQq9uJ6OvAQgK7Dusks8DcXbY1KGcj8jXoDTpVYDXGXb1LYffvv2tuSAJrmTFnPYPrXdItDhRFaErGiLoxxMaRyGjUXo1yMSKxjJrdohsW7eSTpRspddM9aTDvGDE/9j1qrRNcIyrXIVKqYFVlw2MN4/shMi1PnakVw5s8Xp1fo+tnxwl0CsKgEI3yQZZBpFA2ZVgTjEfkiNAAkYK0y0Zi0lGQI+GOiQinhbsPrQfzKsvGpRi/I9nTTNXb2sRRew5qig7YGKp2gMzG2P8kXbydxnaPIeP3pTX7r+9Pp38WaDQkmfAReYuxnS17twccmFSybYPhD77NmLOeyPoINjgdYeTeLmt66WYmFe8AtQHIm4r779veXj53Zg321buOmodlBlAqqt2+Sl0bZhzIpPAcVuogsrzgStsaB2LMyXmq6e3gvMHvb8zvpmoHxHDTJ6JhFqEgb6ZAIzui+S3Z52dVoHIZEtqg5SG8jHFXU1VuoTkJYeSjOEgYxZB3YQHKPALzII/9cFPer11x+yiQE0PDkGygvZjqPVbahIbUVybKuOr2wSQSB6ZxjSsFCfph9miRAMMjnZw+YDPFTgZgkzr8rKM4uWBmDUEWx06TwA5VsCJtHUF8AwEnhLKxig/6Dt5+glrHHYMyPk/9ZzP4tQVdSTRZhugJoUqlPXX7DzOuuz9GkT0fuCB8brZg7Z+zoK6hJxYJDhgcaDoPoDsQfY7y8vqCMZ1lOnBEyLosyhqiQV0vf7k32/pQhJm3TyIT+TFpcxdrIydyZaJwV10iYRgVOxwrn0O1Yo+18Tm9dDNj4q2Iaosx8qBfzLPnfXft7g2aiHapsFPQQGD9szurkiin5vrYHjelLITa/G8InZGIYu0JSFivrnqovINjNhfMQ9WZBEwIIf0UpBG0Jc+dgjCRkmQl7KVwiYThc4kymrsvRMxNwKiQtXUh8gRB5A1AmThRs7QD/oH4IZCVqK7rYYpzMz3HrGf2N/y8MguipwNXAeVhFAnCQpJ+w94fu/tMENlxKnA9KmeCdVEzmmTs93zxziXEklsZ0JJke0VW44oxpKOl1NnxiL0CkfP2jvAPi7Vz8oAtxCRIg77oZ8ycs25euc8/t3iBH7g4XlZjeP/55sNKe9xUWNa2ASdYzuM1+bXQLRkA6alAcUh804nyJqlM/hjk3PujkJyS7SrMzdVQfbunUj4wD/lxApHgW8yM/AWZtR18hzpGUGw+idVLEY4MIeoUlbcR8yg132/fa71Jsp2T+0NOE/A4mEvCGW4twnIUV9+1kI63upk4UamdJLDSwSkehPFPwsp1wLGhclfbgjWLmLhvFpoFzhWJIfjRz/X0lE7aK3CbjssxaLCZTGwDzUNbcOkEDB4lCMNQMw4Yme1NyY6YCTijdDNVsZ0qsNRa/uMMf+XWHK/gG1UHxdBlMcu2ZAZMQbQyJDiziCyiX1C4Ik+qCjXHh5YplM0E7pvU/Ch/baosNRC1p+zDiO+5vw14DvgMMDnPDBXAtThyEQRNiImgMhh0CIbicBac9zHcR7/y1btdaHW1cukdSUS9gv142aD3Dxh9DeSKUOZOZACq3yHpH4k7ZTV1miayrh82OhwTTEI5GtGhBSiW9yFY1pv+cLnijnH49t8QLgcqezGtDjAIkUHAcXulnT3GWXJoRgGGRzs5tWSLFhmvWZCHizNtb0p1rhl14hGTCTwX1ea61IBNGet+OU/DeScqC5h9azJviWDGHAfqjkd0VCjwYAXp7vUF4hvB+GNQJkMYW6xbMLoAK5XAZ3qeQw2bpj/CkSBH7iFh8zCxwhqM3IMz7AVmX+Xts2ITdIPjFY5rmIv6v8ZELTYICH9JvEHkcNCvo9brkYXBEOm5JgVSogAxb2LTDbmTBsElGPkyIpX76ZXN9hGLmGyami1O5YBBLNP6b2dkrN0DeVlN7Olp1fWhcUnGy0REiSmy9m87J4DRPJqsmzHeioIlgvSGYsR8IqfPdg9bvIRobUcBYs4AxyEMI7Rpi/fo8j7AsS+B3dTDbXzY4YNdgQ1+gvb7E49clQopZPpgbd64RnUzqrMZULmGlGnFsjFPjLN3NhZHpDjLt0nBdpged9mGyiIm5sZaBtG/AfNRuvi7HnNVBe0YGe1Yd3bphs6YCVaLI7859aal9fnmjbhuXETivpqVC1qGVoEMC82mRN6G0vqCP1/CcNCpeYTRAP6SnGeg9s0M+veko0WhASbOYqK1HWT8txBm9zCpBykvDUBbEPkTEvkeA4b8kT98tz1P1ctHxMtjCVoR+R1B2fxsW0hXC8izoAfzgqieANxuB82EZ1O6CmFlGEtv8JbX4pufovpbVOsLozYfYOhEZFlE+OU3hr7z5wnx5p2o1sRtcnGhP1HNWBtXa2lIl6xtDYqPCT00sR2gSxjX0l0ws8OfjOqIUAFgV6F2Y0Fr6mRGhvNHKKrbkeA9amoCaqo78bxHEB5CdWuPyzgAGdkOkMXAXWT8H7B63asFe4EMPuGNXj7KXJzgMWq+nf2P0ZrqDL55GuG/QTv3C2jFR3UrymMoT2QVI8fapIEleKnG0Dwkq4Vz3uNLs36MZ18COQd0UjbwlWKUGEYNuitM0wAkA3SDbAfWonYRkfjin1S92HpafPPdgqzEMY9NvWl9e0FLGNgicWXH4s4RjYGlFMMGcv7NRepwdR/yKZctHh6HpjEgjWhvrdMUIi8wqKK5APAc6twxqHqga3uvEpEFRNJ7+qtrqhu4MvEL/PhyAj0fYSrooGxFW92eN7t7PQfShPA+KgswugA3s55HCzw6vHvZkS7U24AxFfvKROoxPEhVULcPQObcvJkv3FZN3KlF9ZOIjEO1P9qT6Ij6CF3ZJzLlTVTn4motgVyOlXpEm3qBqwl4OfucfHUIcHpm5Xc0Mz3xFEOKXiGaGQqRYVgZgrEVWIoQ40IA1iRxtAXfNBKx24jHt9Pe3snjP/LOuPNXJwdqDhdHful1rdxY6KVHqsjCu00pqvVRk1oOkVlgi3N4Oy9op8RbU1jKb3vY0U8hZnGuyRcf199QsPAIFs8uJSL/C0yv+pZYPGniCHbsI9ZHqncy9ZqnOXLYAkxsDNlHgiqzHYsagG0HGvDZTH/ZSmemLdv/c4AvKyjp3kp7dBZI2b4yCdqJeKtzFElEgU3MuO9BYt1/AncMVoeClvY8GtEJug2jm+mIbSM2rptgpUM0+mdUFuR4+ECTqLMm33plv+Re/XCH1m1CR4XAWjjsaEtruaVmpt0b8XMT010Tb7zeMTrSk+Sss27auL3Q1HMT012nuPEyrJQFqa5fnVW9McVHeeyS1aaUoaRFacQy79ag50D/GWNPmygQdmZ/zyhcq8oGRQcU85hIw2BXZJT4zhNnZjY27lehhndKqg31rV19Jl/J5E2zPyojTFbyT91T4fri3znMP0bZMMY1YwNrVhdHnXfDOJveo2PbDscEps2NOOtlf49s9I3/78Y/BDgzmOhaJRpE5ZXjbnyv60DuKe9f6gSwqaxLG/qO4RAFDoDv6zo6Oj440O9vi3b7UQ221NbW9v2t4aE8NHHQIJRE4uPzR2uH2vgfFTZZy1avCWIAAAAASUVORK5CYII=";

$imagenDentro = ImageService::createImageFileFromBase64($imagem,'simova.png');


$additionalValues['HTML'] = $html;
//$additionalValues['IMAGE'] = $imagem;

$additionalValues['SEND_EMAIL'] = 1;
$additionalValues['SEND_ALERTS'] = 0;
$additionalValues['SEND_EMAIL_PDF'] = 0;
$additionalValues['SEND_EMAIL_PDF_NAME'] = 'indicadores.pdf';
$additionalValues['SEND_EMAIL_IMAGE'] = 0;
$additionalValues['EMAIL_ATTACHMENTS'] = [$pdf,$pdf2];
$additionalValues['EMAIL_INLINE_FILES'] = [$imagenDentro['file']];

$envio = Alerts::sendToUser('fabio.calegari@simova.com.br','Teste alerts '.date('d/m/Y H:i:s'), 'Teste',$additionalValues);
print_r($envio);

Métodos da classe DateUtils

echo DateUtils::formatFromTable('2020-02-01 12:34:45'); // converte um campo de formato do banco de dados para BR com hora/minuto/segundo
echo DateUtils::formatFromTable('2020-02-01 12:34:45','d/m/Y'); // converte um campo de formato do banco de dados para BR somente d/m/y
echo DateUtils::formatFromTable('2020-02-01 12:34:45','H:i:s'); // converte um campo de formato do banco de dados para somente hora/minuto/segundo
echo DateUtils::formatFromTable('2020-02-01 12:34:45','H'); // converte um campo de formato do banco de dados para somente hora

var_dump(DateUtils::getDate());     // dia atual formato BR
var_dump(DateUtils::getDate(0,'BR'));     // dia atual formato BR (padrão)
var_dump(DateUtils::getDate(0,'DB'));     // dia atual formato Banco de dados
var_dump(DateUtils::getDate(0,'Y-m-d'));     // dia atual formato Banco de dados

var_dump(DateUtils::getDate(-1));   // dia anterior
var_dump(DateUtils::getDate(+1));   // amanhã
var_dump(DateUtils::getDate(-7));   // 7 dias atrás
var_dump(DateUtils::getDate(+7));   // 7 dias à frente

var_dump(DateUtils::getDateTime());     // dia atual
var_dump(DateUtils::getDateTime(-1));   // dia anterior

var_dump(DateUtils::getDateBetween());
var_dump(DateUtils::getDateBetween(-1));
var_dump(DateUtils::getDateBetween(+1,-27));
var_dump(DateUtils::getDateBetween(-7));
var_dump(DateUtils::getDateBetween(+7));

var_dump(DateUtils::getPeriodOfDay()); // somente o dia atual

// abordagem moderna:
list($inicioDb, $fimFimDb) = DateUtils::getDateBetween(-1,0,'DB');

// abordagem padrao para recuperar os valores da função:
$periodo = DateUtils::getDateBetween(-1,0,'DB',true);
$inicioDb = $periodo['INI'];
$fimFimDb = $periodo['FIM'];

Para simular uma request do xMova

$xmovaService = new XmovaService();


 Para consultar os dados que são montados diretamente em uma tabela (não considerando entryPoint):

$tableDS = 'EQP';
$sqlWhere = 'LIBERADO = 1'; //condição WHERE da consulta na tabela
 Atencao, para ordenar a lista ao XMova diferente do CRUD configurar um JSON no campo nfs_core_ds_tabela.ORDER_BY (ver documentacao) e VALIDAR O JSON ANTES!
$_recordsToMobileXMOVA = $xmovaService->getTableData($tableDS, $sqlWhere);
print_r($_recordsToMobileXMOVA);


 Se é um nome de entryPoint usar esse método abaixo:

$_typeRequest = 'Equipamento_Mobile'; // nome do entryPoint
$_objNFSJSONRequest
$method = 'LIST'; // ou pode ser GET (para validação de campos e retorno com json especificado
$_recordsToMobileXMOVA = $xmovaService->processEntryPointxMova($_typeRequest, $_objNFSJSONRequest, $method);
print_r($_recordsToMobileXMOVA);


Um recurso bem interessante é usar dois entryPoint com o mesmo nome mas com os tipos SQL e PHP
o Tipo SQL será executado primeiro e com isso no PHP você pode recuperar os dados de resultado do entryPoint PHP 
usando a variável $_recordsToMobileXMOVA e adicionando valores para ela, teste criando esses dois entryPoints e na versão PHP coloque:

foreach ($_recordsToMobileXMOVA as $row) {
	$row['DISPLAY_TEXT'] = $row['CODIGO'].PHP_EOL.$row['DESCRICAO'];
}

Métodos para consulta

$equipamentos = Dao::table('eqp')
			->groupBy('SEQ_DB')
			->toArray()
			->get();


// $lista = Dao::table('EQP')->get();                        
// var_dump($lista);                     
// $clientesList = Dao::table('FUNCIONARIO')
//                         ->select(['SEQ_DB','POSICAO_PLAT','POSICAO_PLON','POSICAO_DH','DESCRICAO','CODIGO','UPD_DH'])
//                         ->whereIsNotNull('POSICAO_PLAT')
//                         ->whereIsNotNull('POSICAO_PLON')
//                         ->limit(9999999)
//                         ->get();

// var_dump($clientesList);

/*
$tabela = xDS::getTabela('EQP');
$tabela = xDS::getTabela('FUNCIONARIO');
foreach ($tabela->CAMPOS as $campo) {
	echo $campo->NOME, " ", $campo->TIPO,"\n";
}
*/

// var_dump(\core\TabelaBO::getAllDataToArray("FUNCIONARIO","SEQ_DB"));

/**
* Usando o NfsQueryBuilder:
* Abordagem IDEAL que contém tratamento se parametro for null
*/
$inicioDb = '2020-09-17';
$fimFimDb = '2020-09-17 23:59:59';
$result = NFSQueryBuilder::select('*')
	->from('eqp_apt', 'apt')
	->where('apt.INI_DH between :inicio and :fim')
	->setParameter('inicio', $inicioDb)
	->setParameter('fim', $fimFimDb)
	->get();

var_dump($result);

// OU assim:
$result = NFSQueryBuilder::select('*')
	->from('eqp_apt', 'apt')
	->where("apt.INI_DH between '{$inicioDb}' and '{$fimFimDb}'")
	->get();

// OU assim:
$where = "apt.INI_DH between '".$inicioDb."' and '".$fimFimDb."'";
$result = NFSQueryBuilder::select('*')
	->from('eqp_apt', 'apt')
	->where($where)
	->get();


Testando o JSON de um NfsQueryBuilder (configuração de gráfico ou relatório):
 Pode ser JSON ou pode ser um Array

$json = '{	"equipamentos": {
		"select": ["eqp.SEQ_DB","eqp.EQP_CLASSE_SEQ_DB",
			"eqp.CODIGO",
			"eqp.DESCRICAO"
		],
		"from": "eqp eqp",
		"group_result_by_unique": "SEQ_DB",
		"order_by": [
			["eqp.SEQ_DB", "ASC"]
		]
	}
}';
$array = json_decode($json, true);
var_dump(\core\NFSQueryBuilder::getQuery($array));

Debugando o NfsQueryBuilder

$_SESSION['DEBUG_MODE'] = 1;
print_r($_SESSION['DEBUG_CONFIG']);
print_r($_SESSION['DEBUG_PARAMS']);
print_r($_SESSION['DEBUG_SQL']);

/**
* Debugando UM gráfico do Dashboard
* Crie um painel de teste e defina somente UM gráfico para esse painel
*/
$_SESSION['DEBUG_MODE'] = 1;
print_r($_SESSION['DEBUG_CONFIG']);
print_r($_SESSION['DEBUG_FILTERS_VALUES']);
print_r($_SESSION['DEBUG_FILTERS_CONFIG']);
print_r($_SESSION['DEBUG_FILTERS_AND_VALUES']);
print_r($_SESSION['DEBUG_DATA']);


// Alerts::sendToGroup('GRUPO_TESTE','Titulo','Texto');
// Alerts::sendToUser('email@xxxx.com.br','Titulo','Texto');
// Alerts::sendToTopic('TOPICO_TESTE','Titulo','Texto');
// Alerts::sendToUsers(['email@xxxx.com.br','email2222@xxxx.com.br'],'Titulo','Texto');  // AINDA NAO FUNCIONA

$destinatario = 'fabio.calegari@simova.com.br'; // usuário ativo/cadastrado (tabela alerts_users)
$titulo = 'Titulo do Alerta (até 50 caracteres) '; // o core corta automaticamente e pega somente os primeiros 50 bytes
$texto = 'Texto do Alerta (até 1000 caracteres)'; // o core corta automaticamente e pega somente os primeiros 1000 bytes
$retornoDoEnvio = Alerts::sendToUser($destinatario, $titulo, $texto);

print_r($retornoDoEnvio);

Upload de Base64 - UploadService

$seqDbUpload = UploadService::uploadBase64("data:image/jpeg;base64,".$myBase64Url);

print_r($seqDbUpload);

Login

Acesso por Token

Essa funcionalidade tem como finalidade permitir o acesso a rotas especificas do sistema sem que o Usuário precise efetuar o login.

Para isso um usuário cadastrado no sistema, precisara de permissão para gerar um token o qual será vinculado a esse usuário; o usuário poderá visualizar as informações sobre seus tokens em seu perfil na plataforma web.

Configurando o ambiente

Para que possamos utilizar o acesso por token, precisamos habilitar a funcionalidade no ambiente, isso pode ser feito seguindo esses passos:

[!WARNING] Um problema comum que pode acontecer é que ao criar as tabelas executando o create DS/DDL, uma restrição do campo ID pode gerar um erro não permitindo geração dos tokens, para resolver, basta executar o create DS/DDL novamente(isto ira remover as restrições desse campo).

1. Parâmetro

Precisamos adicionar o parâmetro "ACESSO_POR_TOKEN" na tabela nfs_core_par_parametros, e configurarmos nesse registro(coluna CONTEUDO) as rotas nas quais iremos permitir que o usuário possa gerar um token de acesso:

1.1 SQL:

INSERT INTO nfs_core_par_parametros (EMPRESA, FILIAL, `LOCAL`, NOME, CONTEUDO, TIPO)
VALUES(9999, 9999, 9999, 'ACESSO_POR_TOKEN', '{}', 1);

1.2 Valor coluna CONTEUDO:

{
  "Minha descrição PowerBI": {
    "route": "/nfs/api/powerbi/v1/"
  },
  "Minha descrição acesso rota WEB": {
    "route": "/custom/panel/Funcionario"
  }
}

1.2.1 Exemplo:

{
  "Tabela: Ultimos 1000 apontamentos pendencia": {
    "route": "/nfs/api/powerbi/v1/t/apontamento_pendencia"
  },
  "EntryPoint: Ultimas 100 pendencias": {
    "route": "/nfs/api/powerbi/v1/aptPendenciaUltimas100"
  },
  "Painel de agendamento técnico": {
    "route": "/custom/panel/Funcionario"
  }
}

[!WARNING] Após essa parte, executar o create DS/DDL para criar as tabelas de acesso por token, pode ser que seja necessario executar esse comando 2 vezes para que na segunda ele remova a restrições do campo ID(o qual pode impedir a criação do token).

2. Permissão para o Usuário

Agora será necessário através do acl, atribuirmos a permissão de acesso por token ao usuário que será responsável por gerar o token. acl.png

Feito isso, o ambiente já estará preparado para trabalhar com token de acesso.

Acesso Web

Aqui veremos como configurar o ambiente para permitir que o usuário acesse alguma pagina especifica do sistema através do token de acesso.

Para isso, iremos utilizar o Painel de Agendamento técnico como exemplo:

1. Configuração do parâmetro:

No parâmetro ACESSO_POR_TOKEN(core_par_parametros), precisamos adicionar a rota para o painel e adicionar um LABEL para ela, isso será feito na coluna CONTEUDO, neste caso irei inserir no formato dos exemplos anteriores:

{
  "Painel de agendamento técnico": {
    "route": "/custom/panel/Funcionario"
  }
}

2. Gerar Token

Após logarmos com o nosso usuário, o qual possui permissão para criação do token de acesso, iremos acessar o perfil onde poderemos gerar o nosso token, uma área "Acesso por token" vai estar disponível na parte inferior da pagina.

Como o nosso usuário não possui tokens ainda, poderemos ver os botões para Gerar o Token e Recarregar a Lista de Tokens respectivamente.

Ao clicarmos no botão "Gerar token de acesso" um modal irá surgir na tela, nele poderemos definir:

  • Quais as Empresas, Filias e Locais esse token irá permitir o acesso;
  • Qual a rota será atribuída ao token;
  • Por quanto tempo o token será valido(de 1 a 365 dias);
  • Caso o usuário seja corporativo, se o token irá permitir o acesso a múltiplas EFLs simultaneamente.

Demonstração gerando token

Conteúdo do e-mail de ativação: email.png

Para validar o token o usuário precisa estar conectado a plataforma web

validated.png

3. Utilizando o Token

Vamos precisar do link de acesso para a rota especifica, para isso vamos acessar novamente o perfil do usuário e lá, poderemos ver que o token validado já esta disponível para uso:

Demonstração acessando web

Pronto! Neste caso utilizei o chrome no modo anonimo pois eu já estava logado com usuário na sessão normal, se um usuário não estiver logado ele poderá acessar a essa rota vinculada ao token sem que o mesmo precise "logar".

A classe Usuario foi projetada para representar os usuários que interagem com a plataforma. Ela encapsula informações e comportamentos relacionados aos indivíduos que utilizam o sistema, oferecendo recursos e funcionalidades personalizadas para atender às suas necessidades.

Usuário Terceiro (Third Party)

No NFS tem a possíbilidade de vincular um usuário a uma tabela app, onde chamamos ela de terceiro, com isso o cliente tem a possibilidade liberar um usuário externo para consultar relatórios e paineis.

Com isso nas funcionalidade que usam o Nfs Query Builder vai ter o parâmetro :NFS_THIRD_PARTY que vai conter o SEQ_DB da tabela de Terceiro

Configuração

A primeira coisa a ser realizada é ativar o parâmetros

THIRD_PARTY_ENABLED

com valor igual a 1.

Controle de Acesso - Usuário

No Controle de Acesso a o criar um novo usuário vai ter a opção Terceiro. Todas os usuário marcados com esse flag serão exibidos para serem vínculados a um terceiro.

flag_terceiro_usuario.png

Sempre lembre-se de vincular o usuário a uma empresa se não você não vai conseguir acessar o sistema. {.is-danger}

INSERT INTO nfs_acl_usuario_empresa
(SEQ_DB, EMPRESA, FILIAL, `LOCAL`, USUARIO_SEQ_DB, EMPRESA_SEQ_DB, INS_DH, INS_USUARIO_SEQ_DB, ATIVO, DELETED, `OPTIONS`)
VALUES(NULL, 1, 1, 1, 141, 1, '2020-05-27 12:20:32.0', 100, 1, 0, '{"theme":"default"}');
INSERT INTO nfs_acl_usuario_empresa
(SEQ_DB, EMPRESA, FILIAL, `LOCAL`, USUARIO_SEQ_DB, EMPRESA_SEQ_DB, INS_DH, INS_USUARIO_SEQ_DB, ATIVO, DELETED, `OPTIONS`)
VALUES(NULL, 1, 1, 2, 141, 1, '2020-05-27 12:20:32.0', 100, 1, 0, '{"theme":"default"}');
INSERT INTO nfs_acl_usuario_empresa
(SEQ_DB, EMPRESA, FILIAL, `LOCAL`, USUARIO_SEQ_DB, EMPRESA_SEQ_DB, INS_DH, INS_USUARIO_SEQ_DB, ATIVO, DELETED, `OPTIONS`)
VALUES(NULL, 1, 1, 3, 141, 1, '2020-05-27 12:20:32.0', 100, 1, 0, '{"theme":"default"}');
INSERT INTO nfs_acl_usuario_empresa
(SEQ_DB, EMPRESA, FILIAL, `LOCAL`, USUARIO_SEQ_DB, EMPRESA_SEQ_DB, INS_DH, INS_USUARIO_SEQ_DB, ATIVO, DELETED, `OPTIONS`)
VALUES(NULL, 1, 1, 4, 141, 1, '2020-05-27 12:20:32.0', 100, 1, 0, '{"theme":"default"}');

Controle de Acesso - Grupo

Crie um Grupo e depois vincule esse usuário a esse grupo.

Insert para cria um grupo:

INSERT INTO nfs_acl_grupo
(SEQ_DB, EMPRESA, FILIAL, `LOCAL`, NOME, RESTRICTED, INS_DH, ATIVO, DELETED, HOMEPAGE)
VALUES(NULL, 1, 1, 1, 'GRUPO TERCEIRO', 0, '2015-11-04 13:48:20.0', 1, 0, NULL);

CRUD - Tabela de Relacionamento como Usuário

Nesse caso vou adotar que o seu sistema tenha uma tabela chamada app_terceiro, vamos fazer um vínculo entre o terceiro e o usuário, exemplo:

INSERT INTO nfs_core_ds_tabela
(NOME, TABELA_REAL, DESCRICAO, UNQ, DISPLAY, TIPO, MOBILE_TABLE, MOBILE_MESSAGE, MOBILE_MESSAGE_FK, FILTRO_USER, MAX_ROWS_WO_FILTER, `OPTIONS`, ORDER_BY, INDICATORS)
VALUES('TERCEIRO_USUARIO', 'APP_TERCEIRO_USUARIO', 'Terceiro x Usuário', NULL, 'ID', 1, 0, 0, NULL, 0, 100, NULL, NULL, NULL);

INSERT INTO nfs_core_ds_tabela_campo
(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('TERCEIRO_USUARIO', 'TERCEIRO', 2, 0, 1, 1, 1, 'Terceiro', 'Terceiro', 'FK', NULL, NULL, NULL, NULL, 'IU', '0', NULL, 'TERCEIRO', NULL, NULL, NULL, NULL, NULL, NULL, NULL);

INSERT INTO nfs_core_ds_tabela_campo
(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('TERCEIRO_USUARIO', 'USUARIO', 2, 0, 1, 1, 1, 'Usuário', 'Usuário', 'USER', NULL, NULL, NULL, NULL, 'IU', '0', NULL, 'NFS_ACL_USUARIO', NULL, NULL, NULL, NULL, NULL, NULL, NULL);

Ponto a observar:

  • Ao criar o registro na nfs_core_ds_tabela_campo o tipo deve ser USER e o LINK é NFS_ACL_USUARIO.

Essa tabela NFS_ACL_USUARIO meio que cria uma copia da nfs_acl_usuario para usar algumas informações, como o SEQ_DB, nome, sobrenome, email.

O resultado vai ser:

crud_usuario_sistema.png

No caso já adicionei o usuário que eu criei a um terceiro.

Ao realizar a associação e salvar, quando o usuário Dalton logar no sistema ele vai ter o SEQ_DB do terceiro na imagem no seu campo, recomendo por enquanto usar em relatório, painels entre outros componentes que usam Nfs Query Builder

Como usar

Sugiro que você dê acesso para esse grupo somente o necessário que ele vai usar, por exemplo, no caso somente dei permissão para acessar o menu relatórios, submenus de terceiros, assim como o relatório de terceiro.

Insert do grupo permissão e rota. (Sugiro que olhe no menu o seq_db os seus itens a serem dispobilizados)

INSERT INTO nfs_acl_grupo_permissao
(SEQ_DB, EMPRESA, FILIAL, `LOCAL`, MENU_SEQ_DB, TABELA_NOME, SIUD, GRUPO_SEQ_DB, CAMPOS_PERMITIDOS, INS_DH, ATIVO, DELETED)
VALUES(NULL, 1, 9999, 9999, 600, NULL, '1000', 8, '', '2017-08-15 12:45:03.0', 1, 0);
INSERT INTO nfs_acl_grupo_permissao
(SEQ_DB, EMPRESA, FILIAL, `LOCAL`, MENU_SEQ_DB, TABELA_NOME, SIUD, GRUPO_SEQ_DB, CAMPOS_PERMITIDOS, INS_DH, ATIVO, DELETED)
VALUES(NULL, 1, 9999, 9999, 601, NULL, '1000', 8, '', '2017-08-15 12:45:03.0', 1, 0);

INSERT INTO nfs_acl_grupo_permissao
(SEQ_DB, EMPRESA, FILIAL, `LOCAL`, MENU_SEQ_DB, TABELA_NOME, SIUD, GRUPO_SEQ_DB, CAMPOS_PERMITIDOS, INS_DH, ATIVO, DELETED)
VALUES(NULL, 1, 9999, 9999, 6600, NULL, '1000', 8, '', '2017-08-15 12:45:03.0', 1, 0);
INSERT INTO nfs_acl_grupo_permissao
(SEQ_DB, EMPRESA, FILIAL, `LOCAL`, MENU_SEQ_DB, TABELA_NOME, SIUD, GRUPO_SEQ_DB, CAMPOS_PERMITIDOS, INS_DH, ATIVO, DELETED)
VALUES(NULL, 1, 9999, 9999, 6601, NULL, '1000', 8, '', '2017-08-15 12:45:03.0', 1, 0);
INSERT INTO nfs_acl_grupo_permissao
(SEQ_DB, EMPRESA, FILIAL, `LOCAL`, MENU_SEQ_DB, TABELA_NOME, SIUD, GRUPO_SEQ_DB, CAMPOS_PERMITIDOS, INS_DH, ATIVO, DELETED)
VALUES(NULL, 1, 9999, 9999, 6602, NULL, '1000', 8, '', '2017-08-15 12:45:03.0', 1, 0);

INSERT INTO nfs_acl_grupo_permissao_rota
(SEQ_DB, EMPRESA, FILIAL, `LOCAL`, TIPO, ROTA, GRUPO_SEQ_DB, INS_DH, ATIVO, DELETED)
VALUES(NULL, 1, 1, 1, 'ALLOW', '/acl/*', 1, '2019-03-27 14:16:01.0', 1, 0);
INSERT INTO nfs_acl_grupo_permissao_rota
(SEQ_DB, EMPRESA, FILIAL, `LOCAL`, TIPO, ROTA, GRUPO_SEQ_DB, INS_DH, ATIVO, DELETED)
VALUES(NULL, 1, 1, 2, 'ALLOW', '/acl/*', 2, '2018-02-06 12:27:07.0', 1, 0);

Relatório - NFS_THIRD_PARTY

Para ser usado no relatório no WHERE do NFS Query Builder adicione o parâmetro :NFS_THIRD_PARTY.

Exemplo:

Relatório de Resumo do Terceiro no modelo Excel

UPDATE nfs_reports set enabled = 0 where DISPLAY_NAME ='relatorio_resumo_terceiro';

INSERT INTO nfs_reports (SEQ_DB, NAME, DISPLAY_NAME, FILTERS, QUERIES, TEMPLATE, ENABLED, EXPORT_OPTIONS) VALUES(NULL, 'relatorio_resumo_terceiro', 'Resumo dos Terceiros(.pdf)', '{
	"inicio": {
		"type": "Date",
		"options": {
			"label": "De",
			"required": true
		}
	},
	"fim": {
		"type": "Date",
		"options": {
			"label": "Até",
			"required": true
		}
	},
	"terceiro": {
		"type": "Table",
		"options": {
			"label": "Terceiro",
			"multiple": false,
			"required": false
		}
	}
}
', '{
	"filial": {
		"dump": "",
		"main": true,
		"select": [
			"f.LOCAL",
			"f.DESCRICAO",
			"f.ENDERECO",
			"f.CEP",
			"f.ESTADO",
			"f.TELEFONE",
			"f.FAX",
			"f.SITE",
			"concat(DATE_FORMAT(STR_TO_DATE(:inicio, ''%d/%m/%Y''), ''%d/%m/%Y''), '' - '', DATE_FORMAT(STR_TO_DATE(:fim, ''%d/%m/%Y''), ''%d/%m/%Y'')) PERIODO"
		],
		"from": "endereco_filial f"
	},
	"total": {
		"dump": "",
		"main": true,
		"select": [
			"SUM(SEC_TO_TIME(TIMESTAMPDIFF(second,a.INI_DH,a.FIM_DH))) HORAS_ATIVIDADE",
			"SUM(a.PRODUCAO) PRODUCAO"
		],
		"from": "apontamento_mo_terceiro a",
		"inner_join": [
			["a", "boletim_mo", "b", "b.SEQ_DB = a.SEQ_DB_DEVICE_MASTER_SEQ_DB"],
			["a", "terceiro", "t", "t.SEQ_DB = a.TERCEIRO_SEQ_DB"],
			["a", "divisao_6", "d6", "d6.SEQ_DB = a.DIVISAO_6_SEQ_DB"],
			["a", "divisao_5", "d5", "d5.SEQ_DB = a.DIVISAO_5_SEQ_DB"],
			["a", "divisao_4", "d4", "d4.SEQ_DB = a.DIVISAO_4_SEQ_DB"],
			["a", "divisao_3", "d3", "d3.SEQ_DB = a.DIVISAO_3_SEQ_DB"],
			["a", "divisao_2", "d2", "d2.SEQ_DB = a.DIVISAO_2_SEQ_DB"],
			["a", "divisao_1", "d1", "d1.SEQ_DB = a.DIVISAO_1_SEQ_DB"]
		],
		"where": [
			"DATE(a.INI_DH) >= DATE_FORMAT(STR_TO_DATE(:inicio, ''%d/%m/%Y''), ''%Y-%m-%d'')",
			"DATE(a.INI_DH) <= DATE_FORMAT(STR_TO_DATE(:fim, ''%d/%m/%Y''), ''%Y-%m-%d'')",
			"a.TERCEIRO_SEQ_DB in (:NFS_THIRD_PARTY)",
			"a.PRODUCAO IS NOT NULL"
		],
		"having": ["HORAS_ATIVIDADE > 0"]
	},	
	"por_nivel": {
		"dump": "",
		"main": true,
		"select": [
			"a.DIVISAO_5_SEQ_DB",
			"CONCAT(d1.CODIGO, '' - '', d1.DESCRICAO) NIVEL_1",
			"CONCAT(d2.CODIGO, '' - '', d2.DESCRICAO) NIVEL_2",
			"CONCAT(d3.CODIGO, '' - '', d3.DESCRICAO) NIVEL_3",
			"CONCAT(d4.CODIGO, '' - '', d4.DESCRICAO) NIVEL_4",
			"CONCAT(d5.CODIGO, '' - '', d5.DESCRICAO) NIVEL_5",
			"SUM(SEC_TO_TIME(TIMESTAMPDIFF(second,a.INI_DH,a.FIM_DH))) HORAS_ATIVIDADE",
			"SUM(a.PRODUCAO) PRODUCAO"
		],
		"from": "apontamento_mo_terceiro a",
		"inner_join": [
			["a", "boletim_mo", "b", "b.SEQ_DB = a.SEQ_DB_DEVICE_MASTER_SEQ_DB"],
			["a", "terceiro", "t", "t.SEQ_DB = a.TERCEIRO_SEQ_DB"],
			["a", "divisao_6", "d6", "d6.SEQ_DB = a.DIVISAO_6_SEQ_DB"],
			["a", "divisao_5", "d5", "d5.SEQ_DB = a.DIVISAO_5_SEQ_DB"],
			["a", "divisao_4", "d4", "d4.SEQ_DB = a.DIVISAO_4_SEQ_DB"],
			["a", "divisao_3", "d3", "d3.SEQ_DB = a.DIVISAO_3_SEQ_DB"],
			["a", "divisao_2", "d2", "d2.SEQ_DB = a.DIVISAO_2_SEQ_DB"],
			["a", "divisao_1", "d1", "d1.SEQ_DB = a.DIVISAO_1_SEQ_DB"]			
		],
		"where": [
			"DATE(a.INI_DH) >= DATE_FORMAT(STR_TO_DATE(:inicio, ''%d/%m/%Y''), ''%Y-%m-%d'')",
			"DATE(a.INI_DH) <= DATE_FORMAT(STR_TO_DATE(:fim, ''%d/%m/%Y''), ''%Y-%m-%d'')",
			"a.TERCEIRO_SEQ_DB in (:NFS_THIRD_PARTY)",
			"a.PRODUCAO IS NOT NULL"
		],		
		"group_by":[  
			"a.DIVISAO_5_SEQ_DB"
		],
		"order_by":[  
			["d1.CODIGO","ASC"],
			["d2.CODIGO","ASC"],
			["d3.CODIGO","ASC"],
			["d4.CODIGO","ASC"],
			["d5.CODIGO","ASC"]
		]
	},
	"por_apontamento": {
		"dump": "",
		"main": true,
		"select": [
			"a.DIVISAO_5_SEQ_DB",
			"CONCAT(t.CODIGO, '' - '', t.DESCRICAO) TERCEIRO",
			"CONCAT(d6.CODIGO, '' - '', d6.DESCRICAO) ATIVIDADE",
			"a.QTD_FUNCIONARIO",
			"a.INI_DH",
			"a.FIM_DH",
			"SEC_TO_TIME(TIMESTAMPDIFF(second,a.INI_DH,a.FIM_DH)) HORAS_ATIVIDADE",
			"a.PRODUCAO"
			],
		"from": "apontamento_mo_terceiro a",
		"inner_join": [
			["a", "boletim_mo", "b", "b.SEQ_DB = a.SEQ_DB_DEVICE_MASTER_SEQ_DB"],
			["a", "terceiro", "t", "t.SEQ_DB = a.TERCEIRO_SEQ_DB"],			
			["a", "divisao_6", "d6", "d6.SEQ_DB = a.DIVISAO_6_SEQ_DB"],
			["a", "divisao_5", "d5", "d5.SEQ_DB = a.DIVISAO_5_SEQ_DB"],
			["a", "divisao_4", "d4", "d4.SEQ_DB = a.DIVISAO_4_SEQ_DB"],
			["a", "divisao_3", "d3", "d3.SEQ_DB = a.DIVISAO_3_SEQ_DB"],
			["a", "divisao_2", "d2", "d2.SEQ_DB = a.DIVISAO_2_SEQ_DB"],
			["a", "divisao_1", "d1", "d1.SEQ_DB = a.DIVISAO_1_SEQ_DB"]		
		],
		"where": [
			"DATE(a.INI_DH) >= DATE_FORMAT(STR_TO_DATE(:inicio, ''%d/%m/%Y''), ''%Y-%m-%d'')",
			"DATE(a.INI_DH) <= DATE_FORMAT(STR_TO_DATE(:fim, ''%d/%m/%Y''), ''%Y-%m-%d'')",
			"a.TERCEIRO_SEQ_DB in (:NFS_THIRD_PARTY)",
			"a.PRODUCAO IS NOT NULL"
		],
		"order_by":[
			["d1.CODIGO","ASC"],
			["d2.CODIGO","ASC"],
			["d3.CODIGO","ASC"],
			["d4.CODIGO","ASC"],
			["d5.CODIGO","ASC"],
			["a.INI_DH","ASC"]
		],
		"group_result_by":"DIVISAO_5_SEQ_DB"
	}
}', '{% extends ''reports/base.twig'' %}

{% macro header(images, filial, filters, parameters) %}
	{% from ''reports/macros.twig'' import img64 %}
	<div class="row space-before highlight"  >
		<div class="col-xs-12 text-center" style="border: 1px solid">
			<h3><strong>RELATÓRIO DE RESUMO DOS TERCEIROS</strong></h3>
		</div>
	</div>
	
	<div class="row space-before" style="border: 1px solid">
		<div class="col-xs-2" style="padding-top:5px; padding-bottom:5px">    
			{% if filial.LOCAL == 2 %}
				{{ img64(images[''agis_sacyr''].image_base64,''100px'') }}
			{% elseif filial.LOCAL == 3 %}
				{{ img64(images[''agis_busnello''].image_base64,''100px'') }}
			{% else %}
				{{ img64(images[''agis''].image_base64,''100px'') }}	
			{% endif %}
		</div>
		<div class="col-xs-6 text-right" style="border-right: 1px solid">
			<h3>{{filial.DESCRICAO}}</h3>
			<p>{{filial.ENDERECO}} - CEP: {{filial.CEP}} 
				{% if filial.ESTADO is not null %}
					- {{filial.ESTADO}}
				{% endif %}
			</p>
			<p>Tel: {{filial.TELEFONE}} 
				{% if filial.FAX is not null %}
					- Fax: {{filial.FAX}}
				{% endif %}
			</p>
			<p>{{filial.SITE}}</p>
		</div>

		<div class="col-xs-4">
			<p>Período: <strong>{{ filial.PERIODO }}</strong></p>
			<p>Data/Hora Emissão:  <strong>{{ ''now''|date(''d/m/Y H:i:s'') }}</strong></p>
			
		</div>
		
	</div>
{% endmacro %}

{% block content %}
	{% set filial = filial[0]%}
	{% set total = total[0]%}
	{{ _self.header(images, filial) }}
	<div class="row space-before highlight" style="border: 1px solid">
		<div class="col-xs-12 ">
			<strong>RESUMO </strong>
		</div>
	</div>
	
	<div class="row" style="border: 1px solid">
		<div class="col-xs-12">
			<strong>Terceiros</strong>
		</div>
	</div>
	{% for n in por_nivel %}
		<div class="row" style="border: 1px solid">
			<div class="col-xs-12">
				<table class="table table-condensed">
				
					<thead>
						<tr>
							<th class="text-center" style="font-size: 12px;vertical-align:middle">Nível 1</th>
							<th class="text-center" style="font-size: 12px;vertical-align:middle">Nível 2</th>
							<th class="text-center" style="font-size: 12px;vertical-align:middle">Nível 3</th>
							<th class="text-center" style="font-size: 12px;vertical-align:middle">Nível 4</th>
							<th class="text-center" style="font-size: 12px;vertical-align:middle">Nível 5</th>
							<th class="text-center" style="font-size: 12px;vertical-align:middle;width: 30%"></th>
							
						</tr>
					</thead>
					<tbody>
						<tr>
							<td class="text-center" style="font-size: 12px">{{ n.NIVEL_1 }}</td>
							<td class="text-center" style="font-size: 12px">{{ n.NIVEL_2 }}</td>
							<td class="text-center" style="font-size: 12px">{{ n.NIVEL_3 }}</td>
							<td class="text-center" style="font-size: 12px">{{ n.NIVEL_4 }}</td>
							<td class="text-center" style="font-size: 12px">{{ n.NIVEL_5 }}</td>
						</tr>
					</tbody>
				
				</table>
			</div>
		</div>
		
		<div class="row" style="border: 1px solid">
			<div class="col-xs-12">
				<table class="table table-condensed">
					<thead>
						<tr>
							<th class="text-center" style="font-size: 12px;vertical-align:middle">Terceiro</th>
							<th class="text-center" style="font-size: 12px;vertical-align:middle">Atividade</th>
							<th class="text-center" style="font-size: 12px;vertical-align:middle">Início</th>
							<th class="text-center" style="font-size: 12px;vertical-align:middle">Fim</th>
							<th class="text-center" style="font-size: 12px;vertical-align:middle">Horas da Atividade</th>
							<th class="text-center" style="font-size: 12px;vertical-align:middle">Quantidade de Funcionários</th>
							<th class="text-center" style="font-size: 12px;vertical-align:middle">Produção</th>
						</tr>
					</thead>
					<tbody>
						{% for a in por_apontamento[n.DIVISAO_5_SEQ_DB] %}							
							<tr>
								<td class="text-center" style="font-size: 12px;width: 25%">{{ a.TERCEIRO }}</td>
								<td class="text-center" style="font-size: 12px;width: 25%">{{ a.ATIVIDADE }}</td>
								<td class="text-center" style="font-size: 12px;width: 10%">{{ a.INI_DH|date(''d/m/Y H:i:s'') }}</td>
								<td class="text-center" style="font-size: 12px;width: 10%">{{ a.FIM_DH|date(''d/m/Y H:i:s'') }}</td>
								<td class="text-center" style="font-size: 12px;width: 10%">{{ a.HORAS_ATIVIDADE|nfs_type(''HORA'') }}</td>
								<td class="text-center" style="font-size: 12px;width: 10%">{{ a.QTD_FUNCIONARIO }}</td>
								<td class="text-center" style="font-size: 12px;width: 10%">{{ a.PRODUCAO }}</td>
							</tr>							
						{% endfor %}
					
						<tr>
							<td colspan=''4'' class="text-left" style="font-size: 12px;width: 70%"><strong><i>Total por Nível</i></strong></td>
							<td class="text-center" style="font-size: 12px;width: 10%">{{ n.HORAS_ATIVIDADE|nfs_type(''HORA'') }}</td>
							<td class="text-center" style="font-size: 12px;width: 10%"></td>
							<td class="text-center" style="font-size: 12px;width: 10%">{{ n.PRODUCAO }}</td>
						</tr>
					</tbody>
				</table>
			</div>
		</div>
	{% endfor %}
	<div class="row" style="border: 1px solid">
		<div class="col-xs-12">
			<table class="table table-condensed">
				<tbody>
					<tr>
						<td colspan=''4'' class="text-left" style="font-size: 12px;width: 70%"><strong><i>Total Geral</i></strong></td>								
						<td class="text-center" style="font-size: 12px;width: 10%">{{ total.HORAS_ATIVIDADE|nfs_type(''HORA'') }}</td>
						<td class="text-center" style="font-size: 12px;width: 10%"></td>
						<td class="text-center" style="font-size: 12px;width: 10%">{{ total.PRODUCAO }}</td>
					</tr>
				</tbody>
			</table>
		</div>
	</div>
	<div class="break-after">
	</div>
				
{% endblock %}', 1, '{
	"pdf": {
		"orientation": "Landscape",
		"footer-right": "[title] - [page]/[topage]"
	},
	"images": [
		"agis",
		"agis_sacyr",
		"agis_busnello"
	]
}');

Relatório de Resumo do Terceiro no modelo Excel

 UPDATE nfs_reports set enabled = 0 where DISPLAY_NAME ='relatorio_resumo_terceiro_excel';

INSERT INTO nfs_reports (SEQ_DB, NAME, DISPLAY_NAME, FILTERS, QUERIES, TEMPLATE, ENABLED, EXPORT_OPTIONS) VALUES(129, 'relatorio_resumo_terceiro_excel', 'Resumo dos Terceiros(.xls)', '{
	"inicio": {
		"type": "Date",
		"options": {
			"label": "De",
			"required": true
		}
	},
	"fim": {
		"type": "Date",
		"options": {
			"label": "Até",
			"required": true
		}
	},
	"terceiro": {
		"type": "Table",
		"options": {
			"label": "Terceiro",
			"multiple": false,
			"required": false
		}
	}
}
', '{
	"filial": {
		"dump": "",
		"main": true,
		"select": [
			"f.DESCRICAO",
			"f.ENDERECO",
			"f.CEP",
			"f.ESTADO",
			"f.TELEFONE",
			"f.FAX",
			"f.SITE",
			"concat(DATE_FORMAT(STR_TO_DATE(:inicio, ''%d/%m/%Y''), ''%d/%m/%Y''), '' - '', DATE_FORMAT(STR_TO_DATE(:fim, ''%d/%m/%Y''), ''%d/%m/%Y'')) PERIODO"
		],
		"from": "endereco_filial f"
	},
	"total": {
		"dump": "",
		"main": true,
		"select": [
			"SUM(SEC_TO_TIME(TIMESTAMPDIFF(second,a.INI_DH,a.FIM_DH))) HORAS_ATIVIDADE",
			"SUM(a.PRODUCAO) PRODUCAO"
		],
		"from": "apontamento_mo_terceiro a",
		"inner_join": [
			["a", "boletim_mo", "b", "b.SEQ_DB = a.SEQ_DB_DEVICE_MASTER_SEQ_DB"],
			["a", "terceiro", "t", "t.SEQ_DB = a.TERCEIRO_SEQ_DB"],
			["a", "divisao_6", "d6", "d6.SEQ_DB = a.DIVISAO_6_SEQ_DB"],
			["a", "divisao_5", "d5", "d5.SEQ_DB = a.DIVISAO_5_SEQ_DB"],
			["a", "divisao_4", "d4", "d4.SEQ_DB = a.DIVISAO_4_SEQ_DB"],
			["a", "divisao_3", "d3", "d3.SEQ_DB = a.DIVISAO_3_SEQ_DB"],
			["a", "divisao_2", "d2", "d2.SEQ_DB = a.DIVISAO_2_SEQ_DB"],
			["a", "divisao_1", "d1", "d1.SEQ_DB = a.DIVISAO_1_SEQ_DB"]
		],
		"where": [
			"DATE(a.INI_DH) >= DATE_FORMAT(STR_TO_DATE(:inicio, ''%d/%m/%Y''), ''%Y-%m-%d'')",
			"DATE(a.INI_DH) <= DATE_FORMAT(STR_TO_DATE(:fim, ''%d/%m/%Y''), ''%Y-%m-%d'')",
			"a.TERCEIRO_SEQ_DB in (:NFS_THIRD_PARTY)",
			"a.PRODUCAO IS NOT NULL"
		],
		"having": ["HORAS_ATIVIDADE > 0"]
	},	
	"por_nivel": {
		"dump": "",
		"main": true,
		"select": [
			"a.DIVISAO_5_SEQ_DB",
			"CONCAT(d1.CODIGO, '' - '', d1.DESCRICAO) NIVEL_1",
			"CONCAT(d2.CODIGO, '' - '', d2.DESCRICAO) NIVEL_2",
			"CONCAT(d3.CODIGO, '' - '', d3.DESCRICAO) NIVEL_3",
			"CONCAT(d4.CODIGO, '' - '', d4.DESCRICAO) NIVEL_4",
			"CONCAT(d5.CODIGO, '' - '', d5.DESCRICAO) NIVEL_5",
			"SUM(SEC_TO_TIME(TIMESTAMPDIFF(second,a.INI_DH,a.FIM_DH))) HORAS_ATIVIDADE",
			"SUM(a.PRODUCAO) PRODUCAO"
		],
		"from": "apontamento_mo_terceiro a",
		"inner_join": [
			["a", "boletim_mo", "b", "b.SEQ_DB = a.SEQ_DB_DEVICE_MASTER_SEQ_DB"],
			["a", "terceiro", "t", "t.SEQ_DB = a.TERCEIRO_SEQ_DB"],
			["a", "divisao_6", "d6", "d6.SEQ_DB = a.DIVISAO_6_SEQ_DB"],
			["a", "divisao_5", "d5", "d5.SEQ_DB = a.DIVISAO_5_SEQ_DB"],
			["a", "divisao_4", "d4", "d4.SEQ_DB = a.DIVISAO_4_SEQ_DB"],
			["a", "divisao_3", "d3", "d3.SEQ_DB = a.DIVISAO_3_SEQ_DB"],
			["a", "divisao_2", "d2", "d2.SEQ_DB = a.DIVISAO_2_SEQ_DB"],
			["a", "divisao_1", "d1", "d1.SEQ_DB = a.DIVISAO_1_SEQ_DB"]			
		],
		"where": [
			"DATE(a.INI_DH) >= DATE_FORMAT(STR_TO_DATE(:inicio, ''%d/%m/%Y''), ''%Y-%m-%d'')",
			"DATE(a.INI_DH) <= DATE_FORMAT(STR_TO_DATE(:fim, ''%d/%m/%Y''), ''%Y-%m-%d'')",
			"a.TERCEIRO_SEQ_DB in (:NFS_THIRD_PARTY)",
			"a.PRODUCAO IS NOT NULL"
		],		
		"group_by":[  
			"a.DIVISAO_5_SEQ_DB"
		],
		"order_by":[  
			["d1.CODIGO","ASC"],
			["d2.CODIGO","ASC"],
			["d3.CODIGO","ASC"],
			["d4.CODIGO","ASC"],
			["d5.CODIGO","ASC"]
		]
	},
	"por_apontamento": {
		"dump": "",
		"main": true,
		"select": [
			"a.DIVISAO_5_SEQ_DB",
			"CONCAT(t.CODIGO, '' - '', t.DESCRICAO) TERCEIRO",
			"CONCAT(d6.CODIGO, '' - '', d6.DESCRICAO) ATIVIDADE",
			"a.QTD_FUNCIONARIO",
			"a.INI_DH",
			"a.FIM_DH",
			"SEC_TO_TIME(TIMESTAMPDIFF(second,a.INI_DH,a.FIM_DH)) HORAS_ATIVIDADE",
			"a.PRODUCAO"
			],
		"from": "apontamento_mo_terceiro a",
		"inner_join": [
			["a", "boletim_mo", "b", "b.SEQ_DB = a.SEQ_DB_DEVICE_MASTER_SEQ_DB"],
			["a", "terceiro", "t", "t.SEQ_DB = a.TERCEIRO_SEQ_DB"],			
			["a", "divisao_6", "d6", "d6.SEQ_DB = a.DIVISAO_6_SEQ_DB"],
			["a", "divisao_5", "d5", "d5.SEQ_DB = a.DIVISAO_5_SEQ_DB"],
			["a", "divisao_4", "d4", "d4.SEQ_DB = a.DIVISAO_4_SEQ_DB"],
			["a", "divisao_3", "d3", "d3.SEQ_DB = a.DIVISAO_3_SEQ_DB"],
			["a", "divisao_2", "d2", "d2.SEQ_DB = a.DIVISAO_2_SEQ_DB"],
			["a", "divisao_1", "d1", "d1.SEQ_DB = a.DIVISAO_1_SEQ_DB"]		
		],
		"where": [
			"DATE(a.INI_DH) >= DATE_FORMAT(STR_TO_DATE(:inicio, ''%d/%m/%Y''), ''%Y-%m-%d'')",
			"DATE(a.INI_DH) <= DATE_FORMAT(STR_TO_DATE(:fim, ''%d/%m/%Y''), ''%Y-%m-%d'')",
			"a.TERCEIRO_SEQ_DB in (:NFS_THIRD_PARTY)",
			"a.PRODUCAO IS NOT NULL"
		],
		"order_by":[
			["d1.CODIGO","ASC"],
			["d2.CODIGO","ASC"],
			["d3.CODIGO","ASC"],
			["d4.CODIGO","ASC"],
			["d5.CODIGO","ASC"],
			["a.INI_DH","ASC"]
		],
		"group_result_by":"DIVISAO_5_SEQ_DB"
	}
}', '$objPHPExcel = new \\PHPExcel();
$sheet = $objPHPExcel->getActiveSheet();
$row = 1;

$styleItalic = [
	''font'' => array(
		''bold'' => true,
		''italic'' => true,
		''color'' => array(''rgb'' => ''000000'')
	),
	''fill'' => [
		''type'' => \\PHPExcel_Style_Fill::FILL_SOLID,
		''color'' => [''rgb'' => ''FFFFFF'']
	],
	''alignment'' => array(
		''horizontal'' => PHPExcel_Style_Alignment::HORIZONTAL_LEFT,	
	)
];

$styleCenter = [
	''fill'' => [
		''type'' => \\PHPExcel_Style_Fill::FILL_SOLID,
		''color'' => [''rgb'' => ''FFFFFF'']
	],
	''alignment'' => array(
		''horizontal'' => PHPExcel_Style_Alignment::HORIZONTAL_CENTER,	
	)
];
				
$styleBold = [
	''font'' => array(
		''bold'' => true,
		''color'' => array(''rgb'' => ''000000'')
	),
	''fill'' => [
		''type'' => \\PHPExcel_Style_Fill::FILL_SOLID,
		''color'' => [''rgb'' => ''FFFFFF'']
	],
	''alignment'' => array(
		''horizontal'' => PHPExcel_Style_Alignment::HORIZONTAL_CENTER,	
	),
	''borders'' => array(
		''outline'' => array(
			''style'' => PHPExcel_Style_Border::BORDER_THICK,
			''color'' => array(''rgb'' => ''F0F0F0''),
		),
	)
];

$styleTitleWhite = [
	''font'' => array(
		''bold'' => true,
		''color'' => array(''rgb'' => ''000000'')
	),
	''fill'' => [
		''type'' => \\PHPExcel_Style_Fill::FILL_SOLID,
		''color'' => [''rgb'' => ''FFFFFF'']
	],
	''alignment'' => array(
		''horizontal'' => PHPExcel_Style_Alignment::HORIZONTAL_LEFT,	
	),
	''borders'' => array(
		''outline'' => array(
			''style'' => PHPExcel_Style_Border::BORDER_THICK,
			''color'' => array(''rgb'' => ''F0F0F0''),
		),
	)
];

$styleTitleGrey = [
	''font'' => array(
		''bold'' => true,
		''color'' => array(''rgb'' => ''000000'')
	),
	''fill'' => [
		''type'' => \\PHPExcel_Style_Fill::FILL_SOLID,
		''color'' => [''rgb'' => ''F0F0F0'']
	],
	''alignment'' => array(
		''horizontal'' => PHPExcel_Style_Alignment::HORIZONTAL_LEFT,	
	),
	''borders'' => array(
		''outline'' => array(
			''style'' => PHPExcel_Style_Border::BORDER_THICK,
			''color'' => array(''rgb'' => ''F0F0F0''),
		),
	)
];
			
$styleTitleBlue = [
	''font'' => array(
		''bold'' => true,
		''color'' => array(''rgb'' => ''FFFFFF'')
	),
	''fill'' => [
		''type'' => \\PHPExcel_Style_Fill::FILL_SOLID,
		''color'' => [''rgb'' => ''2d5f8b'']
	],
	''alignment'' => array(
		''horizontal'' => PHPExcel_Style_Alignment::HORIZONTAL_LEFT,	
	),
	''borders'' => array(
		''outline'' => array(
			''style'' => PHPExcel_Style_Border::BORDER_THICK,
			''color'' => array(''rgb'' => ''FFFFFF''),
		),
	)
];
$styleTitle = [
	''font'' => array(
		''size'' => 24,
		''bold'' => true,
		''color'' => array(''rgb'' => ''000000'')
	),
	''fill'' => [
		''type'' => \\PHPExcel_Style_Fill::FILL_SOLID,
		''color'' => [''rgb'' => ''F0F0F0'']
	],
	''alignment'' => array(
		''horizontal'' => PHPExcel_Style_Alignment::HORIZONTAL_CENTER,	
	),
	''borders'' => array(
		''outline'' => array(
			''style'' => PHPExcel_Style_Border::BORDER_THICK,
			''color'' => array(''rgb'' => ''FFFFFF''),
		)
	)
];

foreach (range(''A'',''G'') as $column) {
	$sheet->getColumnDimension($column)->setAutoSize(true);
}

$sheet->getStyle("A{$row}:G{$row}")->applyFromArray($styleTitle);
$sheet->mergeCells("A{$row}:G{$row}");
$sheet->SetCellValue("A{$row}", "RELATÓRIO DE RESUMO DOS TERCEIROS");
$sheet->getRowDimension(''1'')->setRowHeight(40);
$sheet->getColumnDimension(''A'')->setWidth(100);
$row++;


$filial = $templateData[''filial''][0];

$sheet->SetCellValue("A{$row}", $filial[''DESCRICAO'']);
$sheet->SetCellValue("B{$row}", "PERÍODO: ".$filial[''PERIODO'']);
$row++;

$endereco = $filial[''ENDERECO''] .'' - ''. $filial[''CEP''];
if ($filial[''ESTADO''] != null) {
	$endereco = $endereco.'' - ''. $filial[''ESTADO''];
}
$sheet->SetCellValue("A{$row}", $endereco );
$sheet->SetCellValue("B{$row}", "Data/Hora Emissão:".date(''d/m/Y H:i:s''));
$row++;	

$tel = $filial[''TELEFONE''];
if ($filial[''FAX''] != null) {
	$tel = $tel.'' - ''. $filial[''FAX''];
}
$sheet->SetCellValue("A{$row}", $tel);
$row++;	

$sheet->SetCellValue("A{$row}", $filial[''SITE'']);	
$row++;

$total = $templateData[''total''][0];

$row ++;
$sheet->getStyle("A{$row}:G{$row}")->applyFromArray($styleTitleGrey);
$sheet->mergeCells("A{$row}:G{$row}");
$sheet->SetCellValue("A{$row}", "RESUMO");			

$row ++;			
$sheet->getStyle("A{$row}:G{$row}")->applyFromArray($styleTitleWhite);
$sheet->mergeCells("A{$row}:G{$row}");
$sheet->SetCellValue("A{$row}", "Terceiros");	

foreach ($templateData[''por_nivel''] as $n) {
	$row ++;		
	$sheet->getStyle("A{$row}:G{$row}")->applyFromArray($styleBold);				
	$sheet->SetCellValue("A{$row}", "Nível 1");
	$sheet->SetCellValue("B{$row}", "Nível 2");
	$sheet->SetCellValue("C{$row}", "Nível 3");
	$sheet->SetCellValue("D{$row}", "Nível 4");
	$sheet->SetCellValue("E{$row}", "Nível 5");
	
	$row ++;
	$sheet->getStyle("A{$row}:G{$row}")->applyFromArray($styleCenter);
	$sheet->SetCellValue("A{$row}", $n[''NIVEL_1'']);
	$sheet->SetCellValue("B{$row}", $n[''NIVEL_2'']);
	$sheet->SetCellValue("C{$row}", $n[''NIVEL_3'']);
	$sheet->SetCellValue("D{$row}", $n[''NIVEL_4'']);
	$sheet->SetCellValue("E{$row}", $n[''NIVEL_5'']);
							
	$row ++;
	$sheet->getStyle("A{$row}:G{$row}")->applyFromArray($styleBold);
	$sheet->SetCellValue("A{$row}", "Terceiro");
	$sheet->SetCellValue("B{$row}", "Atividade");
	$sheet->SetCellValue("C{$row}", "Início");
	$sheet->SetCellValue("D{$row}", "Fim");
	$sheet->SetCellValue("E{$row}", "Horas da Atividade");
	$sheet->SetCellValue("F{$row}", "Quantidade de Funcionários");
	$sheet->SetCellValue("G{$row}", "Produção");
	
	foreach ($templateData[''por_apontamento''][$n[''DIVISAO_5_SEQ_DB'']] as $a) {	
		$row ++;						
		$sheet->getStyle("A{$row}:G{$row}")->applyFromArray($styleCenter);
		$sheet->SetCellValue("A{$row}", $a[''TERCEIRO'']);
		$sheet->SetCellValue("B{$row}", $a[''ATIVIDADE'']);
		$sheet->SetCellValue("C{$row}", date("d/m/Y H:m:s", strtotime($a[''INI_DH''])));
		$sheet->SetCellValue("D{$row}", date("d/m/Y H:m:s", strtotime($a[''FIM_DH''])));
		$sheet->SetCellValue("E{$row}", \\core\\Utils::converteSegundos($a[''HORAS_ATIVIDADE'']));
		$sheet->SetCellValue("F{$row}", $a[''QTD_FUNCIONARIO'']);
		$sheet->SetCellValue("G{$row}", $a[''PRODUCAO'']);		
	}
	$row ++;
	$sheet->getStyle("A{$row}:D{$row}")->applyFromArray($styleItalic);	
	$sheet->getStyle("E{$row}:G{$row}")->applyFromArray($styleCenter);	
	$sheet->mergeCells("A{$row}:D{$row}");
	$sheet->SetCellValue("A{$row}", "Total por Nível");				
	$sheet->SetCellValue("E{$row}", \\core\\Utils::converteSegundos($n[''HORAS_ATIVIDADE'']));
	$sheet->SetCellValue("F{$row}", '''');
	$sheet->SetCellValue("G{$row}", $n[''PRODUCAO'']);
}

$row ++;
$sheet->getStyle("A{$row}:D{$row}")->applyFromArray($styleItalic);	
$sheet->getStyle("E{$row}:G{$row}")->applyFromArray($styleCenter);	
$sheet->mergeCells("A{$row}:D{$row}");
$sheet->SetCellValue("A{$row}", "Total Geral");				
$sheet->SetCellValue("E{$row}", \\core\\Utils::converteSegundos($total[''HORAS_ATIVIDADE'']));
$sheet->SetCellValue("F{$row}", '''');
$sheet->SetCellValue("G{$row}", $total[''PRODUCAO'']);

$sheet->setTitle(substr($templateData[''display_name''], 0, 30));
', 1, '{
	"excel": {
		"template":  ""
	}
}');

Private User

Private User é uma nova funcionalidade do NFS que permite usar a tabela NFS_ACL_USUARIO para:

  • Usar em apontamentos
  • Exibir só os dados relacionados a esse usuário por default
  • Relatórios somente para esse tipo de usuário
  • Vincular o usuário logado com o registro de efetivo_funcionário ( por exemplo)

A primeira configuração é definir quais usuários serão private user, depois é só ir na nfs_acl_usuario e na coluna TIPO_USUARIO_SEQ_DB e difinir igual a 5, também é possível realizar isso pelo Controle de Acesso.

Private User - CRUD

Vamos supor que você tem a tarefa de configurar para que no apontamento_ponto tenha o nfs_acl_usuario. Para isso é necessário o seguinte:

  • NOME deve ser igual a NFS_ACL_USUARIO, hoje só vemos a necessidade de um usuário por tabela.
  • TIPO deve ser igual USER
  • LINK deve ser igual a NFS_ACL_USUARIO
  • PROPERTIES deve ser a seguinte configuração, {"users": "privateUsers"}, importantíssimo para no combo no CRUD mostrar os usuários.
  • Esse item não deve ser configurado, mas por padrão o DISPLAY_FIELDS é NOME :: SOBRENOME

Para no caso de adicinar na Apontamento Ponto.

INSERT INTO nfs_core_ds_tabela_campo (TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES) VALUES('APONTAMENTO_PONTO', 'NFS_ACL_USUARIO', 5, 0, 1, 1, 1, 'Funcionário', 'Funcionário', 'USER', 1, NULL, NULL, NULL, 'IU', '0', NULL, 'NFS_ACL_USUARIO', NULL, NULL, NULL, NULL, NULL, NULL, '{ "users": "privateUsers"}');

Usuário com mais informações

Outra caso comum é ter uma tarefa para ter mais informações relacionado ao nosso Usuário, no caso NÃO É PARA SER CRIADA NENHUM COLUNA NOVA NA NFS_ACL_USUARIO e sim no NFS criar uma app_ para ser relacionado ao usuário, podemos fazer isso hoje com a nossa tabela de Funcionário ficando:

INSERT INTO nfs_core_ds_tabela_campo (TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES) VALUES('FUNCIONARIO', 'NFS_ACL_USUARIO', 7, 0, 1, 1, 1, 'Usuário', 'Usuário', 'USER', 1, NULL, NULL, NULL, 'IU', '0', NULL, 'NFS_ACL_USUARIO', NULL, NULL, NULL, NULL, NULL, NULL, '{ "users": "privateUsers"}');

Por exemplo nessa tabela de Funcionário você pode relacionar um funcionário x usuário e adicionar mais informações como:

  • PIS
  • Endereço
  • Apelido (Não no sentido prejorativo)
  • Conta Bancaria

Entre outras que seja necessário e não tenha na NFS_ACL_USUARIO.

Private User - Relatórios

Quando o usuário for desse tipo terá disponível para ser usado no NFS Query Builder o parâmetro :NFS_ACL_USUARIO_SEQ_DB, onde vai ser disponível filtrar pelo o usuário logado. Então supondo que tenha 5 usuários private user e esteja logado com um, então colocando o where abaixo será filtrado somente pelos meus dados.

Atenção na montagem da query para não realizar algum left join e exibir dados não necessários

{
	"filiais": {
		"dump": "",
		"main": true,
		"select": [
			"distinct fi.SEQ_DB SEQ_DB"
		],
		"from": "filial_banco fi",
		"inner_join": [
			["fi", "apontamento_ponto", "a", "a.filial = fi.SEQ_DB"],
			["a", "nfs_acl_usuario", "nfs", "nfs.seq_db = a.nfs_acl_usuario_seq_db "]
		],
		"where": [
			"a.nfs_acl_usuario_seq_db in (:NFS_ACL_USUARIO_SEQ_DB)",
			"DATE(a.INI_DH) >= DATE_FORMAT(STR_TO_DATE(:inicio, '%d/%m/%Y'), '%Y-%m-%d')",
			"DATE(a.INI_DH) <= DATE_FORMAT(STR_TO_DATE(:fim, '%d/%m/%Y'), '%Y-%m-%d')",
			"fi.CNPJ is not null",
			"fi.DESCRICAO is not null"
		]
	}
}

Caso o relatorio esteja sendo feito utilizando Entry Point, podemos filtrar os dados pegando o usuario atual da sessão e caso seja um usuario PRIVATE, aplicar as tratativas, exemplo:

$user = xDS::getUser();

$apontamentos = Dao::table('APT');
if ($user->isPrivateUser()) {
	$apontamenos->where(['NFS_ACL_USUARIO_SEQ_DB' => $user->SEQ_DB]);
}
$apontamentos->get();

Buscar Dados de Outro Usuário

Existe casos onde é necessário buscar dados de outro usuário num entry point quando o mesmo é um terceiro.

Para resolver esse problema foi criado um parâmetro AllowAnyPrivateUserSearch para tirar a limitação com a busca pelo usuário de terceiro e poder encontrar qualquer dados.

Um exemplo, é o usuário Luis aprovar um boletim e no entry point vai mandar para o usuário Dalton que também ao modificar o boletim envia de voltar para o Luis e ambos usuário são terceiros, com isso precisa usar a flag AllowAnyPrivateUserSearch. Esse parâmetro é usado somente no update.

Como usar

$params['AllowAnyPrivateUserSearch'] = true; // vai permitir buscar e alterar os dados 
$seqDb = 1; 
$updatedValues = []; // seus valores para serem alterados
$tableBo->updateRow('MY_UPDATED_TABLE', $seqDb, $updatedValues, $params);

Acesso por AD

Hoje no NFS temos a opção de acesso por Active Directory através do protocolo SAML (O que é SAML?), essa configuração é realizada dentro da coluna OPTIONS na tabela NFS_HOST, chamamos de login_custom, exemplo de como é feito na Suzano SAL:

{
    "newProcess": true,
    "login_custom": {
        "main_url": "https://login.microsoftonline.com/tenant_id/saml2",
        "redirect_route": "/login",
        "sso_provider_name": "suzano-sal.simova.cloud",
        "sso_issuer_name": "client_id",
        "sso_cod_sis": "SAL",
        "btn_name": "Acesso AD Suzano",
        "id": "tenant_id"
    }
}

Como é possível observar exisite a chave login_custom, nela temos algumas informações que são fornecidades pelo cliente:

  • main_url: URL de acesso ao SAML, ela é encontrada no sistema de configuração do cliente, geralmente se for Azure: https://login.microsoftonline.com/tenant_id/saml2
  • ID: Nesse campo deve ser passado o Tenant ID
  • sso_issuer_name: Nesse campo deve ser passado o Client ID (Cliente ID).

O que é configurado por nós são:

  • redirect_route: Será por enquanto o /login(Obrigatório que seja essa rota).
  • sso_provider_name: A URL de acesso ao NFS.
  • sso_cod_sis: Se o cliente não passar o Sso Cod Sis coloque por exemplo o nome do domain do cliente
  • btn_name: Texto que vai aparecer no botão de login

Local na Azure onde tem as informações

2022-10-13_10-40.png

Após toda a configuração no login vai aparecer um botão para permitir acesso por AD e o cliente não precisa mais colocar usuário e senha, porém o usuário do cliente deve estar cadastrado no NFS porque ele vai buscar pelo EMAIL.

2022-10-13_10-40.png

Outro exemplo:

{
  "newProcess": true,
  "login_custom": {
    "main_url": "https://login.microsoftonline.com/8d410b59-5774-4b5f-8bcf-ab7476ad974b/saml2",
    "redirect_route": "/login",
    "sso_provider_name": "cummins-dev.h.simova.cloud",
    "sso_issuer_name": "54f80e23-9e8f-4987-b5de-40f6ce75d2d1",
    "sso_cod_sis": "CUMMINS-DEV",
    "btn_name": "Test Dalton AD",
    "id": "cummis_dev"
  }
}

Segurança e Credenciais

Autenticação em duas etapas

Como habilitar e usar a Autenticação Multifator

A autenticação multifator está fixa no sistema e pode ser ativada ou desativada pelo próprio usuário. Para verificar o status do MFA, basta acessar o menu Seguraça e Credenciais como no exemplo abaixo:

autenticação_multifator.png

Baixe o aplicativo utilizando o link abaixo:

https://play.google.com/store/apps/details?id=com.azure.authenticator&pli=1

Para configurar a chave de acesso basta clicar no botão Multifator Inativo e escanear o QR Code através de um dispositivo móvel.

OBS: o QR code é gerado e atualizado de forma dinamica no sistema, a imagem abaixo é apenas um exemplo.

autenticação_multifator_qr_code.png

Tendo o aplicativo instalado, após escanear já será criado automaticamente o usuário Simova - Painel NFS - MFA no aplicativo Microsoft autenticator. Basta acessá-lo e inserir o código gerado nos campos do sistema e salvar.

Acesso Corporativo

Para habilitar o acesso corporativo é necessário ir na tabela nfs_acl_usuario e habilitar na coluna corporate para 1.

A partir daí ao entrar no sistema o login será um pouco diferente podendo escolher:

  • Nenhuma filial/local, se ir e clicar em entrar, implicitamente quer dizer que foi logado em todas as filiais.
  • Uma ou mais filias/locais, nesse caso é possível escolher uma ou mais filiais e locais para serem acessados simultaneamnete.

Painel

ATENÇÃO Nos Query Builders do Painel que usam :NFS_FILIAL e :NFS_LOCAL passaram a passar mais de um argumento quando for escolhido mais de uma filial, logo eles precisam serem mudados para IN ao invés de =

Exemplo da MaqCampo

No painel da MaqCampo tem um local que passa um indicador dizendo se a OS está fechada:

Antes:

select count(o.SEQ_DB) from app_os o where o.STATUS_OS_SEQ_DB = 3 and  o.DATA_ABERTURA > date_sub(now(), interval 30 DAY) and ATIVO = 1  AND DELETED = 0 AND o.EMPRESA = :NFS_EMPRESA and o.FILIAL = :NFS_FILIAL and o.`LOCAL` = :NFS_LOCAL

Depois

select count(o.SEQ_DB) from app_os o where o.STATUS_OS_SEQ_DB = 3 and  o.DATA_ABERTURA > date_sub(now(), interval 30 DAY) and ATIVO = 1  AND DELETED = 0 AND o.EMPRESA = :NFS_EMPRESA and o.FILIAL in (:NFS_FILIAL) and o.`LOCAL` in (:NFS_LOCAL)

SQL scripts

Caso não exista a coluna corporate execute o seguinte alter table:

ALTER TABLE nfs_acl_usuario ADD CORPORATE TINYINT(1) DEFAULT 0;

ATENÇÃO Como é algo novo ainda pode conter erros, caso encontre alguma abrir um jira para o time do core atuar.

Manipulaçao de Imagens

Exemplo de como obter a imagem, tratar e manipular imagens

Como obter IMAGE_MOBILE

A primeira coisa que deve ser feito para obter uma imagem é na consulta principal trazer somente o SEQ_DB da imagem, e nunca trazer ela de forma crua como já existe em muitos relatórios.

Após isso é necessário carregar/obter a imagem, que vai ser sempre em base64.

// 
$fieldName = 'FOTO_THUMBNAIL'; // se quiser obter a imagem original é só colocar FOTO
$tableName = 'APONTAMENTO_FOTO';
$seqDb = 859;

// $imageBase64 vai ser uma string em base64
$imageBase64 = ImageService::getImageFromMobile($fieldName, $tableName, $seqDb);

É extramente importante usar esse método, porque isso abstrai a forma de obter foto, assim se o CORE tirar as imagens do banco de dados não vai ser necessário alterar em nada o seu relatório.

Rotacionar imagem

Para rotacionar uma imagem basta utilizarmos a função convertOrientationImage como no exemplo abaixo:

Parametros da função:

Método para converter a orientação da imagem.

string $base64Img imagem passada como base 64 string $type Tipo da rotação, portrait ou landscape int $degrees Valor do graus para girar a imagem, por padrão é 90 bool $isBinary

return string $imagem em base64

$imgService = new ImageService();
$contentBase64 = $imgService->convertOrientationImage($base64, 'portrait', 90);

echo '<img src="data:image/png;base64,'.$contentBase64.'">';

Upload de Base64 - UploadService

$seqDbUpload = UploadService::uploadBase64("data:image/jpeg;base64,".$myBase64Url);

print_r($seqDbUpload);

Gerar imagem de um gráfico

O exemplo de configuração fica em Graficos e Dashboards, para acessar basta clicar aqui.

MENUS

  • Para acessar algumas funcionalidades do sistema de forma facil, será necessario configurar o acesso a esses itens no menu através de alguns passos:

NFS_CORE_MENU

  • Nesta tabela é onde iremos adicionar o menu em si, para adiciona-lo podemos seguir o exemplo:
INSERT INTO nfs_core_menu (SEQ_DB, EMPRESA, FILIAL, `LOCAL`, DESCRICAO, `TYPE`, ATIVO, FATHER, MENUORDER, URL, `FILTER`, ICON)
VALUES (300, 9999, 9999, 9999,'Mapas e Localizações', 'MENU', 1, NULL, 2, NULL, NULL, 'fa fa-map-o');

Onde:

  • SEQ_DB: Identificação do menu;
  • DESCRICAO: Representação do item do menu;
  • TYPE: Indica a estrutura a que pertence (mas não influencia na montagem do menu);
  • FATHER: Indica o menu pai na estrutura (verificar tabela abaixo);
  • MENUORDER: Ordem do item dentro do menu PAI (ordenação baseada em FATHER, MENUORDER e SEQ_DB);
  • URL: URL do menu (mais usado para itens CUSTOM);
  • FILTER: Filtro de tabelas;
  • ICON: Ícone do item do menu.
SEQ_DB RESERVADOS PARA MENUS
| SEQ_DB        | DESCRICAO                       |
| ------------- |:-------------------------------:|
| 11            | Sistema                         |
| 100           | Gestão à vista                  |
| 123           | Simova Apps                     |
| 200           | Parte Diária e Apontamentos     |
| 234           | Idiomas e Textos e Traduções    |
| 300           | Mapas e Localizações            |
| 400           | Cadastros                       |
| 500           | SIMOVA Big Data                 |
| 600           | Relatórios                      |
| 700           | Gerenciamento de Dados          |
| 1234          | Aplicativo SimovaApps           |
| 1236          | Dispositivos SimovaApps         |
| 9999          | Outros                          |
| 10002         | Marte                           |
| 10003         | Painel de Equipamentos com MARTE|
| 10004         | Mapa de Equipamentos com MARTE  |
| 10005         | Cadastros e Eventos             |
| 10010         | Simova Alerts                   |
| 10020         | Cadastros(GIS)                  |
| 10099         | Grupos GIS                      |
| 12345         | Chave Acesso SimovaApps         |

[!WARNING] Caso queira adicionar um menu como submenu, defina como FATHER o menu que irá conte-lo.

NFS_ACL_GRUPO_PERMISSAO

  • Para que um grupo de usuários possa acessar um menu adicionado em nfs_core_menu, será necessario adicionar a permissão para o mesmo, como no exemplo:

**sql INSERT INTO nfs_acl_grupo_permissao (EMPRESA, FILIAL, LOCAL, MENU_SEQ_DB, TABELA_NOME, SIUD, GRUPO_SEQ_DB, CAMPOS_PERMITIDOS, INS_DH, ATIVO, DELETED) VALUES(1, 9999, 9999, 300, NULL, '1111', 1, '', '2017-08-15 12:45:03', 1, 0); **

MENU Gis, Alerts e MARTE

Remover do cadastro nfs_core_menu os itens de MARTE e Alerts Confirmar que o item Mapas e Localizações seja o SEQ_DB 300

  • Na tabela nfs_grupo_permissao, incluir para cada grupo os valores na coluna MENU_SEQ_DB:

  • 10002 - Marte

  • 10010 - Simova Alerts

  • 10020 - Gis (precisa ativar o parametro GIS_ENABLED = 1 (nfs_core_par_parametros))

Controle de Acesso

[!WARNING] OBS: a tabela CONTROLE_ACESSO deverá ser excluida e utilizar a tabela SIMOVAAPPS para realizar as configurações. Para remover o menu CONTROLE_ACESSO colocar o _ignore* na tabela _nfs_core_menu* não funciona. A solução para a tabela CHAVE_ACESSO é excluir a tabela, pois agora será utilizada a tabela SIMOVAAPPS para fazer as configurações, logo pode ser removida a tabela CHAVE_ACESSO, pois já existe a validação no core que não exibirá esse menu quando não houver a tabela.

O menu controle de acesso está disponível para os usuários que possuem:

  • TIPO_SEQ_DB 2 (ADMIN) ou TIPO_SEQ_DB 100 (USERADMIN);
  • O módulo de controle de acesso liberado para o seu usuário ou ao grupo que ele pertence.

Grupo

O cadastro de grupos tem como finalidade conceder acesso ao sistema quando vinculado a usuários específicos. Essa funcionalidade é gerenciada pela tabela nfs_acl_grupo. A visibilidade do grupo está condicionada à configuração da coluna RESTRICTED. Se o valor desta coluna for definido como 1, o grupo será exibido apenas quando o usuário logado possuir o TIPO_USUARIO_SEQ_DB 2 ou 3.

Essa abordagem oferece um controle mais refinado sobre a visibilidade dos grupos, restringindo a exposição a usuários com permissões específicas. Portanto, ao configurar o valor da coluna RESTRICTED, é possível modular a visibilidade do grupo, adaptando-a de acordo com as necessidades específicas do seu controle de acesso no sistema.

Usuario

O módulo de usuário é responsável por facilitar a criação e gestão de contas de usuários, fornecendo acesso ao sistema. Este módulo opera principalmente através das tabelas nfs_acl_usuario, nfs_acl_usuario_empresa e nfs_acl_grupo_usuario.

Funcionalidades Principais

  1. nfs_acl_usuario: contém informações vitais sobre os usuários, incluindo dados como nome, senha e configurações específicas da conta do usuário.

  2. nfs_acl_usuario_empresa: armazena dados relacionados à empresa do usuário, estabelecendo uma conexão detalhada entre o usuário e a respectiva entidade à qual está associado.

  3. nfs_acl_grupo_usuario: relaciona os usuários ao respectivos grupos.

Lógica de Exibição

Ao realizar login, o sistema verifica o tipo de usuário ativo. Se o tipo de usuário for classificado como TIPO_USUARIO_SEQ_DB 2 ou 3, a lógica de exibição especial é acionada:

  • Grupos com a restrição RESTRICTED 1 serão listados;
  • Todos os tipos de usuários cadastrados serão apresentados, oferecendo uma visão completa das contas de usuário disponíveis no sistema.

Acesso de Rotas

Permite liberar as funcionalidades do sistema para usuários e grupos. O cadastro utiliza as tabelas nfs_acl_grupo_permissao_rota (permissão para o grupo) e nfs_acl_usuario_permissao_rota (permissão para o usuário), e:

  • Os módulos são listados da tabela nfs_system_modules;
  • Os módulos devem ser vinculados com o host na tabela nfs_hosts_n_system_modules.

[!IMPORTANT] Se o usuário logado possuir o TIPO_USUARIO_SEQ_DB 2 ou 3 serão listados os grupos com RESTRICTED 1 e todos os tipos de usuários cadastrados.

Exemplo de permissão via banco para todos os painéis em todos EFL

INSERT INTO `nfs_acl_grupo_permissao_rota` (`EMPRESA`, `FILIAL`, `LOCAL`, `TIPO`, `ROTA`, `GRUPO_SEQ_DB`, `INS_DH`, `ATIVO`, `DELETED`)
SELECT distinct 9999, 9999, 9999, 'ALLOW', '/panel/*', g.SEQ_DB, CURRENT_TIMESTAMP(), 1, 0
from nfs_acl_grupo g
where g.ATIVO = 1;

Exemplos de configuração do campo ROTA do comando acima:

  • Permissão para todos os calendários: /calendar/*
  • Permissão para o calendário com seq_db 1: /calendar/1
  • Permissão para todas as funcionalidades do cadastro de assinatura: /t/apontamento_assinatura/*
  • Permissão apenas para a listagem do cadastro de dtac: /t/apontamento_dtac/list
  • Permissão apenas para a inserção do cadastro de dtac: /t/apontamento_dtac/insert
  • Permissão apenas para a edição do cadastro de dtac: /t/apontamento_dtac/edit/*
  • Permissão para todos os cadastros: /t/*
  • Permissão para todos os relatórios: /reports/*
  • Permissão para todos os painéis: /panel/*

Exemplo prático de ativação dos painéis para um usuário utilizando a funcionalidade via tela:

salvar.gif

Exemplo prático de desativação dos painéis para um usuário utilizando a funcionalidade via tela:

desativar.gif

Exemplo de limpeza total da listagem via tela ( Não confundir com ativação/desativação das funcionalidades ):

limparlista.gif

Controle de Campos

Este cadastro deve ser utilizado quando é necessário esconder um campo do cadastro e da tela de detalhes. O cadastro utiliza as tabelas nfs_acl_grupo_permissao_campo (controle para o grupo) e nfs_acl_usuario_permissao_campo (controle para o usuário).

[!IMPORTANT] Se o usuário logado possuir o TIPO_USUARIO_SEQ_DB 2 ou 3 serão listados os grupos com RESTRICTED 1 e todos os tipos de usuários cadastrados.

Exibir Campos de SIUDT (Select/Insert/Update/Delete/Toggle)

Para determinar se os botões de Select (S), Insert (I), Update (U), Delete (D) e Toggle (T) devem ser exibidos, é necessário configurar as permissões nas tabelas nfs_acl_grupo_permissao e nfs_acl_usuario_permissao. As permissões podem ser configuradas tanto para grupos quanto para usuários.

Configuração por Grupo

A tabela nfs_acl_grupo_permissao é usada para definir permissões para um grupo de usuários. Vamos analisar um exemplo de configuração para o cadastro de EQUIPE:

INSERT INTO nfs_acl_grupo_permissao (
    SEQ_DB, EMPRESA, FILIAL, `LOCAL`, MENU_SEQ_DB, TABELA_NOME, SIUDT, GRUPO_SEQ_DB, CAMPOS_PERMITIDOS, INS_DH, ATIVO, DELETED
) VALUES (
    91, 1, 9999, 9999, NULL, 'EQUIPE', '10000', 6, '', '2020-06-15 09:59:52', 1, 0
);

Coluna SIUDT

Permissões de Select, Insert, Update, Delete e Toggle representadas como uma string de 5 caracteres, onde cada caractere pode ser 1 (permitido) ou 0 (não permitido).

  • O primeiro caractere representa a permissão de Select;
  • O segundo caractere representa a permissão de Insert;
  • O terceiro caractere representa a permissão de Update;
  • O quarto caractere representa a permissão de Delete;
  • O quinto caractere representa a permissão de Desativar o Registro.

Exemplo de Configuração

A configuração '10000' na coluna SIUDT indica que o grupo tem permissão para Select apenas. Ou seja:

  1. Select (S) = 1 (permitido);
  2. Insert (I) = 0 (não permitido);
  3. Update (U) = 0 (não permitido);
  4. Delete (D) = 0 (não permitido);
  5. Toggle (T) = 0 (não permitido)

A lógica para exibir os botões de ação (S, I, U, D, T) depende dos valores configurados na coluna SIUDT. Se o valor correspondente for 1, o botão será exibido; caso contrário, ele não será exibido.

Exemplo de configuração via tela:

image.png

actionpermission.png

Botões de ações na tela do CRUD

field_action_1.27f10a09.gif

Para adicionar um controle de acesso em um botão de ação depois de cria-lo (veja aqui como criar um botão de ação) basta selecionar a tabela a qual foi criada e escolher, pode ser por grupo ou usuário.

Controle de Acesso Field Action

Para controlar o acesso a esses campos foram criadas as tabelas nfs_acl_usuario_permissao_field_action e nfs_acl_grupo_permissao_field_action.

Estrutura da tabela de Usuário Permissão

CREATE TABLE `nfs_acl_usuario_permissao_field_action` (
  `SEQ_DB` bigint(20) NOT NULL AUTO_INCREMENT,
  `EMPRESA` bigint(20) NOT NULL DEFAULT '0',
  `FILIAL` bigint(20) NOT NULL DEFAULT '0',
  `LOCAL` bigint(20) NOT NULL DEFAULT '0',
  `TIPO` enum('ALLOW','DENY') DEFAULT NULL,
  `FIELD_ACTION_SEQ_DB` bigint(20) NOT NULL COMMENT 'Ação definida na tabela NFS_CORE_DS_FIELD_ACTION.',
  `INS_USUARIO_SEQ_DB` bigint(20) NOT NULL,
  `INS_DH` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `ATIVO` int(11) DEFAULT '1',
  `DELETED` int(11) DEFAULT '0',
  PRIMARY KEY (`SEQ_DB`),
  KEY `EMPRESA` (`EMPRESA`),
  KEY `FILIAL` (`FILIAL`),
  KEY `LOCAL` (`LOCAL`),
  KEY `FK_DS_FIELD_USUARIO_ACTION` (`FIELD_ACTION_SEQ_DB`),
  KEY `FK_USUARIO_ACTION` (`INS_USUARIO_SEQ_DB`),
  CONSTRAINT `FK_DS_FIELD_USUARIO_ACTION` FOREIGN KEY (`FIELD_ACTION_SEQ_DB`) REFERENCES `nfs_core_ds_field_action` (`SEQ_DB`) ON DELETE NO ACTION ON UPDATE NO ACTION,
  CONSTRAINT `FK_USUARIO_ACTION` FOREIGN KEY (`INS_USUARIO_SEQ_DB`) REFERENCES `nfs_acl_usuario` (`SEQ_DB`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;

Estrutura da tabela Grupo Permissão

CREATE TABLE `nfs_acl_grupo_permissao_field_action` (
  `SEQ_DB` bigint(20) NOT NULL AUTO_INCREMENT,
  `EMPRESA` bigint(20) NOT NULL DEFAULT '0',
  `FILIAL` bigint(20) NOT NULL DEFAULT '0',
  `LOCAL` bigint(20) NOT NULL DEFAULT '0',
  `TIPO` enum('ALLOW','DENY') DEFAULT NULL,
  `FIELD_ACTION_SEQ_DB` bigint(20) NOT NULL COMMENT 'Ação definida na tabela NFS_CORE_DS_FIELD_ACTION.',
  `GRUPO_SEQ_DB` bigint(20) NOT NULL,
  `INS_DH` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `ATIVO` int(11) DEFAULT '1',
  `DELETED` int(11) DEFAULT '0',
  PRIMARY KEY (`SEQ_DB`),
  KEY `EMPRESA` (`EMPRESA`),
  KEY `FILIAL` (`FILIAL`),
  KEY `LOCAL` (`LOCAL`),
  KEY `FK_DS_FIELD_ACTION` (`FIELD_ACTION_SEQ_DB`),
  KEY `FK_GRUPO_ACTION` (`GRUPO_SEQ_DB`),
  CONSTRAINT `FK_DS_FIELD_ACTION` FOREIGN KEY (`FIELD_ACTION_SEQ_DB`) REFERENCES `nfs_core_ds_field_action` (`SEQ_DB`) ON DELETE NO ACTION ON UPDATE NO ACTION,
  CONSTRAINT `FK_GRUPO_ACTION` FOREIGN KEY (`GRUPO_SEQ_DB`) REFERENCES `nfs_acl_grupo` (`SEQ_DB`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;

Quando for para dar permissão para um grupo ou usuário em um campo que vá em um CRUD é possível fazer isso pela de Tela de controle de Acesso por Campos.

Exemplo Acesso para o Grupo de SEQ_DB 2

INSERT INTO nfs_acl_grupo_permissao_field_action (`EMPRESA`, `FILIAL`, `LOCAL`, `TIPO`, `FIELD_ACTION_SEQ_DB`, `GRUPO_SEQ_DB`, `INS_DH`, `ATIVO`, `DELETED`) VALUES(4, 1, 1, 'ALLOW', 3, 2, '2019-07-30 18:44:26.000', 1, 0);

Ocultar Eventos Dinâmicos

Esta funcionalidade permite ocultar a visualização de eventos dinâmicos na aplicação. As funcionalidades são configuradas na tabela "nfs_acl_ocultar_eventos_dinamicos_funcionalidades". As permissões de visualização são controladas nas tabelas "nfs_acl_usuario_ocultar_eventos_dinamicos" e "nfs_acl_grupo_ocultar_eventos_dinamicos". A princípio, apenas a funcionalidade de indicadores da home está disponível para esse controle.

Como configurar a funcionalidade no banco

Primeiro configurar a tabela de funcionalidade:

INSERT INTO nfs_acl_ocultar_eventos_dinamicos_funcionalidades (NOME, INS_DH)
VALUES('INDICADORES (HOME)', '2020-06-15 00:00:00');

Após a configuração da funcionalidade de indicadores da home inserida no banco, podemos então utilizar a tela de acl ou configurar via banco, ex:

via Banco de dados para grupo de usuário:

INSERT INTO nfs_acl_grupo_ocultar_eventos_dinamicos (`EMPRESA`, `FILIAL`, `LOCAL`, `GRUPO_SEQ_DB`,
`FUNCIONALIDADE_SEQ_DB`, `INS_DH`, `ATIVO`, `DELETED`) VALUES(1, 1, 1, 1, 1, '2019-07-30 00:00:00.000', 1, 0);

via Tela do ACL:

ocultar-eventos-dinamicos-gif.gif

Private User/Usuário Restrito

Tipo de Usuário Restrito

Esta introdução aborda a nova funcionalidade que permite a restrição dos usuários 3:SUPERADMIN e 100:USERADMIN. Nós utilizamos o campo RESTRICTED na tabela nfs_acl_tipo_usuario para impor restrições a esses tipos de usuários, proporcionando um controle mais detalhado sobre a visibilidade e o acesso.

Exemplo campo tipo usuário na página de criação de usuários:

usertype.gif

Como utilizar a nova funcionalidade

Para utilizar essa funcionalidade, siga os passos abaixo:

Acesse a tabela nfs_acl_tipo_usuario. Configure o campo RESTRICTED para 1 para os tipos de usuários 3:SUPERADMIN e 100:USERADMIN. Isso irá restringir o acesso a estes tipos de usuários quando estiver na criação de usuários.

  • Exemplo de código SQL para atualizar o campo RESTRICTED:

UPDATE nfs_acl_tipo_usuario SET RESTRICTED = 1 WHERE tipo_usuario_id IN (3, 100);

[!WARNING]

Ao fazer qualquer alteração no banco de dados, sempre se certifique de que você está usando a cláusula WHERE corretamente. Fazer um UPDATE sem WHERE pode alterar todos os registros e criar problemas sérios!

[!WARNING]
Não execute UPDATE sem a cláusula WHERE!!!

Alerta sobre a importância do controle de usuários

É importante enfatizar que essa funcionalidade foi desenvolvida especificamente para controlar o acesso dos usuários 3:SUPERADMIN e 100:USERADMIN. Isso é crucial para evitar a criação inadvertida de novos usuários com acesso ao Admin Console. Mantendo o campo RESTRICTED configurado como 1 para esses usuários, podemos manter um alto nível de segurança e controle no sistema.

Introdução às Rotas Personalizadas

O Módulo Rotas Personalizadas objetiva a fácil criação e manutenção de rotas para fins específicos dentro do Sistema. Dentre esses fins, estão Funcionalidades que serão usadas por clientes específicos e/ou produtos sob demanda.

Pré-requisitos

  1. Registrar Módulo, disponibilizando-o para todos os Clientes;
  2. Criar tabela de Rotas Personalizadas, <database_do_cliente>.nfs_acl_route_custom.
CREATE TABLE nfs_acl_route_custom (
  SEQ_DB bigint(20) NOT NULL AUTO_INCREMENT,
  DISPLAY_NAME varchar(255) DEFAULT NULL,
  NAME varchar(255) DEFAULT NULL,
  TYPE varchar(255) DEFAULT NULL,
  ROUTE varchar(255) 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 AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;

Onde:

  • DISPLAY_NAME: Nome do Link;
  • NAME: Nome para identificação da Rota;
  • TYPE: Tipo da Rota (criação apenas disponível para DASH / Gestão à Vista);
  • ROUTE: Rota / Link de acesso à funcionalidade.

Disponibilizar Módulo no Sistema

O Módulo Rotas Personalizadas deve ser criado em nfs_cloud.nfs_system_modules. Esse deve ser executado apenas uma única vez!

INSERT INTO nfs_cloud.nfs_system_modules (
	INS_DH, ATIVO, DELETED, ROUTE, MODULE, DESCRIPTION
) VALUES (
	CURRENT_TIMESTAMP, 1, 0, '/custom', 'Custom', 'Personalizações'
);

Habilitar Cliente

Para habilitar o Módulo de Rotas Personalizadas é necessário vincular o módulo ao Cliente, adicionado o HOST:

INSERT INTO nfs_cloud.nfs_hosts_n_system_modules (
	INS_DH, ATIVO, DELETED, HOST, SYSTEM_MODULES
) VALUES (
	CURRENT_TIMESTAMP, 1, 0, 165, 5
);

Onde:

  • HOST: SEQ_DB do Cliente de (nfs_cloud.nfs_hosts);
  • SYSTEM_MODULES: SEQ_DB do Módulo (nfs_cloud.nfs_system_modules).

Criando Rotas Personalizadas

Uma vez habilitada para o Cliente, é necessário criar a Rota Personalizada e disponibilizá-la no Menu. Para criar uma Rota Personalizada insera-a na tabela nfs_acl_route_custom:

-- exemplo: Rota Personalizada do Painel de Agendamento de Técnico
INSERT INTO nfs_acl_route_custom (
	DISPLAY_NAME, NAME, TYPE, ROUTE
) VALUES (
	'Painel de Agendamento', 'schedule_panel', 'DASH', '/custom/panel/funcionario'
);

Feito isso, a disponibilização é feita através do Controle de Acesso em "Liberar Funcionalidades", Módulo "Personalizações".

Dicas

Como Efetuar pesquisas no banco

Por exemplo: pesquisar se algum entryPoint usa função de diffDate em todos os bancos

Para fazer uma pesquisa em todos os bancos, na tabela nfs_entry_point com uma parte da palavra que eu deseja encontrar, utilize a consulta abaixo:

select distinct 
concat('select count(*),''',ic.table_schema,''' from ',ic.table_schema,'.', ic.TABLE_NAME,' WHERE 
CODE like \'%diff(%\'
\n union \n') c
from information_schema.`TABLES` ic
where ic.TABLE_NAME = 'nfs_entry_point';

Na 3 º linha, colocar o campo e a pesquisa: CODE like \'%diff(%\' Na ultima linha, colocar o nome da tabela que deseja buscar: TABLE_NAME = 'nfs_entry_point'

clipboard_-_26_de_agosto_de_2021_às_12_00.png

Clicar com o botão direito no resultado e exportar usando Delimited text clipboard_-_26_de_agosto_de_2021_às_12_02.png

Antes de rodar, remover o c da primeira linha e o union da ultima linha clipboard_-_26_de_agosto_de_2021_às_12_05.png

O resultado dessa consulta, retorna a quantidade de registros que atende a pesquisa em cada banco clipboard_-_26_de_agosto_de_2021_às_12_06.png

Ao final: Encontre o banco que retornou o resultado e o entryPoint referente a sua pesquisa

Exemplo: Modelagem

Exemplo de como modelar um campo do apontamento enviado pelo mobile, para gravar na tabela real de apontamento no banco de dados do cliente

Mobile (xMova)

Na Aba: Model

  • Criar um novo campo: -> valor decimal
Apontamento sync=out cleanupDays=7
	id inc
	
	boletim Boletim
	seqEquipamento SeqEquipamento

	SEQ_DB_DEVICE_MASTER long autofill=boletim.SEQ_DB_DEVICE
	SEQ_DB_DEVICE long uuid
	
	data Now timeTypeField=tipoData
	flagOnline int notFill onlineFlagCreate
	location Location inlineData

	foto Binary picture
	observacao Str
	valor decimal
	server name=EQP_Apt_Foto`

WEB (NFS)

  • Configurar o campo na tabela nfs_core_par_parametros_mobile

    • Definir de onde vem (nome do campo no mobile) e para onde vai (nome do campo na tabela real)
    • Definir o TIPO
    • Definir o código do aplicativo no INSTALLCODE ou utilizar o PARAM_GROUP de acordo com a tabela app_simovaapps clipboard_-_20_de_janeiro_de_2022_às_09_07(1).png
  • Configurar o campo na Modelagem de Campos (nfs_core_ds_tabela_campo) clipboard_-_20_de_janeiro_de_2022_às_09_07.png

  • Acessar o Painel > Admin Console > DS/DDL full

    • Esta ação cria o campo na tabela real clipboard_-_20_de_janeiro_de_2022_às_09_08.png
  • Verificar se o campo foi criado na tabela real clipboard_-_20_de_janeiro_de_2022_às_09_11.png

Responsavel: Dalton (CORE)

Enviar a dúvida primeiro no grupo de desenvolvedores (DEVS). Dependendo da situação, alguém pode já ter informações que possam ser úteis. Caso contrário, se for algo que o responsável pela sustentação não tenha domínio, levará mais tempo, pois será necessário analisar o funcionamento atual para orientar ou intervir no problema. Alem disso é bom manter um histórico da resolução dos problemas, caso alguém venha a tê-los novamente no futuro, pode buscar aqui no Devs. Caso vocês perguntem e ninguém responda, iremos dar um feedback para analisarmos quando possível

Boas praticas:

Para agilizar o processo de analise, caso você esteja tendo um problema em um ambiente especifico, procure sempre passar informações sobre o ambiente/rota(qual servidor? qual base? qual url?) para que possamos analisar, o mesmo vale para screenshoots, alguns exemplos de screenshoots: ex1.png

  1. Url do local onde esta acontecendo o problema;
  2. Informações sobre qual usuario esta logado e qual/quais são as EFLs.

ex1.png

  1. Tabela atual;
  2. Banco, base e tabela.

DS

Anotações CRUD - Crud Annotations

Permite que anotações sejam adicionadas aos registros do sistema via CRUD.

Essas anotações são visíveis para todos os usuários do sistema e vinculam informações relevantes — como ideias, lembretes, tarefas e insights — diretamente a um registro.

Exemplos de anotações:

  • Maquina sem carregador de celular;
  • Aguardando técnico vistoriar RPM fictício;
  • Apontamento ajustado a pedido do fulano.
Não deve usada como registro de informações sistêmicas, follow-up operacional ou dados do cadastro em si.

Criando uma anotação

Para criar uma anotação, acesse qualquer registro do sistema por meio do CRUD. Após isso, o componente CRUD Annotations estará disponível no canto direito da tela.

Abrindo o componente de anotações em um registro CRUD

Ao clicar no ícone, será exibida uma caixa para inserção de anotações. Se já houver anotações cadastradas, o sistema exibirá aquelas que ainda não foram arquivadas. Após digitar o conteúdo desejado, clique no botão "Salvar" para registrar a anotação.

Você também pode categorizar a anotação utilizando o padrão de cores disponível no momento do cadastro.

Preenchendo e salvando uma nova anotação com categorização por cor

Estrutura de uma anotação

Estrutura de um card de anotação com legendas numeradas

Legenda:

  • 1: Mensagem da anotação;
  • 2: Autor da anotação;
  • 3: Botão "Responder";
  • 4: Botão "Arquivar";
  • 5: Mensagem da resposta;
  • 6: Autor da resposta;
  • 7: Botões de ação da resposta;
  • 8: Indicador de autor da ação na resposta;
  • 9: Indicador de autor da ação na resposta.

Respondendo uma anotação

Para interagir com uma anotação e respondê-la, basta clicar no botão "Responder" localizado na própria anotação. Em seguida, será possível registrar sua resposta e deixa-la vinculada à anotação.

Usuário clicando em responder e inserindo uma resposta à anotação

Interagindo com uma resposta de anotação

As respostas podem ser marcadas por qualquer usuário, permitindo as seguintes ações:

  • Riscar: aplica um tachado ao texto da resposta, indicando que ela foi desconsiderada ou superada.
  • Marcar como concluída: altera o fundo da resposta para sinalizar que a questão foi resolvida.

Marcando uma resposta como riscada ou concluída

Arquivando anotação

Para arquivar uma anotação, clique no botão "Arquivar" e confirme a ação no modal de confirmação que será exibido.

Uma vez arquivada, a anotação ficará disponível na aba "Arquivadas" no componente de anotações.

Processo de arquivamento de uma anotação via botão e modal de confirmação

Listando registros com anotações

Para listar os registros que possuem anotações em uma tabela, acesse a lista de registros dessa tabela por meio do CRUD.

Em seguida, abra o componente de anotações: ele exibirá todos os registros que possuem anotações não arquivadas, com a quantidade de anotações de cada um.

A borda de cada “item” será colorida conforme o tipo da anotação de maior importância associada ao registro — sendo "Alerta" a prioridade mais alta e "Padrão" a mais baixa.

Visualização dos registros com anotações e destaque por cor de prioridade

Estilo de coluna baseado em valor ou campo

Customização para Alterar Estilos nos Campos da Edição em Lote e Tela de Detalhes segundo o valor de 1 ou mais campos.

Exemplos de aplicação das novas configurações: (coluna CONFIG no nfs_core_ds_tabela_campo)

1. Duração:

{
   "customStyle":[
      {
         "conditions":[
            {
               "valueField":"INI_FIM_DIFF_STR",
               "operator":">",
               "value":"00:00:00"
            }
         ],
         "style":"color:green;font-weight:bold"
      },
      {
         "conditions":[
            {
               "valueField":"INI_FIM_DIFF_STR",
               "operator":">",
               "value":"01:00:00"
            }
         ],
         "style":"color:yellow;font-weight:bold"
      },
      {
         "conditions":[
            {
               "valueField":"INI_FIM_DIFF_STR",
               "operator":">",
               "value":"02:00:00"
            }
         ],
         "style":"color:red;font-weight:bold"
      }
   ]
}

No exemplo a configuração executa 3 validações, uma para boletins com duração maiores que 0, outra para boletins com duração maior que 1 hora e

a última para boletins com duração maior que 2 horas.

2. Valor específico:

{
   "customStyle":[
      {
         "conditions":[
            {
               "valueField":"EQUIPAMENTO",
               "operator":"=",
               "value":44774
            }
         ],
         "style":"color:blue;font-weight:bold"
      }
   ]
}

Neste caso, verificamos se o registro possui o equipamento especificado através de seu SEQ_DB.

3. Valor numérico:

{
   "customStyle":[
      {
         "conditions":[
            {
               "valueField":"MEDICAO_FIM",
               "operator":">",
               "value":0.0
            }
         ],
         "style":"color:green;font-weight:bold;background-color:#000;"
      },
      {
         "conditions":[
            {
               "valueField":"MEDICAO_FIM",
               "operator":"==",
               "value":0.0
            }
         ],
         "style":"color:red;font-weight:bold;background-color:#000;"
      }
   ]
}

Neste exemplo, verificamos se o valor da medição é positivo e mudamos o estilo para fonte verde se verdadeiro, caso contrario a fonte ficará na cor vermelha.

4. Valor dinamico

{
 "customStyle":[
      {
         "conditions":[
            {
              "valueField":"INI_DIFF_FIM_STR",
              "operator":">",
              "value":{
                "valueField":"ATIVIDADE",
                "fkField":"CODIGO",
                "fkFieldValue":"2",
                "return":"TEMPO_PREVISTO"
              }
            }
         ],
         "style":"color:green;font-weight:bold;background-color:#000;"
      },
  ]
}
PropriedadeFunção
ValueFieldDefine qual o campo que possui a tabela FK a ser filtrada
FKFieldQual o campo será utilizado no filtro
FKFieldValueValor a ser filtrado
returnCampo com o valor a ser retornado

5. Múltiplas condições:

{
   "customStyle":[
      {
         "conditions":[
            {
               "valueField":"INI_FIM_DIFF_STR",
               "operator":">",
               "value":"00:00:00"
            },
 			{
               "valueField":"GRUPO_ATIVIDADE",
               "operator":"=",
               "value":73
            }
         ],
         "style":"color:green;font-weight:bold"
      },
      {
         "conditions":[
            {
               "valueField":"INI_FIM_DIFF_STR",
               "operator":">=",
               "value":"00:59:59"
            }
         ],
         "style":"color:yellow;font-weight:bold"
      },
      {
         "conditions":[
            {
               "valueField":"INI_FIM_DIFF_STR",
               "operator":">",
               "value":"02:00:00"
            }
         ],
         "style":"color:red;font-weight:bold"
      }
   ]
}

Capturas de tela

batch details

Field Action

Como Funciona

field_action_title.png

A funcionalidade Field Action (Botão de Ação) foi desenvolvido como um plugin que pode ser adicionado facilmente em diversas telas, porém ainda é necessário um desenvolvimento da parte core algo que não deve levar muito tempo, hoje é possível configurar para:

  • Qualquer CRUD
  • Página de Detalhes do Boletim -- Topo do Detalhes -- Para cada Boletim

Para configurar um field action é necessário da tabela nfs_core_ds_field_action

CREATE TABLE `nfs_core_ds_field_action` (
  `SEQ_DB` bigint(20) NOT NULL AUTO_INCREMENT,
  `TABELA` varchar(250) DEFAULT NULL COMMENT 'Nome da Tabela',
  `DESCRICAO` varchar(300) DEFAULT NULL COMMENT 'Descrição da Ação',
  `CONFIG` text COMMENT 'Configuração do elemento',
  `INS_DH` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `ATIVO` int(11) DEFAULT '1',
  `DELETED` int(11) DEFAULT '0',
  `NAME` varchar(250) DEFAULT NULL COMMENT 'Um nome único para uma ação',
  PRIMARY KEY (`SEQ_DB`),
  KEY `FK_DS_TABELA` (`TABELA`),
  CONSTRAINT `FK_DS_TABELA` FOREIGN KEY (`TABELA`) REFERENCES `nfs_core_ds_tabela` (`NOME`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Campos

Serão listados só os campos mais importantes.

  • TABELA: Quando for uma configuração para o CRUD esse campo terá que ser preenchido com a tabela desejada.
  • DESCRIÇÃO: É uma descrição do seu botão de ação aconselho sempre a preencher para qualquer dev que for usar ou verificar para que sever o botão.
  • CONFIG: É um JSON de configuração, será explicado mais abaixo.
  • NAME: Usado para ações que não estão no CRUD, logo não tem uma tabela para ser configurada, é definida pelo core e será descrito logo abaixo.

Name

NAMEDescriçãoTela
DETAILSSerá exibido no topo da tela de DetalhesTela de Detalhes
DETAILS_SECONDARYSerá exibido para cada Boletim do DetalhesTela de Detalhes

Config

CampoValorDefinição
idLivreId do Elemento HTML
textLivreTexto do Botão
icon_classLista de ÍconeVer o HTML do ícone na lista do métronic, ex.: fa fa-cloud-upload
button_classBotõesPode ser mais de uma classe.
typelink ou buttonlink envia para outra página, button executa uma ação por meio de um entry point
urllivreEndereço interno usar o pathname, ex:. https://smartos.simova.cloud/t/os/edit/{{seqDb}}, no caso pathname é /t/os/edit/ caso na tela tenha o seqDb para passar como parâmetro use {{}} chaves duplas, como resultado "url":"t/os/edit/{{seqDb}}"
entryPointNumberSomente usado quando o type igual a buttonNúmero do entry point que foi criado como parâmetro é passado todas as variáveis do pageVars e a tabela deve ser FIELD_ACTION_SERVICE
reloadtrue ou falseSe não for passada a chave, por padrão reinicia a página após a ação do button.

[!NOTE] Explicação de alguns parametros da URL para rotas de Tabela/CRUD(/t/):

  • p1 (parametro 1) : Especifica o tipo de consulta que será aplicada utilizando o comparador(c1).
    • nomeDaColuna: Define a coluna na qual a condição será aplicada;
    • sql_condition:
  • v1 (valor 1): Define qual a ser comparado na consulta;
  • c1 (condicao 1): condição a ser utilizada na consulta do SQL, seu valor deve ser um destes:
    • neq (not equal, operador ''<>'');
    • lt (less than, operador ''<'');
    • lte (less than or equals, operador ''<='');
    • gt (greater than, operador ''>'');
    • gte (greater than or equals, operador ">=");
    • eq (equals, operador "=").
  • vlr(valor de referencia);
  • integration: indica que a ação executada é integração e com isso traz os checkboxes na lista do CRUD onde foi executada a ação.

Exemplos

Botão de Integração na tela de OS

INSERT INTO nfs_core_ds_field_action (`TABELA`, `DESCRICAO`, `CONFIG`,  `INS_DH`, `ATIVO`, `DELETED`, `NAME`) VALUES('OS', 'Ação de Integração de OS', '{
    "url": "/t/os?p1=proc_st&v1=4&c1=eq&v1r=Selecione a(s) OS(s) que já foram integradas e serão liberadas para novo processo de integração&integration=1&desc=''Tese''",
    "text": "Integração",
    "icon_class": "fa fa-eject",
    "button_class": "btn-lg btn-default ajaxify tooltips",
    "tooltip":"Liberar os para integração"
}', '2019-07-30 18:42:56.000', 1, 0, NULL);

Botão para Criar no Boletim na tela de Detalhes

INSERT INTO nfs_core_ds_field_action (`TABELA`, `DESCRICAO`, `CONFIG`, `INS_DH`, `ATIVO`, `DELETED`, `NAME`) VALUES(NULL, 'Criar Novo Details', '{
      "id": "novo-details",
      "text": "Novo Details",
      "icon_class": "fa fa-plus",
      "url" : "/details/new/{{seqDb}}/{{seqQii}}/{{seqPanel}}?popup=1",
      "button_class": "btn-primary"

}', '2019-07-30 18:42:56.000', 1, 0, 'DETAILS');

Botão na tela de Detalhes para Ir para Relatório de Assistência Técnica

INSERT INTO nfs_core_ds_field_action (`TABELA`, `DESCRICAO`, `CONFIG`, `INS_DH`, `ATIVO`, `DELETED`, `NAME`) VALUES(NULL, 'Details Relatórios', '{
      "text": "Assis. Técnica",
      "icon_class": "fa fa-plus",
      "url" : "/reports/assistencia_tecnica/filters",
      "button_class": "btn-success"
}', '2019-07-30 18:42:56.000', 1, 0, 'DETAILS');

Botão na tela de Detalhes dentro de cada Boletim que executa um Entry Point

[!WARNING] Na tela de Detalhes tem a variável $data e a chave value sempre é o seq_db do boletim $data['value']. Para saber todas as variável possíveis use o NfsLogger::error("{$data}", 'METHOD_DATE'); Com o SEQ_DB do boletim é possível fazer o que quiser e obter a maioria das informações necessárias para manipular os dados.

INSERT INTO nfs_core_ds_field_action (`TABELA`, `DESCRICAO`, `CONFIG`,`INS_DH`, `ATIVO`, `DELETED`, `NAME`) VALUES(NULL, 'Exibe botão na secondary table', '{
    "id": "integration_btn_sec",
  	"text" : "Integrar",
  	"icon_class" : "fa fa-cloud-upload",
  	"button_class" : "btn-primary",
  	"type": "button",
  	"url" : "",
  	"entryPointNumber" : "550"
}', '2019-07-30 18:42:56.000', 1, 0, 'DETAILS_SECONDARY');

Entry Point 550:

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`) VALUES('SYSTEM', NULL, NULL, NULL, 9999, 9999, 9999, 'FIELD_ACTION_SERVICE', 550, '$this->code[''msg''] = "TEste Entry Point - Qii: {$data[''seqQii'']}
seq db boletim: {$data[''value'']}
";
$this->code[''type''] = "success";', 1, 12, 1, 'PHP', NULL, NULL, '2018-10-02 09:28:59.000', '2019-08-19 16:28:38.000');

Sempre retorna para a tela as variáveis msg é uma mensagem que será exibida e type pode ser encontrado aqui.

Adicionar Botão de Ação no Relatório de Indicadores

Nesse caso de ação a tela do Relatório é recarregada após a ação.

Detalhes importante do JSON abaixo é a chave fieldAction dentro da chave report, nela é passado o nome da coluna que vai exibir e o NAME do field action.

{
  "osIntegracao": {
    "label": "OS's com Faturamento liberado",
    "color": "",
    "icon": "fa fa-signal",
    "display": "[result]",
    "interval": "120000",
    "decimalPlace": "2",
    "queryBuilder": {
      "osIntegracao": {
        "select": [
          "count(os.seq_db) result"
        ],
        "from": "os os",
        "inner_join": [
          [
            "os",
            "status_os",
            "st",
            "st.SEQ_DB = os.STATUS_OS_SEQ_DB"
          ],
          [
            "os",
            "cliente",
            "c",
            "c.SEQ_DB = os.CLIENTE_SEQ_DB"
          ]
        ],
        "where": [
          "st.DESCRICAO in ('Liberada Faturamento')"
        ]
      }
    },
    "queryBuilderData": {
      "osIntegracao": {
        "select": [
          "os.seq_db Integrar",
          "os.codigo Codigo",
          "c.DESCRICAO Cliente",
          "st.DESCRICAO Status"
        ],
        "from": "os os",
        "inner_join": [
          [
            "os",
            "status_os",
            "st",
            "st.SEQ_DB = os.STATUS_OS_SEQ_DB"
          ],
          [
            "os",
            "cliente",
            "c",
            "c.SEQ_DB = os.CLIENTE_SEQ_DB"
          ]
        ],
        "where": [
          "st.DESCRICAO in ('Liberada Faturamento')"
        ]
      }
    },
    "report": {
      "displayColumns": [
        "Codigo",
        "Cliente",
        "Status",
        "Integrar"
      ],
      "links": {
        "Codigo": "/t/os/%Integrar%"
      },
      "fieldAction":{
        "Integrar" : "INDICATOR_FAT"
      }
    }
  }
}
  • Field Action com NAME igual a INDICATOR_FAT
INSERT INTO nfs_core_ds_field_action (`TABELA`, `DESCRICAO`, `CONFIG`, `INS_DH`, `ATIVO`, `DELETED`, `NAME`) VALUES(NULL, 'Exibe botão no indicator report', '{
    "id": "integration_btn_sec",
  	"text" : "",
  	"icon_class" : "fa fa-cloud-upload",
  	"button_class" : "btn-primary",
  	"type": "button",
  	"url" : "",
  	"entryPointNumber" : "550"
}', '2019-07-30 18:42:56.000', 1, 0, 'INDICATOR_FAT');

Entry Point 550:

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`) VALUES('SYSTEM', NULL, NULL, NULL, 9999, 9999, 9999, 'FIELD_ACTION_SERVICE', 550, 'ConexaoDB::execute("update app_os set status_os_seq_db = 3 where seq_db = {$data[''value'']} "); 

$this->code[''msg''] = "TEste Entry Point - Qii: {$data[''seqQii'']}
painel: {$data[''value'']}
";
$this->code[''type''] = "success";', 1, 12, 1, 'PHP', NULL, NULL, '2018-10-02 09:28:59.000', '2019-08-21 23:23:18.000');

Sempre retorna para a tela as variáveis msg é uma mensagem que será exibida e type pode ser encontrado aqui.

Controle de Acesso Field Action

Acesse aqui para entender como funciona a configuração do controle de acesso para o Field Action.

Filtros de Data

Botão adicionado aos filtros do CRUD com sugestões de ranges de datas. Além dos atalhos também são exibidas as últimas datas filtradas, com um máximo de 5 datas que ficam armazenadas no storage do navegador.

As telas que receberam o botão no filtro foram: /t/<table> e /details.

1.png2.png

Inputs


Nos formulários, os campos de data (DATE, DATA, DH, DHS) receberam um botão que irá memorizar o valor atual do input para que o mesmo seja utilizado posteriormente. E o botão que irá exibir os atalhos padrão e personalizados.

O armazenamento é por campo e automático, atingindo o limite o último salvo é descartado, por padrão o limite é 2 mas pode ser personalizado via configuração.

5.png3.png4.png

Config (inputs)

É possível criar uma configuração para personalizar esse recurso, basta criar um novo registro na tabela nfs_core_par_parametros com o NOME CRUD_DATE_SHORTCUTS.

O conteúdo será um JSON a seguinte estrutura:

{
   "DEFAULT_OPTIONS":"disabled",
   "MAX_MEMO":3,
   "DH":{
      "1":{
         "dia":"1",
         "hora":"current",
         "label":"D +1"
      },
      "2":{
         "dia":"2",
         "hora":"current",
         "label":"D +2"
      },
      "3":{
         "dia":"1",
         "hora":"00:00:00",
         "label":"D +1 00h00"
      },
      "4":{
         "dia":"1",
         "hora":"23:59:59",
         "label":"D +1 23h59"
      },
      "5":{
         "dia":"2",
         "hora":"00:00:00",
         "label":"D +2 00h00"
      },
      "6":{
         "dia":"-5",
         "hora":"12:00:00",
         "label":"D -5 12h00"
      }
   },
   "DATA":{
      "1":{
         "dia":"1",
         "label":"D +1"
      },
      "2":{
         "dia":"2",
         "label":"D +2"
      },
      "3":{
         "dia":"-5",
         "label":"D -5"
      }
   }
}

"DEFAULT_OPTIONS":"disabled": Desativa os atalhos padrões. "MAX_MEMO":3": Define a quantidade de datas que podem ser salvas na memória. "DH": {}: Recebe os atalhos personalizados de campos do tipo DH e DHS. "DATA": {}: Recebe os atalhos personalizados de campos do tipo DATA e DATE.

Objeto DH:

      "1":{
         "dia":"10",
         "hora":"current",
         "label":"D +10"
      },
      "2":{
         "dia":"-1",
         "hora":"07:00:00",
         "label":"D -10"
      },

dia: Espera um numero inteiro negativo ou positivo, ele define o parametro de dias para trás ou para frente com relação a data atual, ou zero para a data atual. hora: Espera "current" para obter a hora atual, ou um valor 'HH:MM:SS'. label: A descrição que será apresentada no botão.

Objeto DATA:

      "1":{
         "dia":"5",
         "label":"D +5"
      },
      "2":{
         "dia":"-5",
         "label":"D -5"
      },

dia: Espera um numero inteiro negativo ou positivo, ele define o parametro de dias para trás ou para frente com relação a data atual, ou zero para a data atual. label: A descrição que será apresentada no botão.

Exemplo


{
   "DEFAULT_OPTIONS":"disabled",
   "MAX_MEMO":3,
   "DH":{
      "1":{
         "dia":"1",
         "hora":"current",
         "label":"D +1"
      },
      "2":{
         "dia":"2",
         "hora":"current",
         "label":"D +2"
      },
      "3":{
         "dia":"1",
         "hora":"00:00:00",
         "label":"D +1 00h00"
      },
      "4":{
         "dia":"1",
         "hora":"23:59:59",
         "label":"D +1 23h59"
      },
      "5":{
         "dia":"2",
         "hora":"00:00:00",
         "label":"D +2 00h00"
      },
      "6":{
         "dia":"-5",
         "hora":"12:00:00",
         "label":"D -5 12h00"
      }
   },
   "DATA":{
      "1":{
         "dia":"1",
         "label":"D +1"
      },
      "2":{
         "dia":"2",
         "label":"D +2"
      },
      "3":{
         "dia":"-5",
         "label":"D -5"
      }
   }
}

nfs_core_par_parametros

Possíveis valores e decrição dos Parametros que podem ser configurados na tabela nfs_core_par_parametros.

USAR_PAINEL_ANTIGO - Usar painel de gestão à vista antigo

Foram feitas melhorias visuais no painel de gestão à vista, para deixar de usar o antigo e somente habilitar o novo é necessário que a coluna CONTEUDO desse parâmetro esteja com valor 0, por padrão é 1 para manter a compatibilidade junto aos clientes.

Quando o parâmetro estiver habilitado vai mostrar um botão para o novo painel, porém, se não vai mostrar o novo painel sem um link para o antigo.

Painel Antigo

painel_antigo.png

Painel Novo

painel_novo.png

Script SQL

Caso o seu ambiente não tenha o parâmetro:

INSERT INTO nfs_core_par_parametros (EMPRESA, FILIAL, `LOCAL`, NOME, CONTEUDO, TIPO) VALUES(9999, 9999, 9999, 'USAR_PAINEL_ANTIGO', '1', 1);

Parametros para documentar

  • ACESSO_POR_TOKEN
  • ASSOCIATION_TASK_OPER
  • ASSOCIATION_TASK_OPER_GRUPO
  • ASSOCIATION_TASK_OPER_GRUPO_METRICA
  • ASSOCIATION_TASK_OPER_OPER_GRUPO
  • CALCULO_DATA
  • CORE_SIMOVAAPPS_LIST
  • CRUD_EXTENSOES_PERMITIDAS_UPLOAD
  • CRUD_EXTENSOES_UPLOAD_PERMITIDAS_VISUALIZACAO
  • CRUD_LIST_IMAGE_GALLERY_LIMIT
  • CRUD_LIST_LOVN_LINE_BREAK
  • CRUD_OPCOES_BOOL
  • CRUD_REFRESH_AFTER_INSERT_UPDATE
  • CRUD_TAMANHO_MAXIMO_UPLOAD
  • DETAILS_MODE
  • EMAIL_REMETENTE
  • EQP_VEIC_MENSAGEM_STATUS_MENSAGEM
  • EQP_VEIC_MENSAGEM_TIPO_MENSAGEM
  • EXCEPTION_SPLIT_ENTITY
  • EXIBICAO_CAMPOS_COM_LIGACAO
  • EXIBICAO_DATA_BOLETIM
  • GIS_DEFAULT_POSITION
  • GIS_ENABLED
  • GIS_IMPORT_ENABLED
  • GIS_TRACKING_QUANTITY
  • HEADER_ON_CRUD_PDF_EXPORT
  • HOME_QUICKACCESS_OPTIONS
  • HOME_QUICKACCESS_OPTIONS_1
  • HOME_QUICKACCESS_OPTIONS_2
  • JORNADA_TRABALHO
  • LIMIT_TEMP_RELATORIO
  • MAQUINA_JORNADA_TRABALHO
  • MARTE_ENABLED
  • MARTE_MAP_TRACKING_SEGMENT_BREAK_SECONDS
  • MARTE_OTA_UDP_PORT
  • NOME_PRODUTO
  • OPCAO_DATA_1_DIA
  • OPCAO_DATA_2_DIAS
  • OPCAO_DATA_AGORA
  • OPCAO_DATA_HOJE
  • OPCAO_DATA_ONTEM
  • OS_TABLE_NAME
  • RESIZE_IMG
  • SIMOVA_ALERTS_APP_KEY
  • SMS_ALLCANCE_CONFIG
  • SMS_ZENVIA_CONFIG
  • SPLIT_ENABLED
  • SUPERFICIES_FORMA
  • TABELAS_AJUSTE_PONTO
  • THIRD_PARTY_ENABLED
  • USAR_PAINEL_ANTIGO
  • WORKBOARD_GRID
  • WORKSHIFT_AUTO_ENABLED
  • WORKSHIFT_MO_AUTO_ENABLED

DS Tabela

Documentação em desenvolvimento { .is-warning }

Sobre

Para criar as tabelas no sistema sem que seja necessario a execução do SQL diretamento no banco, iremos inserir a estrutura da tabela nos padrões do NFS na tabela NFS_CORE_DS_TABELA e assim poderemos vincular os campos através do registros na tabela NFS_CORE_DS_TABELA_CAMPO

Estrutura

NOME

Nome da tabela, exemplo:

  • EQP {.grid-list}

NOME REAL

Nome da tabela no banco, geralmente é o prefixo APP+NOME, exemplo:

  • APP_EQP {.grid-list}

DESCRICAO

Descrição da tabela, exemplo:

  • Equipamentos {.grid-list}

UNQ

Campo unico da tabela, exemplo:

  • ID {.grid-list}

DISPLAY

Campos da tabela que serão utilizados para exibição dos registros, exemplo:

  • DESCRICAO
  • CODIGO
  • CODIGO,DESCRICAO {.grid-list}

TIPO

  • 1: Padrão
  • 2: Tabela N
  • 3: Somente Leitura
  • 5: View {.grid-list}

TIPO_MOBILE

  • 1: Apontamento
  • 2: Boletim {.grid-list}

Opções

ID

As tabelas criadas pelo DS em sua maioria possuem o campo ID por padrão, podemos customizar o comportamento desse campo utilizando passando um json de configuração no campo OPTIONS, seguem algumas dessa configurações:

  • id-control: Usado para definir o comportamento do campo IDao inserir registros na tabela, pode ser "fixed"(com um valor statico que não muda) ou caso não seja definido, por padrão, ficará incremental de acordo com empresa/filial/local.
{"id-control":"fixed"}
  • id-value: Usado em conjunto ao id-control = fixed, define o valor padrão que será utilizado como o valor fixo de ID(o valor padrão é 9999).
{"id-control":"fixed"}
{"id-value":9999}

Campo ID fixo

Adicionando o JSON abaixo aos valores de OPTIONS da TABELA fará com que os comandos de INSERT não façam o "select max(id) + 1" que é executado quando o valor informado do ID está em branco ou 0. Recomenda-se ativar essa opção para TODAS as tabelas MOBILE ganhando desempenho no processo de insert

REMOVER MANUALMENTE DA TABELA A PK de EMPRESA+FILIAL+LOCAL+ID
ALTER TABLE app_XXXXXXXXXX DROP PRIMARY KEY;
{
  "id-control": "fixed",
  "id-value": 9999
}

ORDER_BY - Controle de ordenção das tabelas

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

Por padrão, as telas de CRUD de demais consultas via TabelaBO usam a configuração de NFS_CORE_DS_TABELA.DISPLAY_FIELDS como ordenação.

Para utilizar uma ordenação diferente é possível usar o campo NFS_CORE_DS_TABELA.ORDER_BY informando nesta os campos desejados para ordenação, exemplo: FRENTE ASC,CODIGO ASC

E, pode ir além, caso queira enviar para o XMOVA uma ordenação específica, seja pela chamada via tabela direta (server name no xmova está igual tabela do NFS - exemplo: server name=EQP_CLASSE) ou por entryPoint do tipo SQL (exemplo EqpClasseMobile) basta mudar esse campo para um formato JSON contendo duas chaves: CRUD e MOBILE, conforme abaixo:

{
  "CRUD": "DESCRICAO ASC",
  "MOBILE": "SEQ_DB ASC"
}

BOTÃO PRÓXIMA PÁGINA - Controle de Navegação entre Registros na Tela de Edição

BOTÃO PRÓXIMA PÁGINA - Controle de Navegação entre Registros na Tela de Edição CRUD Este documento descreve o funcionamento e a configuração do botão "Próxima Página" na tela de edição de CRUD e demais consultas via TabelaBO, utilizando a configuração ORDER_BY para determinar a ordenação dos registros.

Pré-requisitos

Antes de utilizar o botão "Próxima Página", certifique-se de que os seguintes pré-requisitos foram atendidos:

As telas de edição de CRUD e demais consultas via TabelaBO utilizam, por padrão, a configuração de ordenação definida em NFS_CORE_DS_TABELA.DISPLAY_FIELDS.

Para personalizar a ordenação, é possível utilizar o campo NFS_CORE_DS_TABELA.ORDER_BY. Este campo aceita uma lista de campos separados por vírgulas, indicando a ordem desejada. Exemplo: "FRENTE ASC, CODIGO ASC".

Para uma ordenação específica no contexto do XMOVA, seja pela chamada via tabela direta ou pelo entryPoint do tipo SQL, você pode modificar o campo NFS_CORE_DS_TABELA.ORDER_BY para um formato JSON com as chaves "CRUD" e

Configuração do Botão "Próxima Página"

Para ativar o botão "Próxima Página" e permitir a navegação entre registros durante a edição do CRUD, inclua a seguinte configuração na coluna JSON da tabela nfs_core_ds_tabela:

Exemplo de JSON na coluna:

{
  "button_next": true
}

Com a configuração acima, o botão "Próxima Página" será habilitado na tela de edição de CRUD e permitirá que o usuário navegue para o próximo registro com base na ordenação definida em NFS_CORE_DS_TABELA.ORDER_BY.

OPTIONS para tabelas MOBILE

Bloqueio de inserções/alterações relacionadas a um boletim

Para isso foi criado uma nova opção que pode ser inserida na tabela de boletim(nfs_core_ds_tabela), que faz que a tabela de boletim não possa receber inserções manuais(via crud ou edição em lote) por um periodo a partir do INI_DH do boletim(por padrão, por 24horas a partir do inicio do boletem), configuração:

  • Acessar a tabela nfs_core_ds_tabela;
  • Iserir a opção tmpLock nas opção disponíveis na coluna OPTIONS da tabela de boletim:
{
	"menu_name":"Boletim Máquina", 
	"favorite": true,
	"ignore_validation_tables": ["APONTAMENTO_MAQUINA_INTERMEDIARIA"],
    "tmpLock": {
      "enabled": true,
      "period": 24
    }
}

exemplo de OPTION da tabela BOLETIM_MAQUINA

Campos Herdados da Tabela Master

Campos herdados são campos que podem ser configurados em uma tabela mobile do tipo 1 (APONTAMENTO ) para que tenham seu valor definido com base em sua tabela master(BOLETIM)

Configurações:

As configurações devem ser feitas na coluna OPTIONS da tabela nfs_core_ds_tabela, no exemplo a seguir foi utilizada a tabela APONTAMENTO_MAQUINA:

{
    "inherit": {
        "override":true,
        "fields": {
            "EQP_SEQ_DB":"EQP_SEQ_DB"
        }
    }
}
  • inherit: Nome da Configuração
  • override: Se os valores dos campos pela tabela de apontamento vão ser sempre sobrescritos pelos valores do BOLETIM - true ou false
  • fields: Campos que vão ser herdados, "CAMPO_APT": "CAMPO_BOLETIM"

Mudança do INI_DH do apontamento de acordo com o INI_DH do boletim

Para sincronizar o valor do INI_DH do primeiro APONTAMENTO de acordo com o INI_DH de seu boletim, podemos utilizar o parametro SYNC_FIRST_INI_WITH_END_TABLE na configuração do calculo de data(nfs_core_par_parametros).

{
 "calc1":{
      "TABLES":[
         {
            "NAME":"APONTAMENTO_MAQUINA",
            "GROUP":"EQUIPAMENTO_SEQ_DB",
            "SYNC_FIRST_INI_WITH_END_TABLE":true,
            "FILTER":""
         }
      ],
      "END_TABLE":"BOLETIM_MAQUINA"
   }
}

Campo TIPO PROC_ST

Exemplo de uso do campo do tipo PROC_ST: (usado para controles de processamento ou integração)

Dica de boa prática: Separar cada campo para uma responsabilidade. Por exemplo, se existe algum processo de integração então pode-se criar um campo INTEGRACAO_ST para esse controle.

Criando um campo PROC_ST:

INSERT INTO nfs_core_ds_tabela_campo (TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES) VALUES('EQP_BOLETIM', 'TESTE', 100, 0, 1, 1, 1, NULL, NULL, 'PROC_ST', NULL, NULL, NULL, NULL, 'IU', '0', NULL, NULL, NULL, '0:Pendente;1:Liberado Int.;4:Integrado;9:Processado ERP', NULL, NULL, NULL, NULL, NULL);

Quando rodar o DS serão gerados 3 campos:

DDL => ALTER TABLE APP_EQP_BOLETIM ADD COLUMN TESTE_ST CHAR(1) NULL DEFAULT '0';
DDL => ALTER TABLE APP_EQP_BOLETIM ADD COLUMN TESTE_DH TIMESTAMP NULL DEFAULT NULL;
DDL => ALTER TABLE APP_EQP_BOLETIM ADD COLUMN TESTE_DESC VARCHAR(250) NULL DEFAULT NULL;

Veja que no campo OPTIONS foi usada uma declaração de campo do tipo LOV1, que será usada no campo TESTE_ST. Exemplo: '0:Pendente;1:Liberado Int.;4:Integrado;9:Processado ERP'

O campo Descrição e Descrição Resumida ser usado e será adicionado ao valor padrão.

Se precisar mudar o texto completo ou alguma outra propriedade, pode-se usar o setFields na declaração na nfs_core_ds_tabela.

Usando o setFields para manipular campos e suas propriedades:

Exemplo:

	"setFields": {
          "PROC_ST": {
              "SYS": 0,
				"DESCRICAO":"Teste de gravação",
				"TIPO":"LOV1",
				"LOV":{"0":"Pendente","1":"Liberado Int.","4":"Integrado","9":"Processado ERP"}
          },
          "PROC_DESC": {
              "SYS": 0,
			"DISABLED":1
          },
          "ID": {
				"DESCRICAO":"ID DO BOLETIM"
          }
     }

Vídeo de exemplo:

Tabela Campo

[!WARNING] ATENÇÃO Documento incompleto!

Tipos de campo

Um campo do tipo LINK pode receber algumas opções, como:

CRUD: Abre um crud filtrado pelo p1 com o v1, podendo ser em uma nova janela ou não.

'OPCOES' => 'type:CRUD;popup:1;crud:MARTE_REPORT_AX;p1:MARTE;v1:SEQ_DB;icon:fa fa-tags',

O tipo GET_URL, por padrão, abre a url informada.

'OPCOES' => 'type:GET_URL;url:{url_desejada};p1:SEQ_DB;v1:SEQ_DB;icon:fa fa-cloud',

É possível abrir uma url baseada no retorno de um entryPoint.

'OPCOES' => 'type:GET_URL;url:reposts/entryPoint/{entry_point_action};p1:SEQ_DB;v1:SEQ_DB;icon:fa fa-link',

O retorno do entryPoint deve seguir o expresso abaixo:

$this->outputValues['report'] = $link_relatorio;
// se for multiple
$this->outputValues['report_filters'] = ['inspecao' => [$inspecao_seq_db]];
// senao for multiple
$this->outputValues['report_filters'] = ['inspecao' => $inspecao_seq_db];

Caso não queira abrir em nova janela, informar popup:0 e então será chamada a url por AJAX.

Nesse tipo também é possível mudar a URL, sem ser por AJAX, causando um redirecionamento da página, para isso, informar ajaxify:0. Exemplo para fazer do download de um item de telemetria:

'OPCOES' => 'type:GET_URL;popup:0;ajaxify:0;url:marte/telematic/download/0;p1:SEQ_DB;v1:SEQ_DB;icon:fa fa-cloud',

Código no core:

        $ajaxify = 'ajaxify';
        $newWindow = true;
        if (isset($options['popup']) && $options['popup'] == 0) {
            $newWindow = false;
            if (isset($options['ajaxify']) && $options['ajaxify'] == 0) {
                $ajaxify = '';
            }    
        }

Com o uso do GET_URL, é possível configurar uma url que permite realizar o download de um relatório em formato pdf.

'OPCOES' => 'type:GET_URL;popup:0;ajaxify:0;url:reports/assistencia_tecnica;p1:format=pdf&pdfAction=download&&form[os][];v1:SEQ_DB;icon:fa fa-file-text-o',

Conforme exemplo anterior, é necessário utilizar popup:0 e ajaxify para evitar que uma nova janela sem conteúdo seja aberta, e, adicionar format=pdf e pdfAction=download no início da QueryParam 'p1', de modo a evitar erro de concatenação com o valor de 'v1' definido.

Outros tipos (falta documentar):

switch ($options['type']) {
    case 'CLONE':
        return static::linkCloneRow($action, $row);
    case 'GET_URL':
        return static::linkGetUrl($action, $row);
    case 'POST_URL':
        return static::linkPostUrl($action, $row);
    case 'GIS':
        return static::linkGis($action, $row);
    case 'CRUD':
    default:
        return static::linkCrud($action, $row);
}

PROC_ST

Exemplo de uso do campo do tipo PROC_ST: (usado para controles de processamento ou integração)

Dica de boa prática: Separar cada campo para uma responsabilidade. Por exemplo, se existe algum processo de integração então pode-se criar um campo INTEGRACAO_ST para esse controle.

Criando um campo PROC_ST:

INSERT INTO nfs_core_ds_tabela_campo (TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES) VALUES('EQP_BOLETIM', 'TESTE', 100, 0, 1, 1, 1, NULL, NULL, 'PROC_ST', NULL, NULL, NULL, NULL, 'IU', '0', NULL, NULL, NULL, '0:Pendente;1:Liberado Int.;4:Integrado;9:Processado ERP', NULL, NULL, NULL, NULL, NULL);

Quando rodar o DS serão gerados 3 campos:

DDL => ALTER TABLE APP_EQP_BOLETIM ADD COLUMN TESTE_ST CHAR(1) NULL DEFAULT '0';
DDL => ALTER TABLE APP_EQP_BOLETIM ADD COLUMN TESTE_DH TIMESTAMP NULL DEFAULT NULL;
DDL => ALTER TABLE APP_EQP_BOLETIM ADD COLUMN TESTE_DESC VARCHAR(250) NULL DEFAULT NULL;

Veja que no campo OPTIONS foi usada uma declaração de campo do tipo LOV1, que será usada no campo TESTE_ST. Exemplo: '0:Pendente;1:Liberado Int.;4:Integrado;9:Processado ERP'

O campo Descrição e Descrição Resumida ser usado e será adicionado ao valor padrão.

Se precisar mudar o texto completo ou alguma outra propriedade, pode-se usar o setFields na declaração na nfs_core_ds_tabela.

VALIDACAO

FIltra os dados de uma uma tabela fk(especificada como link), exemplo:

{
  "filterLink": [{
    "condition": [{
      "field": "RO",
      "operator": "=",
      "value": "0",
      "comparator": "AND"
    },
    {
      "field": "FLAG_PRODUCAO",
      "value": "1",
      "operator": "=",
      "comparator": ""
    }]
  }]
}

ira adicionar a condição ao filtrar por registros da FK: RO = 0 AND FLAG_PRODUCAO = 1

VALIDACAO_VIEW

A validação view é muito usada para exibir ou não um campo.

TABELA_NOMENOMEDESCRICAOTIPOVALIDACAO_VIEW
ITEM_INSPECAOFLAG_FERRAMENTA_MEDICAOUsa Ferramenta de Medição?BOOL
ITEM_INSPECAOFLAG_ROLETEÉ Rolete Inferior?BOOLFLAG_FERRAMENTA_MEDICAO:1

No caso acima o checkbox FLAG_ROLETE (É Rolete Inferior?) somente será exibido quando a FLAG_FERRAMENTA_MEDICAO (Usa Ferramenta de Medição?) for verdadeira, ou em outra palavras, marcada como SIM.

Logo também é verdade que qualquer VALIDACAO_VIEW que depende da FLAG_ROLETE será ou não exibida baseada na sua exibição, exemplo

TABELA_NOMENOMEDESCRICAOTIPOVALIDACAO_VIEW
ITEM_INSPECAOFLAG_FERRAMENTA_MEDICAOUsa Ferramenta de Medição?BOOL
ITEM_INSPECAOFLAG_ROLETEÉ Rolete Inferior?BOOLFLAG_FERRAMENTA_MEDICAO:1
ITEM_INSPECAOMINIMOMínimoDECIMALFLAG_ROLETE:0
ITEM_INSPECAORODANTE_FERRAMENTA_MEDICAOFerramenta de Medição (Tabela de Conversão)LOVNFLAG_ROLETE:0
ITEM_INSPECAOSTANDERStanderDECIMALFLAG_ROLETE:0

Então quando a FLAG_ROLETE não ser exibida baseado no valor falso da FLAG_FERRAMENTA_MEDICAO os campos MINIMO (Mínimo), RODANTE_FERRAMENTA_MEDICAO (Ferramenta de Medição (Tabela de Conversão)) e STANDER (Stander) também não são exibidos. Caso seja necessário desabilitar essa hierarquia tem que seguir os passos da VALIDACAO_VIEW sem validar seus dependentes na sua hierarquia.

VALIDACAO_VIEW sem validar seus dependentes na sua hierarquia

Para não usar essa hierarquia basta adicionar na coluna PROPERTIES da nfs_core_ds_tabela_campo a chave no json { "tree_validation": "false" }

Exemplo

TABELA_NOMENOMEDESCRICAOTIPOVALIDACAO_VIEWPROPERTIES
ITEM_INSPECAOFLAG_ROLETEÉ Rolete Inferior?BOOLFLAG_FERRAMENTA_MEDICAO:1{¶ "tree_validation": "false"¶}

Com isso as VALIDACAO_VIEW que dependem da FLAG_ROLETE são independetes dela ser exibida ou não.

FILTRO VIEW

Traz os registros do campo baseados no valor de outro campo.

FK

Filtro baseado em duas FKs

Filtra os registros de um campo, baseado no valor de outro campo do qual o mesmo possua relacionamento. Possuii a seguinte estrutura:

TABELAA:TABELAB

Exemplo:

  • Tenho a tabela OS contendo os campos FK CLIENTE e EQUIPAMENTO;
  • Quero que apenas os registros de EQUIPAMENTO que possuem relação com o CLIENTE definido na OS estejam disponiveis para seleção:
TABELA_NOMENOMELINKFILTRO_VIEWTIPO
OSCLIENTECLIENTE-FK
OSEQUIPAMENTOEQUIPAMENTOCLIENTE:EQUIPAMENTOFK

[!NOTE] Neste caso o sistema irá procurar na tabela EQUIPAMENTO pelo campo FK relacionado ao CLIENTE e filtrará os registros de acordo com o CLIENTE definido na OS

Tambem podemos definir o filtro para campos que possuem a tabelaA como link, como no caso de precisar filtrar o CLIENTE baseado no baseado no valor de AGENDAMENTO_SERVICO, usando o filtro CLIENTE:AGENDAMENTO_SERVICO

TABELA_NOMENOMELINKFILTRO_VIEWTIPO
OSAGENDAMENTO_SERVICOAGENDAMENTO_SERVICO-FK
OSCLIENTECLIENTECLIENTE:AGENDAMENTO_SERVICOFK

[!WARNING] Note que A TabelaB sempre sera utilizada como a tabela que possui vinculo entre as FKs

Filtro baseado no campo FK de uma tabela:

Filtra os registros de um campo, baseado no valor de um campo em uma outra tabela desde que este segundo campo seja do tipo FK e possua o mesmo LINK. Possui a seguinte estrutura:

FIELD:TABELA:NOME_DO_CAMPO

Exemplo:

  • Tenho a tabela MO_APT contendo os campos FK OPER e SUBATIDADE;
  • A tabela OPER possui o campo SUBATIVIDADE_PRODUCAO que por sua vez é do tipo FK e possui LINK para SUBATIDADE;
  • Na tabela MO_APT, quero que apenas os registros de SUBATIVIDADE que possuem relação com a OPER definido na MO_APT estejam disponiveis para seleção(relação feita através do campo SUBATIVIDADE_PRODUCAO):
TABELA_NOMENOMELINKFILTRO_VIEWTIPO
MO_APTOPEROPER-FK
MO_APTSUBATIVIDADESUBATIVIDADEFIELD:OPER:SUBATIVIDADE_PRODUCAOFK

Filtro por LOVN

Para filtrarmos os registros de um campo de uma tabela baseado no valor de um outro campo, sendo que ambos os campos possuem um relacionamento de "muitos para muitos(n x m)", podemos utilizar o por LOVN, sendo a sua estrura:

PREFIXO:TABELA_DE_RELACIONAMENTO:TABELA_B:TABELA_A
  • PREFIXO: Neste caso será "LOVN";
  • TABELA_DE_RELACIONAMENTO: Nome da tabela que possui o relacionamento entre a TABELA_A e a TABELA_B(TABELA_TABELAB);
  • TABELA_A: Tabela correspondente ao campo cujo os registros serão filtrados;
  • TABELA_B:Tabela correspondente ao campo cujo o valor será utilzado como filtro. {.grid-list}

Exemplo 1

Para filtrar as subatividades baseado no valor da atividade

TABELA_NOMENOMELINKFILTRO_VIEWTIPO
MO_APTATIVIDADEOPER-FK
MO_APTSUBATIVIDADESUBATIVIDADELOVN:OPER_SUBATIVIDADE:SUBATIVIDADE:ATIVIDADEFK

ou para filtra a atividade baseado no valor da subatividade

TABELA_NOMENOMELINKFILTRO_VIEWTIPO
MO_APTATIVIDADEOPERLOVN:OPER_SUBATIVIDADE:ATIVIDADE:SUBATIVIDADEFK
MO_APTSUBATIVIDADESUBATIVIDADE-FK

Outros Exemplos:

Supondo que haja uma tabela LOVN entre Fazenda e Gleba (APP_FAZENDA_N_GLEBA) e na tela de OS haja os campos para escolher a fazenda e gleba, sendo que após escolher a fazenda deve trazer somente as glebas que estão na LOVN daquela fazenda, para isso é somente fazer a seguinte configuração na linha da nfs_core_ds_tabela_campo onde está a GLEBA:

LOVN:FAZENDA_GLEBA:GLEBA:FAZENDA

MASCARA

Regex

Para usar regex, configure o valor do campo "máscara" da seguinte forma:

regex:<regex>

Exemplo:

regex:[a-z1-9]*

Tela

Configurar Botão de Ação (Integração/Extorno, Relatório)

Para configurar um botão de ação basta usar a tabela nfs_core_ds_field_action depois dar permissão para qual usuário/grupo deseja liberar.

Campos:

  • SEQ_DB: Gerado automaticamente, não é necessário inserir.
  • TABELA: Adicionar a tabela a qual a botão irá pertecenter.
  • DESCRICAO: Importante adicionar para diferenciar de outros botões.
  • CONFIG: Campo mais importane que é onde vai a configuração do botão que pode ter os seguintes
    • text: Title do botão
    • icon_class: Classe para adicionar o ícone do botão, veja mais aqui
    • url: Endereço por ser internet ou externo, é possível passar variáveis que estejam no corpo da página, veja mais no exemplo abaixo
    • button_class: Classe do botão veja mais aqui

Exemplo:

SEQ_DBTABELADESCRICAOCONFIGENTRY_POINTINS_DHATIVODELETEDNAME
1OSAção de Integração de OS{"text": "Novo Details","icon_class": "fa fa-plus","url" : "/details/new/{{ seqDb }}/{{ seqQii }}/{{ seqPanel }}","button_class": "btn-primary"}2019-07-30 18:42:5610

Resultado:

download.png

Galeria Simova

download.jpeg

A Galeria Simova foi criada para permitir que o usuário selecione tanto fotos do computador como fotos padrão do sistema cadastradas na nova tabela nfs_lib_image situada na nfs_cloud.

Alterações no Banco de Dados

  • nfs_lib_image: Inserida no nfs_cloud com os campos:
    • IMAGE: Base64 da imagem;
    • IMAGE_THUMBNAIL: Thumbnail da Imagem;
    • ENABLED: Imagem deve ser exibida (1) ou não (0);
    • CATEGORY: Categoria da imagem;
    • DESCRIPTION: Palavras chave utilizadas para filtrar as imagens da Galeria.

Alterações no CRUD - campo UPLOAD: Exibe a Galleria Simova ou utilizar o Drag and Drop files para arrastar arquivos, função galleryPreview(); - campo IMG_MOBILE: Padrão, seleciona arquivos do computador, função simplePreview().

[!WARNING] Na tabela nfs_ds_core_tabela_campo quando este for do tipo UPLOAD e se quer fazer o upload/download de arquivos que não sejam imagens precisa configurar o campo VALIDAÇÃO no formato desejado sem misturar extensões de imagem com extensões de arquivos Ex: **(0:*pdf;1:docx;2:doc;) E para que funcione, as extensões configuradas precisam estar também na tabela nfs_par_parametros nos campos: para arquivos de texto, e arquivos que não sejam imagens CRUD_EXTENSOES_PERMITIDAS_UPLOAD -> 0:txt;1:jpeg;2:png;3:docx para arquivos de imagens-> CRUD_EXTENSOES_UPLOAD_PERMITIDAS_VISUALIZACAO -> png:image/png;jpeg:image/jpeg

Melhorias

  • Barra de Ferramentas:

    • Rotação da imagem: Rotaciona a imagem em 90 graus tanto para a direi quanto para a esquerda. Necessário salvar.
    • Excluir imagem: Adicionar a tabela a qual a botão irá pertecenter.
    • Cancelar seleção: Importante adicionar para diferenciar de outros botões.

Exemplo:

download_(1).jpeg

Exemplo:

Imagem Selecionada:

download_(2).jpeg

Imagem Rotacionada:

image_rotated.e7af1e3f.jpeg Após rotacionar clicar em Salvar; Mensagem:

download_(3).jpeg

Estrutura

Fluxo da Aplicação: Ao passar pela criação do campo no CrudController é retornado um <button id="simova_gallery">;

selecionar_imagens.jpeg

O button é acionado através do evento de click no arquivo Gallery.js; Ao passar pelo evento de click é feita uma requisição para a GalleryController a fim de buscar as imagens dafault cadastradas na nfs_lib_image; Populando a simovagallery.twig e retornado para a requisição esta preenchida; Ao retornar (conteúdo) é inserido no modal que é carregado.

gallery_image_padrao.43af8289.jpeg

Modal: Contém duas abas: Importar Imagem | Biblioteca do Sistema Na aba Biblioteca do Sistema contém as fotos (base64) cadastradas na tabela nfs_lib_image, enquanto em "Importar Imagem" abre o seletor de arquivos do computador.

gallery_image_computer.632d4451.jpeg

Como é feito o preview das imagens selecionadas

Na tabela nfs_core_ds_tabela_campo existe o campo VALIDACAO que é preenchido da seguinte maneira quando se refere a um campo que fará upload de arquivos ou imagens exemplo:

0:*gif;1:*png;2:*jpg

Neste campo são definidas os tipos de extensões que o campo permitirá, quando estiver nos formatos de arquivos de imagem, como os do exemplo acima será feito uma tratativa no sistema verificando o tipo de campo:

-> caso seja UPLOAD será exibida a galeria simova (galleryPreview); -> caso seja IMG_MOBILE será exibido apenas o botão para selecionar arquivos do computador (simplePreview)

Quando forem definidos exempo:

0:*txt;1:*xml;2:*docx;3:*html

Neste caso será feita a verificação destes campos e o preview será de uma imagem padrão, localizada em public/assets/img/file-selected.png

Logo, se não estiver aparecendo imagem ao selecionar algum arquivo de acordo com os exemplos acima deve-se verificar se a extensão do arquivo bate com as descritas neste documento ou definidas na tabela nfs_core_par_parametros nos campos CRUD_EXTENSOES_UPLOAD_PERMITIDAS_VISUALIZACAO e CRUD_EXTENSOES_PERMITIDAS_UPLOAD.

Configurações

1-Criar a tabela nfs_lib_image no Banco de Dados NFS_CLOUD

Executar o comando abaixo:

    USE NFS_CLOUD;

    create table nfs_lib_image (
    SEQ_DB bigint(20) unsigned not null auto_increment,
    IMAGE mediumblob null,
    IMAGE_THUMBNAIL blob null,
    ENABLED tinyint(1) null,
    CATEGORY varchar(250) null,
    DESCRIPTION varchar(100) null,
    PRIMARY KEY (`SEQ_DB`)
    ) ENGINE = InnoDB default CHARSET = utf8 collate = utf8_general_ci;

2-Campo adicionado na tabela nfs_upload:

Executar o comando abaixo:

    select distinct concat('ALTER TABLE ', t.TABLE_SCHEMA, '.', '`nfs_upload` ADD LIB_IMAGE_SEQ_DB INT NULL')
    from information_schema.TABLES t
    where t.TABLE_NAME = 'nfs_core_sys_tipo_campo';

3-Campo LIB_IMAGE_SEQ_DB deve ser adicionado também na nfs_core_ds_tabela_campo:

Executar o comando abaixo:

    select distinct concat('INSERT INTO ', t.TABLE_SCHEMA, '.', '`nfs_core_ds_tabela_campo` (TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES) VALUES (\'UPLOAD\', \'LIB_IMAGE_SEQ_DB\', 1, 0, 1, 1, 1, \'SEQ LIB IMAGE\', \'NFS_CLOUD SEQ LIB IMAGE\', null, null, null, null, null, \'IU\', 0, null, null, null, null, null, null, null, null, null)')
    from information_schema.TABLES t
    where t.TABLE_NAME = 'nfs_core_sys_tipo_campo';

CRUD da Galeria

ADICIONAR/EDITAR:

Para adicionar imagens na Galeria, por enquanto deve ser inserido diretamente no NFS_CLOUD -> nfs_lib_image através dos comandos INSERT e UPDATE no Banco de Dados, tendo como padrão os seguintes campos:

    SEQ_DB BIGINT(21) UNSIGNED NOT null AUTO_INCREMENT, | AUTO INCREMENT
    IMAGE MEDIUMBLOB NULL,                           | BASE64 || NULL
    IMAGE_THUMBNAIL BLOB NULL,                 | BASE64 Redimensionado || NULL
    ENABLED TINYINT(1) NULL                           | ATIVO (0 || 1) || NULL
    CATEGORY VARCHAR(250) NULL                      | NOME, COR, TIPO...
    DESCRIPTION VARCHAR(100) NULL                      | NOME, COR, TIPO...

EXCLUIR:

Basta deletar a linha desejada através do comando DELETE diretamente no Banco de Dados.

ARQUIVOS RELACIONADOS:

    Gallery.scss
    Gallery.js
    GalleryController.php
    base_layout.twig (contém o modal-gallery)
    simovagallery.twig (contém o conteúdo do modal-gallery)

Observações

  • Imagens Trazidas: Só serão trazidas imagens ENABLED = 1, e com o BASE64 IS NOT NULL;

Botões dos campos de data e hora

A configuração é feita na tabela core_par_parametros as chaves são separadas para os campos de data e campos de data e hora.

Campos tipo: DH, DHS (data e hora)


OPCAO_DATA_AGORA

Preenche a data e hora atuais.

Conteúdo: F:agora;DESC:Agora

OPCAO_DATA_HOJE

Preenche a data atual.

Conteúdo: F:hoje;DESC:Hoje

OPCAO_DATA_ONTEM

Preenche a data de ontem.

Conteúdo: F:ontem;DESC:Ontem

Campos tipo: DATA, DATE (data)


OPCAO_DATA_HOJE

Preenche a data atual.

Conteúdo: F:hoje;DESC:Hoje

OPCAO_DATA_1_DIA

Preenche a data atual + 24horas.

Conteúdo: F:addHours;P:24;DESC:Hoje + 1

OPCAO_DATA_2_DIA

Preenche a data atual + 48horas.

Conteúdo: F:addHours;P:48;DESC:Hoje + 2

inputs-data.png

Config

INSERT INTO suzano_sal_bobagro.nfs_core_par_parametros
(EMPRESA, FILIAL, `LOCAL`, NOME, CONTEUDO, TIPO)
VALUES(1, 9999, 9999, 'OPCAO_DATA_1_DIA', 'F:addHours;P:24;DESC:Hoje + 1', 1);
INSERT INTO suzano_sal_bobagro.nfs_core_par_parametros
(EMPRESA, FILIAL, `LOCAL`, NOME, CONTEUDO, TIPO)
VALUES(1, 9999, 9999, 'OPCAO_DATA_2_DIAS', 'F:addHours;P:48;DESC:Hoje + 2', 1);
INSERT INTO suzano_sal_bobagro.nfs_core_par_parametros
(EMPRESA, FILIAL, `LOCAL`, NOME, CONTEUDO, TIPO)
VALUES(1, 9999, 9999, 'OPCAO_DATA_AGORA', 'F:agora;DESC:Agora', 1);
INSERT INTO suzano_sal_bobagro.nfs_core_par_parametros
(EMPRESA, FILIAL, `LOCAL`, NOME, CONTEUDO, TIPO)
VALUES(1, 9999, 9999, 'OPCAO_DATA_HOJE', 'F:hoje;DESC:Hoje', 1);
INSERT INTO suzano_sal_bobagro.nfs_core_par_parametros
(EMPRESA, FILIAL, `LOCAL`, NOME, CONTEUDO, TIPO)
VALUES(1, 9999, 9999, 'OPCAO_DATA_ONTEM', 'F:ontem;DESC:Ontem', 1);

Usando o setFields para manipular campos e suas propriedades:

Exemplo:

	"setFields": {
          "PROC_ST": {
              "SYS": 0,
				"DESCRICAO":"Teste de gravação",
				"TIPO":"LOV1",
				"LOV":{"0":"Pendente","1":"Liberado Int.","4":"Integrado","9":"Processado ERP"}
          },
          "PROC_DESC": {
              "SYS": 0,
			"DISABLED":1
          },
          "ID": {
				"DESCRICAO":"ID DO BOLETIM"
          }
     }

Vídeo de exemplo:

🔗 Configurar Campo FK para Receber Múltiplos Valores

Para permitir que um campo do tipo **FK ** aceite múltiplos valores, é necessário configurar a propriedade allowMultipleFk como true no JSON armazenado na coluna CONFIG da tabela nfs_core_ds_tabela_campo.


🗂️ Local da Configuração

  • Tabela: nfs_core_ds_tabela_campo
  • Coluna: CONFIG (tipo JSON)
  • Propriedade: allowMultipleFk

✅ Exemplo de Configuração

Se o campo já possuir outras configurações, como customStyle ou recognition, a propriedade allowMultipleFk deve ser incluída dentro do mesmo objeto JSON:

{
  "customStyle": {
    "color": "blue",
    "fontSize": "14px"
  },
  "recognition": {
    "pattern": "^[0-9]+$"
  },
  "allowMultipleFk": true
}

Tabela Campo

Decimal

Esse campo é usado para valores numéricos com casas decimais, como preços, porcentagens, ou medidas. Mesmo que só números possam ser digitados, o sistema entende que esse número pode ter vírgula ou ponto para representar os centavos ou frações.

Comportamento

A máscara é aplicada nos seguintes lugares:

  • Lista do crud
  • Formulário do crud: Preenche o caracter separador de milhar (se houver) automaticamente a cada 3 casas. Usuário digita o separador dos decimais quando precisar identificar os decimais. Placeholder exibirá um padrão conforme a mascara. (9[9],[9][9][9].9 -> 99,999.9)

Regra

A combinação de vírgula para a separação dos decimais e vírgula também para a separação dos milhares não é permitida. Ao detectar isso a mascara inverte o separador e irá usar o ponto como separador de milhar. Ex: 9[9],[9][9][9],9 --> 9[9].[9][9][9],9

O mesmo para o uso de ponto para ambos separadores: Ex: 9[9].[9][9][9].9 --> 9[9],[9][9][9].9

Máscara vazia

Quando o campo MASCARA estiver nulo ou vazio, assume automaticamente uma máscara padrão de 8 casas de inteiros e 2 decimais. Quando não houver entrypoint de configuração por filial a máscara configurada será aplicada para todas.

Entrypoint

Filiais que usam diferentes tipos de máscara podem utilizar um entrypoint para fazer essa configuração.

Na tabela core_ds_tabela_campo o campo MASCARA será preenchido com o termo 'ENTRY_POINT'.

Um entrypoint do tipo JSON é criado com a seguinte estrutura no exemplo abaixo:

ACTION:

filial_masks

TABELA:

Tabela em que o campo decimal se encontra.

EQP

CONTENT:

Uma lista de campos, que possui a lista com as respectivas filiais e suas máscaras. É obrigatório o uso da filial 9999 que será a máscara padrão.

{
	"ULTIMO_HORIMETRO": {
		"9999": "9[9][9],[9][9][9].99", /** Default */
		"31": "9[9][9].[9][9][9],99"
	}
}

Exemplo completo do entrypoint (insert)

insert
	into
	nfs_homol_pamela_testes.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('SYSTEM', 'filial_masks', null, null, 9999, 9999, 9999, 'EQP', null, '{
	"ULTIMO_HORIMETRO": {
		"9999": "9[9][9][9][9][9].99",
		"1": "9[9][9][9][9][9],99"
	}
}', 1, 16, 1, 'JSON', null, null, '2019-04-01 10:58:59', '2025-03-31 16:20:55', null, 'thiago.silva.chp@152.249.44.132');

Máscaras (exemplos validados)

['[9][9][9][9]', '9999'],
['[9][9][9][9][9][9]', '999999'],
['[9][9],[9][9]', '99,99'],
['[9][9].99', '99.99'],
['[9][9][9],99', '999,99'],
['[9][9][9][9].99', '9999.99'],
['[9][9][9][9][9][9][9].99', '9999999.99'],
['9[9].9', '99.9'],
['9[9].99', '99.99'],
['9[9][9].99', '999.99'],
['9[9][9].99', '999.99'],
['9[9][9].9999', '999.9999'],
['9[9][9][9].9', '9999.9'],
['9[9][9][9].99', '9999.99'],
['9[9][9][9][9].99', '99999.99'],
['9[9][9][9][9][9].9', '999999.9'],
['9[9][9][9][9][9].99', '999999.99'],
['9[9][9],[9][9][9].99', '999,999.99'],
['9[9][9].[9][9][9],99', '999.999,99'],
['9[9][9][9][9][9],99', '999999,99'],
['9[9][9][9][9][9].999', '999999.999'],
['9[9][9][9][9][9].9999', '999999.9999'],
['9[9][9][9][9][9][9].9', '9999999.9'],
['9[9][9][9][9][9][9].99', '9999999.99'],
['9[9][9][9][9][9][9].999', '9999999.999'],
['9[9][9][9][9][9][9][9].9', '99999999.9'],
['9[9][9][9][9][9][9][9].99', '99999999.99'],
['9[9][9][9][9][9][9][9].999', '99999999.999'],
['9[9][9][9][9][9][9][9].99999', '99999999.99999'],
['9[9][9][9][9][9][9][9].99999999', '99999999.99999999'],
['9[9][9][9][9][9][9][9][9].9', '999999999.9'],
['9[9][9][9][9][9][9][9][9].9', '999999999.9'],
['9[9][9][9][9][9][9][9][9][9].99', '9999999999.99'],
['9[9][9][9][9][9][9][9][9][9][9].999', '99999999999.999'],
['9[9][9][9][9][9][9][9][9][9][9].99999999', '99999999999.99999999'],
['99.99', '99.99'],
['9999,99', '9999,99'],
['9999.999999999999999', '9999.999999999999999']

Inativar registro - Configurações

Ativar Verificação de Relacionamento ao Inativar um Registro

Parâmetro: CRUD_VERIFICA_FK_DESABILITAR_REGISTRO

Este parâmetro controla se a verificação de chaves estrangeiras deve ser realizada antes de inativar um registro em determinadas tabelas.

Como Ativar

  1. Acesse a tabela nfs_core_par_parametros.
  2. Insira ou atualize o registro com:
    • NOME: CRUD_VERIFICA_FK_DESABILITAR_REGISTRO
    • CONTEUDO: Lista de tabelas separadas por vírgula (,), onde a verificação deve ser aplicada.

Exemplo de inserção:

INSERT INTO nfs_core_par_parametros (NOME, CONTEUDO) 
VALUES ('CRUD_VERIFICA_FK_DESABILITAR_REGISTRO', 'tabela1,tabela2,tabela3');

Como Usar

Ao tentar inativar um registro, o sistema verificará automaticamente se a tabela está listada no parâmetro. Se estiver, a inativação só será permitida se não houver dependências em outras tabelas.

Se a tabela não estiver listada, a inativação ocorrerá sem a verificação de relacionamento.

Observações

  • Caso o parâmetro não esteja cadastrado, a verificação não será aplicada.
  • A funcionalidade não impede exclusão física, apenas valida a inativação.
  • Esta verificação também será aplicada ao desativar um registro via API.

EntryPoints

EntryPoints Insert e Update

Definição e campos da tabela

Na tabela de EntryPoint nfs_entry_point temos os seguintes campos:

  • FILE_OR_DOMAIN: Local onde o EntryPoint vai ser usado, sendo:
    • XMOVA - Utilizado para enviar dados para o mobile;
    • SYSTEM - Utilizado dentro do portal NFS;
  • ACTION: Nome do EntryPoint;
  • XMOVA_INSTALLCODE - Número do fluxo (código de instalação) para utilização com XMOVA;
  • TABELA: Tabela onde o EntryPoint vai ser executado;
  • ENTRY_NUM: Número para definir a ordem/forma de execução do EntryCode (estilo uma trigger);
  • CODE: Código do EntryPoint (PHP, SQL);
  • CODETYPE: Define o tipo de linguagem que será usado no campo CODE.

Número dos EntryPoints

Os números vão no campo ENTRY_NUM e servem para definir a forma da execução.

18 - Before Insert

Código a ser configurado para executar ANTES do INSERT na tabela correspondente. Geralmente utilizado para adicionar/complementar dados ao insert, realizar cálculos, etc.. Nas versões antigas do NFS o código utilizado era 30.

Variáveis Disponíveis

  • inputValues
  • tableName
  • tableObject

Variáveis Antigas

  • valores, outputValues -> inputValues na versão atual;
  • nomeTabela -> tableName na versão atual.

191 - After Insert (antigo 19)

Código a ser configurado para executar DEPOIS do INSERT na tabela correspondente. Geralmente utilizado para adicionar um valor em outra tabela que precisa de uma referência na tabela que o EntryPoint está executando.

Nas versões antigas do NFS o código utilizado era 19.

Variáveis Disponíveis

  • inputValues
  • lastInsertId
  • tableObject
  • tableName
  • tBO

Variáveis Antigas

  • valores -> inputValues na versão atual;
  • _lastInsertId -> lastInsertId na versão atual;
  • tabela_par -> tableName na versão atual.

20 - Before Update

Código a ser configurado para executar ANTES do UPDATE na tabela correspondente.

Variáveis

1 - inputValues:

Valores que foram enviados para update no sistema:

$this->inputValues;
/**
* Tipo: Array
* Exemplo de saída: [["SEQ_DB" => 9999, "CAMPO" => valorCampo]]
*/
2 - outputValues

Contem os mesmo valores descritos na $inputValues, caso precise modificar os valores enviados pelo formulario, esta variavel deverá ser modificada:

$this->outputValues;
/**
* Tipo: Array
* Exemplo de aplicação: $this->outputValues['CAMPO'] = novoValorCampo;
*/
3 - tableName

Nome da tabela para qual o EntryPoint foi configurado:

$this->tableName;
/**
* Tipo: String
* Exemplo de saída: "ENCARREGADO"
*/
4 - rowValues

Valores do registro no banco:

$this->rowValues;
/**
* Tipo: Array
* Exemplo de saída: [["SEQ_DB" => 9999, "CAMPO" => valorCampo]]
*/

[!WARNING] Caso a tabela seja ALERTS_USER o registro passa por tratativas antes mesmo da execução deste EntryPoint a qual ira modificar o valor da row, caso não seja este o caso, o valor do inputValues será atribuido a esta variavel.

5 - tableObject

Objeto da classe "Tabela", construido baseado na tabela do EntryPoint:


$this->tableObject;
/**
* Tipo: Objeto(voclasses\Tabela)
* Exemplo de saída:
	voclasses\Tabela
		NOME: "ENCARREGADO"
		TABELA_REAL: "APP_ENCARREGADO"
		DESCRICAO: "Encarregado"
		UNQ: null
		DISPLAY: "LETRA,NOME"
		FILIAL_COMP: false
		LOCAL_COMP: false
		CAMPOS: array(26)
		ACTIONS: array(0)
		TIPO: "1"
		FILTRO_USER: "0"
		MOBILE_MESSAGE: "0"
		MOBILE_MESSAGE_FK: null
		MOBILE_TABLE: "0"
		ORDER_BY: "CRACHA, NOME ASC"
		ORDER_BY_OPTIONS: array(2)
		OPTIONS: ""
		OPTIONS_LOV: null
		MAX_ROWS_WO_FILTER: "100"
		MAX_ROWS_WO_FILTER_MODAL: "100"
		INDICATORS: ""
		numFKFields: 2
		hasLOVNFields: false
		hasLOVXFields: false
		hasFKXFields: false
		FK_UP: array(2)
		FK_DOWN: array(5)
		hasGpsPoint: false
		hasImageMobile: false
		recordCount: 0
		fklTables: array(3)
		fklFields: array(3)
		TABELA_CAMPO_ACTIONS: null
*/

Variáveis Antigas

  • row -> rowValues na versão atual
  • tabela_par -> tableName na versão atual

21 - After Update

Código a ser configurado para executar DEPOIS do UPDATE na tabela correspondente. Geralmente utilizado para atualizar um valor em outra tabela que referencie a que está sendo utilizada.

Variáveis Disponíveis
  • inputValues
  • return
  • rowValues
  • tableName
  • tableObject
  • tBO
  • updateValues
Variáveis Antigas
  • valores, outputValues -> inputValues na versão atual;
  • row -> rowValues na versão atual;
  • tabela_par -> tableName na versão atual.

7 e 45 Manipulação front-end:

Os ENTRY_NUM 7 e 45 podem ser utilizados em conjunto para manipular o javascript da pagina(7) e validar requisições/alterações no PHP (45).

7 - Validação FrontEnd

Utilizado com código JavaScript para realizar validações no FrontEnd ou tela do CRUD, com ele você pode:

  • Esconder/Bloquear campos
  • Criar um novo botão, por exemplo, para mostrar um popup ou para salvar uma nova informação e nesse forma é necessário usar o 45 para salvar no banco de dados.

Exemplo

7 - Validação (JavaScript)

$("#PROC_ST").find("option").css("display", "none");

$(document).ready(function () {
  $(".form-actions #enviar").after(
    '&nbsp;<button id="integrar" type="button" onclick="" data-title="Integração"  class="btn btn-lg green-nfs tooltips"><i class="fa fa-reply"></i>Integração</button>'
  );

  var procDH = $("#PROC_DH").val();
  if (procDH != "" && procDH != 0) {
    $("#integrar").addClass("hide");
    disableBtn();
  }

  $("#integrar").click(function () {
    var dataInicio = $("#INI_DH").val();
    var dataFim = $("#FIM_DH").val();

    if (dataInicio != "" && dataFim != "") {
      nfsui.windowConfirm(
        "Deseja realmente integrar? O campo Data Integração será preenchido!",
        "Sim",
        "Não",
        preencheDataIntegracao,
        cancelar,
        "",
        ""
      );
    } else {
      notify(
        "error",
        "A data inicio e fim do boletim devem ser preenchidas antes da integração!"
      );
    }
  });
});

function cancelar(valor) {
  $("#PROC_ST").val(0);
  $("#PROC_DH").val("");
  $("#PROC_DESC").val("");
}

function preencheDataIntegracao() {
  $.ajax({
    type: "POST",
    url: "/entrypoint",
    data: {
      getData: "getData",
      getDesc: "getDesc",
    },
    dataType: "json",
    success: function (data) {
      $("#PROC_ST").val(1);
      $("#PROC_DH").val(data.getData);
      $("#PROC_DESC").val(data.getDesc);
      $("#integrar").addClass("hide");
      disableBtn();
    },
    error: function () {
      notify("error", "Erro! Tente novamente mais tarde.");
    },
  });
}

function disableBtn() {
  document.getElementById("INI_DH").disabled = true;
  document.getElementById("FIM_DH").disabled = true;
}

[!WARNING] Elementos que recebem a propriedade disabled não são enviados ao realizar o submit do formulario, neste caso utilizar readOnly ao invés de disabled como no exemplo: $('#NOME_CAMPO').readOnly = true.


45 - Validação BackEnd

Utilizado com código PHP para realizar validações no BackEnd.

Para chamar o entry point 45 é necessário realizar um requisição por ajax para /entrypoint.

Exemplo

Na variável $_POST temos os parâmetros que são enviados da tela entry point 7 para o backend entry point 45, por exemplo, para o o exemplo abaixo temos duas variáveis horaInicial e horaFinal se houvesse um entry point 7 ele estaria mais ou menos dessa forma:

function preencheDataIntegracao() {
  $.ajax({
    type: "POST",
    url: "/entrypoint", // Sempre usado para passar pelo entry point 45
    data: {
      horaInicial: "10:00", // Aqui estou colocado os valores constantes, mas poderia ser uma variável
      horaFinal: "12:00",
    },
    dataType: "json",
    success: function (data) {
      $("#PROC_ST").val(1);
      $("#integrar").addClass("hide"); // esconde o botão integrar
    },
    error: function () {
      notify("error", "Erro! Tente novamente mais tarde.");
    },
  });
}

Entry Point 45:

if(isset($_POST['horaInicial']) && $_POST['horaInicial'] != '' && isset($_POST['horaFinal']) && $_POST['horaFinal'] != '') {

	$resultsInicioMaior = \core\NFSQueryBuilder::select([
		"distinct t.seq_db"
	])
	->from('turno', 't')
	->where('TIME_TO_SEC(:horaInicial) >= TIME_TO_SEC(:horaFinal)')
	->setParameters([
		'horaInicial' => $_POST['horaInicial'],
		'horaFinal' => $_POST['horaFinal'],
		])
	->get();

	$results = \core\NFSQueryBuilder::select([
		"distinct t.seq_db"
	])
	->from('turno', 't')
	->where('
		(TIME_TO_SEC(:horaInicial) BETWEEN t.HORA_INICIAL_REFEICAO AND t.HORA_FINAL_REFEICAO) or
		(TIME_TO_SEC(:horaFinal) BETWEEN t.HORA_INICIAL_REFEICAO AND t.HORA_FINAL_REFEICAO)
	')
	->andWhere('t.seq_db != :SeqDB')
	->setParameters([
		'horaInicial' => $_POST['horaInicial'],
		'horaFinal' => $_POST['horaFinal'],
		'SeqDB' => $_POST['SeqDB'],
		])
	->get();

	if(!empty($resultsInicioMaior)){
		echo 'HoraInicioMaior';
		exit;
	}

	if(!empty($results)){
		echo 'HoraInvalida';
		exit;
	}

	echo 'HoraValida';
	exit;
}

if(isset($_POST['getDataSec'])) {
	echo date('d/m/Y H:i:s');
	exit;
}

if(isset($_POST['getData']) && isset($_POST['getDesc']) ) {
	$data1 = date('d/m/Y H:i:s');
	$user = \core\xDS::getUser();
	$data2 = $user->LOGIN .'-'.$user->SEQ_DB;
	echo json_encode(['getData'=>$data1, 'getDesc'=>$data2]);
	exit;
}

[!NOTE] Retornar todos os registros de uma tabela (Não funciona para MOBILE_TABLE (APONTAMENTO OU BOLETIM))


Exemplos de funções e utilizações básicas

[!IMPORTANT]
Não usar chamadas a classe ConexaoDB! Em breve os códigos que >> tiverem essa chamada serão ignorados.

  • GET Modo padrão: Obtenção padrão de dados de 01 registro (trás as _FK)
$dadosCompletosDoEquipamento = Dao::table('EQP')->seqDb(345);
  • GET Modo lazy: Obtenção mais limpa dos dados de 01 registro (não trás as _FK)
$dadosDoEquipamento = Dao::table('EQP')->lazy()->seqDb(14,true);
  • Se precisar só de um campo da tabela (só funciona em modo lazy)
$descricaoEquipamento = Dao::table('EQP')->select(['DESCRICAO'])->seqDb(14,true);
  • Obter uma lista em um Array de registros agrupados por SEQ_DB. Muito utilizado em buscas e for's / montagem de entryPoint/Relatórios
$resultado = Dao::table('equipamento')
			->orderBy('DESCRICAO','DESC')
			->groupBy('SEQ_DB')
			->toArray()
			->get();
  • Atualizar o ponto de GPS do registro com SEQ_DB = 1 da tabela Equipamento
$resultado = Dao::table('equipamento')
			->where(1)
			->updateGpsPoint(-21.121212,-12.345678,'2020-01-01 01:21:59');
  • Mostrar o conteúdo e tipo da variável
echo '<pre>';
var_dump($variavel);
echo '</pre>';
  • Obter 30 registros agrupados pelo SEQ_DB e ordenados pela DESCRICAO (descrecente) da tabela Equipamento. Também mostra a Query resultante.
$resultado = Dao::table('equipamento')
			->orderBy('DESCRICAO','DESC')
			->groupBy('SEQ_DB')
			->limit(30)
			->echoSql()
			->get();
  • Atualizar alguns campos do registro com SEQ_DB = 1 na tabela Equipamento
$seqDb = 1;

$updateEqp = [];
$updateEqp['POSICAO'] = 1;
$updateEqp['POSICAO_PLAT'] = -22.444422;
$updateEqp['POSICAO_PLON'] = -22.33333;
$updateEqp['POSICAO_DH'] = '2020-04-10 03:01:02';

$resultado = Dao::table('equipamento')
			->where($seqDb)
			->updateRow($updateEqp);
  • Obter a DS (estrutura) de uma tabela e seus campos:
$tabelaDS = Dao::table('EQP')->getDS();
$camposTabelaDS = $tabelaDS->CAMPOS;

$camposTabelaDS = Dao::table('EQP')->getDS()->CAMPOS; //versão elegante

Exemplos de EntryPoints

XMOVA

[!WARNING]
Ao criar EntryPoints do tipo XMOVA ou XMOVALOCATION, caso não possuam o XMOVA_INSTALLCODE, utlizar o valor 9999.

SYSTEM - INSERÇÃO E MODIFICAÇÃO

18 - Before Insert

if($this->inputValues['SEQ_DB_DEVICE'] == 0 or $this->inputValues['SEQ_DB_DEVICE'] == '') {
	NfsLogger::WARN(print_r($this->inputValues,true), 'SEQ_DB_DEVICE_ZERADO');
	$this->outputValues['SEQ_DB_DEVICE'] = hexdec(uniqid());
}


if($_SESSION['LOG_SESSION']['ACTION'] == 'CRUD'){ //somente se inserido pelo painel
	NfsLogger::WARN(print_r($this->inputValues,true), 'WEB_EQP_APT_18');

	$pesquisar = false;
	$iApt = $this->inputValues['INI_DH'];
	$fApt = $this->inputValues['FIM_DH'];
	$inicioApt = Utils::convertDHSToMysql($iApt) ?? NULL;
	$fimApt = Utils::convertDHSToMysql($fApt) ?? NULL;
	$grua = $this->inputValues['EQP_SEQ_DB'] ?? NULL;

	if(!empty($this->inputValues['SEQ_DB_DEVICE_MASTER_SEQ_DB'])){
		NfsLogger::WARN('COM BOLETIM - CONFERIR', 'WEB_EQP_APT_18');

		$boletim = Dao::table('EQP_BOLETIM')->select('INI_DH', 'FIM_DH')
			->where(['SEQ_DB' => $this->inputValues['SEQ_DB_DEVICE_MASTER_SEQ_DB']])
			->get();

		$inicioBoletim = $boletim[0]['INI_DH'] ?? NULL;
		$fimBoletim = $boletim[0]['FIM_DH'] ?? NULL;

		if(!empty($inicioBoletim)){
			if(strtotime($inicioBoletim) <= strtotime($inicioApt) and (strtotime($fimBoletim) >= strtotime($fimApt) or empty($fimBoletim))){
				NfsLogger::WARN('BOLETIM CORRETO', 'WEB_EQP_APT_18');
			}
			else{
				NfsLogger::WARN('BOLETIM ERRADO', 'WEB_EQP_APT_18');
				$this->outputValues['SEQ_DB_DEVICE_MASTER_SEQ_DB'] = null;
				$pesquisar = true;
			}
		}
	}
	else{
		NfsLogger::WARN('SEM BOLETIM - PESQUISAR', 'WEB_EQP_APT_18');
		$pesquisar = true;
	}

	if($pesquisar){
		NfsLogger::WARN('PROCURAR BOLETIM FECHADO', 'WEB_EQP_APT_18');

		if(!empty($inicioApt) and !empty($fimApt) and !empty($grua)){

			$boletim = NFSQueryBuilder::select(["b.SEQ_DB"])
				->from('EQP_BOLETIM','b')
				->where("b.EQP_SEQ_DB = {$grua} AND b.INI_DH <= '{$inicioApt}' AND b.FIM_DH >= '{$fimApt}'")
				->get();

			if(!empty($boletim)){
				NfsLogger::WARN('BOLETIM ENCONTRADO: '.$boletim[0]['SEQ_DB'], 'WEB_EQP_APT_18');
				$this->outputValues['SEQ_DB_DEVICE_MASTER_SEQ_DB'] = $boletim[0]['SEQ_DB'];
			}
			else{
				NfsLogger::WARN('PROCURAR BOLETIM ABERTO', 'WEB_EQP_APT_18');

				$boletimAberto = NFSQueryBuilder::select(["b.SEQ_DB"])
					->from('EQP_BOLETIM','b')
					->where("b.EQP_SEQ_DB = {$grua} AND b.INI_DH <= '{$inicioApt}' AND b.FIM_DH IS NULL")
					->get();

				if(!empty($boletimAberto)){
					NfsLogger::WARN('BOLETIM ENCONTRADO: '.$boletimAberto[0]['SEQ_DB'], 'WEB_EQP_APT_18');
					$this->outputValues['SEQ_DB_DEVICE_MASTER_SEQ_DB'] = $boletimAberto[0]['SEQ_DB'];
				}
				else{
					NfsLogger::WARN('BOLETIM NÃO ENCONTRADO', 'WEB_EQP_APT_18');
				}
			}
		}
		else{
			NfsLogger::WARN('VERIFICAR: INÍCIO, FIM OU GRUA ESTÁ VAZIO', 'WEB_EQP_APT_18');
		}
	}

} else {
	NfsLogger::WARN('APONTAMENTO NÃO FOI INSERIDO PELO PAINEL', 'WEB_EQP_APT_18');
}

191 - After Insert

Após a inserção de um novo apontamento, o EntryPoint vai verificar se esse apontamento é uma interferência. Se sim, vai realizar o fechamento automático do mesmo.

if ($tableName == 'APONTAMENTO_MO') {
    //Busca a interferência com configuração do tipo 1
	$interferencias = Dao::table('INTERFERENCIA')
					->select(['SEQ_DB'])
					->innerJoin('i', 'CONFIGURACAO', 'cf')
					->where('cf.TIPO = 1')
					->get();

	//Verifica se o tipo do apontamento foi uma interferência
	if (isset($inputValues['INTERFERENCIA_SEQ_DB'])
	&& isset($interferencias['SEQ_DB'])
	&& $inputValues['INTERFERENCIA_SEQ_DB'] == $interferencias['SEQ_DB']) {

		//Se for interferência, atualiza os campos
		$fechamentoUpdate['FIM_DH'] = $inputValues['INI_DH'];
		$fechamentoUpdate['INI_FIM_DIFF_ST'] = -1;

		//Atualiza o $_lastInsertedId com as informações
		//para fazer o fechamento automático do apontamento
		$resultado = Dao::table('APONTAMENTO_MO')
			->where($_lastInsertId)
			->updateRow($fechamentoUpdate);
    }
}

20 - Before Update

if($tableName == 'FUNCIONARIO') {
	if (isset($rowValues['SEQ_DB'])) {
		if($inputValues['FLAG_ENCARREGADO'] == '1'){
			$tblBO = new \core\TabelaBO();
			$tabelaFuncionario = \core\xDS::getTabela('FUNCIONARIO');
			$seq = $rowValues['SEQ_DB'];
			$filtro = "SELECT f.EMPRESA, f.FILIAL, f.LOCAL, f.CRACHA, f.NOME
				from app_funcionario f
				left join app_funcao fun on f.FUNCAO_SEQ_DB = fun.SEQ_DB
				where f.SEQ_DB = {$seq} AND (f.CRACHA <> fun.CODIGO OR f.FUNCAO_SEQ_DB IS NULL);";

			$funcionarios = \core\ConexaoDB::query($filtro, $params = []);
			$tabelaFuncao = \core\xDS::getTabela('FUNCAO');
			if(!empty($funcionarios)){
				$dadosInsert['EMPRESA'] = $funcionarios[0]['EMPRESA'];
				$dadosInsert['FILIAL'] = $funcionarios[0]['FILIAL'];
				$dadosInsert['LOCAL'] =$funcionarios[0]['LOCAL'];
				$dadosInsert['CODIGO'] = $funcionarios[0]['CRACHA'];
				$dadosInsert['DESCRICAO'] = strtoupper($funcionarios[0]['NOME']);
				$fun = "SELECT fun.SEQ_DB SEQ_DB from app_funcao fun where fun.CODIGO = {$funcionarios[0]['CRACHA']};";
				$novaFuncao = \core\ConexaoDB::query($fun, $params = []);

				if (empty($novaFuncao)){
					$newFuncao = $tblBO->insert($tabelaFuncao, $dadosInsert,false);
					$seqDbFuncao = $newFuncao->lastInsertSeqDb;
					$inputValues['FUNCAO_SEQ_DB'] = $seqDbFuncao;
					$this->code['values'] = $inputValues;
				}
				else{
					$inputValues['FUNCAO_SEQ_DB'] = $novaFuncao[0]['SEQ_DB'];
					$this->code['values'] = $inputValues;
				}
			}
		}
	}
}

21 - After Update

if (isset($rowValues['SEQ_DB'])) {
	$tblBO = new \core\TabelaBO();
	$tabelaFuncionario = \core\xDS::getTabela('FUNCIONARIO');
	$seq = $rowValues['SEQ_DB'];
	$filtro = "SEQ_DB = {$seq} AND FLAG_ENCARREGADO = 1";
	$func = \core\TabelaBO::getFilteredToArray($tabelaFuncionario,'',$filtro);
	$tabelaFuncao = \core\xDS::getTabela('FUNCAO');
	$dadosInsert = [];
	if(!empty($func)){
		$dadosInsert['EMPRESA'] = $func[0]['EMPRESA'];
		$dadosInsert['FILIAL'] = $func[0]['FILIAL'];
		$dadosInsert['LOCAL'] =$func[0]['LOCAL'];
		$dadosInsert['CODIGO'] = $func[0]['CRACHA'];
		$dadosInsert['DESCRICAO'] = strtoupper($func[0]['NOME']);
		$tblBO->insert($tabelaFuncao,$dadosInsert,false);
	}
}

TabelaBO::getRow(tabela, seq_db, filtro)

[!CAUTION] Não usar ConexaoDB::getRow($sql)

$result = \core\TabelaBO::getRow("FUNCIONARIO",43453,"");

$result = \core\TabelaBO::getRow("FUNCIONARIO","","CRACHA = '11111'");

Exportação

DataTables

Exportação PDF

Adicionar header com o logo do cliente e outras informações

É possível configurar o logo do cliente, número de páginas e hora de emissão da exportação. Para isso é necessário ativar o seguinte parâmetro:

INSERT INTO nfs_core_par_parametros
(EMPRESA, FILIAL, LOCAL, NOME, CONTEUDO, TIPO)
VALUES(9999, 9999, 9999, 'HEADER_ON_CRUD_PDF_EXPORT', '1', 1);

Quando não tiver logo, vai aparecer por extenso o nome do cliente.

Exemplos:


Paginação:


Funcionalidades

Configuração de tabela principal no Acesso Rápido

O acesso rápido faz uma consulta na tabela de OS e lista as OS's para serem consultadas, pelo fato de algumas bases possuírem o nome da tabela que armazena as OS's diferente, foi criado o parâmetro OS_TABLE_NAME, para que o sistema possa achar a tabela correta correspondente às OS's

Se não possuir o parâmetro configurado na tabela nfs_core_par_parametros automaticamente será considerado que o nome da tabela é OS.

OBS: não deve ser inserido app_nomeDaTabela ou nfs_nomeDaTabela, apenas o nome da tabela, exemplo -> ordem_servico. {.is-info}

O código abaixo gera comandos SQL para inserir o parâmetro OS_TABLE_NAME na tabela nfs_core_par_parametros, basta copiar o resultado da query e colar no terminal para executar o INSERT:

select
	distinct concat('INSERT INTO ', t.TABLE_SCHEMA, '.', 'nfs_core_par_parametros (EMPRESA, FILIAL, LOCAL, NOME, CONTEUDO, TIPO)
VALUES ($seqDbEmpresa, $seqDbFilial, $seqDbLocal, ' OS_TABLE_NAME', ' $osTableName', 1)')
from
	information_schema.TABLES t
where
	t.TABLE_NAME = 'nfs_core_sys_tipo_campo';

OBS: Para atender uma necessidade de ambientes fora de padrão (por exemplo, cliente que contratam apenas checklist) foi criada a opção de dois parametros:

HOME_QUICKACCESS_OPTIONS_1
HOME_QUICKACCESS_OPTIONS_2

Assim, colocando-se a tabela PENDENCIA no HOME_QUICKACCESS_OPTIONS_1 e a Home fica com esse item de acesso rápido.

Exemplo de configuração OPTIONS_1 e OPTIONS_2 param.png

Irá exibir: param2.png

*OPER(Ativiade)

Ocultando o componente

Caso seja seja necessario ocultar um ou mais componentes de acesso rapido, podemosa definir o CONTEUDO = disabled ao parametro correspondente tabela que deseja ocultar, desta forma: disable_option2.png

Caso deseje ocultar todos os campos, podemos utilizar o parametro HOME_QUICKACCESS_OPTIONS definindo o seu CONTEUDO = disabled, exemplo: no_items.png

Documentação para Inclusão de Botão de Acesso Rápido

Para adicionar um novo botão de acesso rápido ao sistema, siga os seguintes passos:

1. Criação de Registro na Tabela NFS_CORE_MENU

O campo FATHER deve ser o SEQ_DB do menu com a DESCRICAO "Gestão à vista".

É necessário inserir um novo registro na tabela NFS_CORE_MENU, preenchendo os campos conforme abaixo:

  • FATHER: Deve ser o SEQ_DB do menu pai ao qual este item está subordinado. Se for relacionado ao menu "Gestão à vista", utilize o SEQ_DB correspondente.
  • TYPE: Defina como LINK para indicar que é um link para outra funcionalidade.
  • URL: Informe a URL relativa do recurso ou funcionalidade. Exemplo: t/parametro_boletim_automatico.
  • ICON: Especifique a classe do ícone a ser exibido no botão. Exemplo: 'fa fa-tachometer'.
  • Ativo: Define se o item está ativo no sistema. Marque como 1 para ativo ou 0 para inativo.
  • Empresa: Identifica a empresa que pode acessar o item. Use 9999 para liberar para todas as empresas.
  • Filial: Identifica a filial que pode acessar o item. Use 9999 para liberar para todas as filiais.
  • Local: Define o local associado ao item. Use 9999 para liberar para todos os locais.

2. Configuração de Permissões

Após a criação do registro no menu, as permissões de acesso devem ser configuradas para garantir que os usuários/grupos tenham o acesso correto ao novo botão. Isso é feito nas seguintes tabelas:

2.1. Tabela NFS_ACL_GRUPO_PERMISSAO

Essa tabela relaciona permissões com grupos de usuários. Deve-se inserir um novo registro para permitir que um grupo específico acesse o novo botão.

2.2. Tabela NFS_ACL_GRUPO_USUARIO

Aqui, você deve associar os usuários aos grupos que terão permissão para acessar o botão de acesso rápido.

3. Conclusão

Com o registro criado na tabela NFS_CORE_MENU e as permissões devidamente configuradas nas tabelas NFS_ACL_GRUPO_PERMISSAO e NFS_ACL_GRUPO_USUARIO, o novo botão de acesso rápido será exibido apenas para os usuários/grupos autorizados.

Apontamento Ponto

Configuração do apontamento ponto apontamento_ponto.png

1- É necessário criar um campo denominado NFS_ACL_USUARIO do tipo USER com o LINK direcionado para a tabela de NFS_ACL_USUARIO nas tabelas BOLETIM, APONTAMENTO_PONTO e FUNCIONARIO;

2- Caso não haja no ambiente a tabela "APONTAMENTO_PONTO" é necessário cria-la, seguindo a estrutura criada no ambiente: ponto.simova.cloud/t/apontamento_ponto;

2.1- É necessário que a tabela seja ordenada por data, então a coluna ORDER_BY deve estar preenchida como: INI_DH DESC;

3- É necessaŕio criar um grupo de acesso denominado PONTO e permitir apenas rotas autorizadas para o mesmo, para que quando criado os usuários na tabela NFS_ACL_USUARIO seja vinculado este grupo que tem acesso limitado ao ambiente;

4- É necessário criar o acesso pelo menu para o objeto de marcação de ponto, devendo ser preenchido na coluna URL apenas o valor: point; Exemplo: ponto.simova.cloud/point

Observação: pode ser que haja problema na criação de campos vinculando um LINK a uma tabela externa a DS_TABELA, portanto, pode ser que seja necessário fazer um ALTER_TABLE na DS_TABELA_CAMPO para modificar este comportamento. Modelo: ALTER TABLE nfs_core_ds_tabela_campo DROP FOREIGN KEY FK_TABELA_CAMPO_LINK;

Edição em Lote/Batch

Introdução

Esta funcionalidade foi criada para permitir que usuários manipulem registros em massa, economizando tempo e esforço em tarefas de edição repetitivas.

Ao invés de editar cada registro de forma individual, podemos selecionar quais registros queremos editar e faze-lo de uma só vez, alem disso, podemos acessar os registros de outras tabelas que estão relacionadas ao registro da tabela principal e editar, inserir, clonar ou mesmo deleta-los(em todo caso apenas a edição poderá ser feita em lote).

Por padrão todas as tabelas do CRUD podem ser editadas através da edição em lote( exceto as tabelas AUTO ), podemos fazer isso passando o nome da tabela como parâmetro na rota /batch ou configurando um link de acesso que irá aparecer nos registros do CRUD da tabela que especificarmos.

Acesso

As vantagens de utilizar esta forma é que facilita bastante acessar a edição em lote através dos registros do CRUD, para isso devemos fazer a seguinte configuração

  • Acessar a tabela no nfs_core_ds_tabela_campo;
  • Criar um campo do tipo LINK
  • Adicionar na coluna opção da tabela que desejamos adicionar o link para edição em lote a seguinte configuração: type:GET_URL;popup:1;url:batch/mo_boletim/;v1:SEQ_DB;icon:fa fa-th-list

Exemplo de Configuração

Configuração do campo para exibição do link em uma tabela de Boletim

INSERT INTO nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('BOLETIM_MAQUINA', 'LINK_EDICAO_LOTE', NULL, 0, 1, 1, 1, 'Edição em lote', 'Edição em lote', 'LINK', NULL, NULL, NULL, NULL, 'IU', '0', NULL, NULL, NULL, 'type:GET_URL;popup:1;url:batch/boletim_maquina/;v1:SEQ_DB;icon:fa fa-th-list', NULL, NULL, NULL, NULL, NULL);
caso seja necessário ocultar o link como no caso de um usuário read-only(apenas permissão de select na tabela) adicione o parâmetro: ;disabledReadOnly:true; ao final da configuração mostrada acima.

Feito isso os ícones para edição em lote serão exibidos quando acessarmos o CRUD novamente, como no exemplo abaixo: b227c55f51e25789f6c5c3359296de82.png

Acesso sem configuração

Este é o acesso mais pratico, não necessita de configuração alguma, para faze-lo basta acessarmos o recurso batch na url do nosso ambiente, passando a / (barra) e em seguida o nome da tabela que desejamos acessar, desta forma: /batch/tabela c24c0e6a86eb757b2f763f74daf0e3c8.png

Quando não passamos nenhum SEQ_DB, a tela de edição em lote abrira no modo "em branco" para podermos inserir um novo registro nesta tabela.

Agora se passarmos um SEQ_DB, a tela de edição em lote ira abrir o registro da tabela que possui esse SEQ_DB, definir a tabela como principal e por fim, irá trazer as tabelas relacionadas a tabela principal e seus registros: 24eae3d5545d8ba9ade87d3950637074.png e8367de109e1a2e9310147effa1626f2.png

Estrutura do componente

A ideia é que a edição em lote sirva como uma especie de "canivete suíço" quando se trata da manipulação dos registros das tabelas, onde podemos usar a maior parte das ações sem precisar abrir uma infinidade uma infinidade de abas no navegador.

Para dominarmos o seu uso, vamos apresentar um pouco mais sobre a sua estrutura e ações possíveis.

Tabela Mestre(Master)

Esta é a tabela que possui o registro principal o qual os registros das sub-tabelas do componente farão vinculo.

Caso seja acessado sem utilizar o SeqDb registro, será aberto o componente no modo de registro em branco.

5b1113782e84ce12ab2acc8dd3dd8af3.png

Legendas:

  1. Titulo do componente de edição em lote: EDIÇÂO EM LOTE {DESCRIÇÂO DA TABELA MESTRE};
  2. Tab atual, esta ficará marcada com um tom de cinza mais escuro;
  3. Outras tabs disponíveis;
  4. Descrição da tabela seguida pelos botões de ação "Novo Registro Mestre" e "Recarregar registros" sucessivamente;
  5. Pesquisa geral: pesquisa em todas as colunas da tabela;
  6. Filtro por coluna: Filtra os itens de acordo com a pesquisa feita nos inputs relacionados a cada coluna;
  7. Registro da tabela, sendo a sua primeira coluna, ações disponíveis para aquele registro.

Ao clicarmos no botão "Novo Registro Mestre", iremos abrir a tela de registro em branco.

Tabela Secundaria

17e43b15a7d23f070f5e7139b4f0194d.png Possui basicamente a mesma estrutura da tabela mestre - link aki, porem, com duas pequenas diferenças:

  1. Possui um contador de registros selecionados após a descrição da tabela e o botão "Novo registro" irá abrir um modal para que possamos criar este novo registro - link aki;
  2. Caixa de seleção para selecionarmos quais registros desejamos editar.

Registro em Branco

Esta tela possui apenas o componente de Modal com os campos para criarmos um novo registro da tabela Mestre. fd879a6826cc17769375a5b658b0a0db.png

  1. Titulo do Modal;
  2. Formulário para criação do registro;
  3. Botão de ação para persistir as alterações.

Como utilizar

Edição de registros:

Mestre

Para editarmos o nosso registro principal basta acessar a tela de edição em lote e clicar em cima do unico registro que estará disponivel: 178663873a7e62e3d375a15ebaa87003.png

Feito isso o modal de edição irá aparecer na tela: f1bd21a810e9a2e0b63c140e74995f83.png

Agora basta inserirmos os dados que queremos alterar e clicar em confirmar para persistir as alterações.

Secundarios

Para editarmos registros secundários, devemos acessar a tabela destes registros através das abas na edição em lote, feito isso, selecionamos quais registros queremos editar e por fim clicamos em cima de qualquer um dos registros, novamente o modal de edição irá surgir, porem ao persistir as mudanças as alterações serão aplicadas em todos os registros marcados como selecionados: 6732ab280e32f6279199ed83696c6f4c.png

Inserção de registros:

Mestre

Para inserirmos um novo registro "Mestre":

  • Acessar a tabela principal através das abas na edição em lote;
  • Clicar no botão "Novo Registro Mestre" - isto fará com que pagina de inserção de registro em branco sejá aberta
  • Na pagina que abriu, inserimos os dados do registro;
  • Clicamos em confirmar para persistir as alterações;
  • Caso a inserção seja concluída com sucesso, o usuário será redirecionada para tela de edição em lote do novo registro.

Secundários

Para inserirmos novos registros secundários:

  • Acessar a tabela secundaria através das abas na edição em lote;
  • Clicamos no botão "Novo Registro" - o modal para inserção dos dados do registro irá aparecer;
  • Inserimos os dados e clicamos em confirmar para persistir as alterações. creation

Caso tudo ocorra bem, uma mensagem de sucesso ira aparecer na tela e a lista de registros será atualizada.

Clonagem:

  • Clique no ícone de clonagem de registro que fica na coluna de ações do registro;
  • Confirme a ação;
  • Uma mensagem de sucesso será exibida caso de certo;
  • A lista será atualizada com o novo registro.

Deletando registros:

  • Clique no ícone de "X" que fica na coluna de ações do registro;
  • Confirme a ação;
  • Uma mensagem de sucesso será exibida caso de certo;
  • A lista será atualizada com o registro removido.

A clonagem e exclusão de registros só são possiveis em tabelas secundarias {.is-warning}

Configurações adicionais

Algumas configurações extras podem ser feitas para tratar alguns comportamentos do nosso componente de edição em lote, essas configurações devem ser adicionadas dentro da propriedade batch no nfs_core_ds_tabela da nossa tabela mestre:

/** Valor padrão deve ser um objeto vazio*/
{
    "batch": {}
}

Batch é o nome interno do componente de edição em lotes e significa 'Lote', na área de T.I este termo tambem pode ser associado a um conjunto de comandos executados em lote(sequencialmente) por arquivos .bat ou .cmd. {.is-info}

Não exibir tabelas secundarias especificas:

Para remover alguma tabela que não queremos exibir na edição em lote, podemos utilizar a propriedade ignoreTables na nossa configuração do batch, sendo seu valor um array onde podemos passar quais serão as tabelas que serão ignoradas:

{
     "batch": {
       "ignoreTables":["APONTAMENTO_MO_MENSAGEM", "MO_APT_EQP"]
     }
}

Exibir campos do sistema(SYS = 1)

Para adicionar um campo que não esta sendo exibido na tela de visualização, temos que adicionar na configuração do batch a propriedade displaySystemFields, a qual possui a seguinte estrutura:

/** Exibir o campo para todas as tabelas que o tiverem */
{
   "batch":{
      "displaySystemFields": {
         "CAMPO":[]
      }
   }
}

/** Exibir o campo em tabelas especificas */
{
   "batch":{
      "displaySystemFields": {
         "CAMPO":["TABELA1","TABELA2"]
      }
   }
}

Configurações de exibição: Lista/Edição/Inserção

Podemos configurar quais campos desejamos exibir em cada etapa do processo de edição em lote, para isso devemos primeiramente adicionar a propriedade customTableConfig na nossa configuração do batch e assim definirmos qual a tabela que desejamos fazer as configurações:

{
   "batch":{
      "customTableConfig":{
         "MINHA_TABELA":{}
      }
   }
}

Lista de Registros

{
   "batch":{
      "customTableConfig":{
         "MO_APT":{
            "DISPLAY_FIELDS":[
               "INI_DH",
               "FIM_DH",
               "INI_FIM_DIFF_STR",
               "EFETIVO_FUNCIONARIO",
               "OPERADOR",
               "FUNCAO",
               "OPER_GRUPO",
               "OPER",
               "INTERFERENCIA",
               "TP_INTERFERENCIA",
               "OBSERVACAO_INTERFERENCIA",
               "FOLHA_TAREFA"
            ]
         }
      }
   }
}
{
   "batch":{
      "customTableConfig":{
         "MO_APT":{
            "DISPLAY_FIELDS":[
               "INI_DH",
               "FIM_DH",
               "INI_FIM_DIFF_STR",
               "EFETIVO_FUNCIONARIO",
               "OPERADOR",
               "FUNCAO",
               "OPER_GRUPO",
               "OPER",
               "INTERFERENCIA",
               "TP_INTERFERENCIA",
               "OBSERVACAO_INTERFERENCIA",
               "FOLHA_TAREFA"
            ]
         }
      }
   }
}
{
   "batch":{
      "customTableConfig":{
         "MO_APT":{
            "INSERTABLE_FIELDS":[
               "INI_DH",
               "EFETIVO_FUNCIONARIO",
               "OPERADOR",
               "SEQ_DB_DEVICE_MASTER",
               "SEQ_DB_DEVICE"
            ]
         }
      }
   }
}

Exemplo de configuração completa

{
   "batch":{
      "ignoreTables":[
         "APONTAMENTO_MO_MENSAGEM"
      ],
      "displaySystemFields":{
         "INI_FIM_DIFF_STR":[
            "MO_APT"
         ]
      },
      "customTableConfig":{
         "MO_APT":{
            "DISPLAY_FIELDS":[
               "INI_DH",
               "FIM_DH",
               "INI_FIM_DIFF_STR",
               "EFETIVO_FUNCIONARIO",
               "OPERADOR",
               "FUNCAO",
               "OPER_GRUPO",
               "OPER",
               "INTERFERENCIA",
               "TP_INTERFERENCIA",
               "OBSERVACAO_INTERFERENCIA",
               "FOLHA_TAREFA"
            ],
            "EDITABLE_FIELDS":[
               "INI_DH",
               "INI_FIM_DIFF_STR",
               "EFETIVO_FUNCIONARIO",
               "OPERADOR",
               "FUNCAO",
               "OPER_GRUPO",
               "OPER",
               "INTERFERENCIA",
               "TP_INTERFERENCIA",
               "OBSERVACAO_INTERFERENCIA",
               "FOLHA_TAREFA",
               "PROJETO",
               "TIPO_APONTAMENTO",
               "FOLHA_TAREFA2"
            ],
            "INSERTABLE_FIELDS":[
               "INI_DH",
               "EFETIVO_FUNCIONARIO",
               "OPERADOR",
               "SEQ_DB_DEVICE_MASTER",
               "SEQ_DB_DEVICE"
            ]
         }
      }
   }
}

Envio de SMS

Hoje é possível realizar o envio de SMS pelo NFS para um celular de terceiro, antes de qualquer coisa é necessário avaliar junto ao Tiago Pinheiro essa necessidade e ele deve aprovar, porque usamos o Zenvia ou Allcance, que geram custo adicional.

Envio de SMS pelo Zenvia no NFS

Foi criada uma opção de envio de SMS para um terceiro através do Zenvia. Para isso é necessário configurar o parâmetro SMS_ZENVIA_CONFIG na nfs_core_par_parametros sendo um json com from e token:

{ "from": "meu_user", "token" : "meu_token"}

Esses dados vão servir para autenticar junto a API do Zenvia e para enviar um sms basta usar o entry point abaixo:

$zenvia = new ZenviaSmsService();
$smsData = new SmsData('CODIGOPais_DDD_NUMERO', 'Last test');
$result = $zenvia->send($smsData);
print_r($result);

SmSData é um modelo de dados que no seu construtor espera no primeiro parâmetro o número completo do usuário que vai receber a mensagem, sendo assim com o código do país, ddd e número e o segundo parâmetro é a mensagem.

No serviço ZenviaSmsService, ao instancia temos como parâmetro o modelo SmsData que contém os dados necessário para envio.

Para saber mais: https://zenvia.github.io/zenvia-openapi-spec/v2/#tag/SMS

Envio de SMS pelo Allcance no NFS

Foi criada uma opção de envio de SMS para um terceiro através do Allcance. Para isso é necessário configurar o parâmetro SMS_ALLCANCE_CONFIG na nfs_core_par_parametros sendo um json com usuário e senha na chave "login", e, um token na chave "token", caso o cliente tenha disponível um token personalizado sem data de expiração:

{
    "login": { 
        "username": "email",
        "password": "senha"
    },
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIU..."
}

Obs: Em caso de preenchimento da chave "token", essa será priorizada em relação ao uso dos dados de "login"!

Caso seja para testes preliminares é possível você mesmo fazer um cadastro para testes, se não me engano tem direito de 10 sms por dia no horário comercial.

Cada SMS é composto por 152 caracteres, logo se enviar mais do que isso será considerado mais de um, um ponto importante é que para caracteres unicode a cobrança é diferente, sendo 72 representando um SMS, com isso a cobranã pode sair de 2 para 5, eleveando o custo.

Para saber mais entre no site deles: Allcance

Estão disponíveis no CORE dois métodos para uso da classe AllcanceSmsService:

/**
* Envia um texto SMS simples definindo o destino e o texto da mensagem.
*
* @param SmsData $smsData Entidade modelo contendo o destino e o conteúdo do SMS
*/
public function send(SmsData $smsData): string {...}
/**
 * Retorna o saldo de todos os serviços habilitados para a conta/token configurado.
 */
public function getCredits(): string {...}

No entry point o código será algo similar a:

//Envio do SMS
$allcanceService = new AllcanceSmsService();
$smsData = new SmsData('DDD_NUMERO', 'Last test');
$result = $allcanceService->send($smsData);
print_r($result);
//Consulta ao SALDO
$allcanceService = new AllcanceSmsService();
$result = $allcanceService->getCredits();
print_r($result);

SmSData é um modelo de dados que no seu construtor espera no primeiro parâmetro o número completo do usuário que vai receber a mensagem, sendo assim com o ddd e número e o segundo parâmetro é a mensagem.

Envio de Email

Configuração tabela nfs_core_par_parametros

No envio dos e-mails pode ser configurado um remetente, à partir do campo EMAIL_REMETENTE localizado na tabela nfs_core_par_parametros, exemplo:

EMAIL_REMETENTE = "Primavera Máquinas atendimento@primavera.com.br"

  • Executar o comando abaixo para inserir (caso não tenha) o campo EMAIL_REMETENTE na tabela nfs_core_par_parametros, definindo o e-mail a ser configurado onde está meuemail@simova.com.br:

ATENÇÃO

Esse comando SQL gera os INSERT's para as bases do seu banco de dados, após executar o comando você deve copiar o resultado da base que deseja inserir e executar o comando geradono resultado.

Atenção aos valores:

  • $seqDbEmpresa -> é o número do SEQ_DB da empresa que está sendo configurado o envio de email, pode ser consultado na tabela nfs_org_empresa
  • $seqDbFilial -> SEQ_DB da filial
  • $seqDbLocal -> SEQ_DB local
  • $meuEmail -> email a ser inserido, lembrar de colocar entre aspas simples, pois é string.
select distinct concat('INSERT INTO ', t.TABLE_SCHEMA, '.', 'nfs_core_par_parametros (EMPRESA, FILIAL, LOCAL, NOME, CONTEUDO, TIPO)
VALUES ($seqDbEmpresa, $seqDbFilial, $seqDbLocal, 'EMAIL_REMETENTE', '$meuEmail', 1)') from information_schema.TABLES t where t.TABLE_NAME = 'nfs_core_sys_tipo_campo';

O correto é solicitar para cada cliente um endereço de e-mail para ser utilizado, exemplo: atendimento@primavera.com.br e então colocar essa informação no parâmetro EMAIL_REMETENTE.

Se o parâmetro não existir seguirá o processo atual enviando por contato@simova.com.br ou no-reply@simova.cloud

A hierarquia de empresa/filial/local é respeitada, ou seja, é possível ter um e-mail para cada local/filial.

Filtro Único (Filtro Global)

A funcionalidade 'FILTRO ÚNICO', quando habilitada, insere na barra de navegação superior do painel nfs um campo do tipo select que permite filtrar as informações de todas as telas que possuem ligação com os dados da tabela configurada como Filtro Único, funcionando como um filtro global para a aplicação.

Habilitando a funcionalidade:

Para habilitar a funcionalidade na aplicação do cliente, é preciso definir o parâmetro 'SINGLE_FILTER_ENABLED' na tabela 'nfs_core_par_parametros', onde na coluna "CONTEUDO" o valor "1" deve ser atribuido.

Abaixo segue um exemplo de SQL INSERT de como configurar a tabela 'nfs_core_par_parametros':

INSERT INTO `nfs_core_par_parametros` (`EMPRESA`, `FILIAL`, `LOCAL`, `NOME`, `CONTEUDO`, `TIPO`) VALUES (9999, 9999, 9999, 'SINGLE_FILTER_ENABLED', '1', 1);

Configuração:

Para ter acesso a tabela de configuração do Filtro Único, é precisar acessar e rodar em 'Admin console -> NFS ENVIRONMENT > DS/DDL (Full)' para criação da tabela 'NFS_SINGLE_FILTER' e então informar para quais ambientes a funcionalidade deve rodar, e, qual tabela utilizar como parâmetro para realizar o filtro dos dados.

Ao inserir um registro na tabela 'NFS_SINGLE_FILTER' e configurar uma tabela como filtro global, serão carregados no filtro único da barra superior de navegação todos os registros daquela tabela marcados como 'ATIVO'.

Segue abaixo um exemplo simplificado de registro na tabela 'NFS_SINGLE_FILTER':

INSERT INTO `nfs_single_filter` (`SEQ_DB`, `EMPRESA`, `FILIAL`, `LOCAL`, `TABELA`, `ATIVO`) VALUES (1, 1, 9999, 9999, 'PROJETO', 1);

Notas:

[!TIP] Quando o filtro está ativo e selecionado, apenas os registros e relatórios que possuem ligação com a tabela escolhida como Filtro único sofrerão seus efeitos.

[!TIP] Na tela de listagem de registros (CRUD), quando um ‘Filtro Único’ está selecionado, os campos do form 'Filtrar por coluna' também serão afetados por ele, assim, quaisquer campo do tipo 'FK’ que tenha ligação com a tabela configurada no ‘Filtro Único’ sofrerá seus efeitos, listando apenas valores ligados ao filtro único selecionado.

[!IMPORTANT] Quando ouver um campo no form 'Filtrar por coluna' cuja tabela é a mesma da tabela configurada como 'Filtro Único', e o mesmo estiver preenchido, o campo no form 'Filtrar por coluna' terá prioridade sobre o 'Filtro Único' selecionado.

[!TIP] Enquanto estiver em uma tela de listagem de registros, e, optar por selecionar um novo valor para 'Filtro Único', será necessário recarregar a página para que haja alteração na tela.

[!TIP] O valor do último 'Filtro Único' selecionado é salvo localmente no navegador do usuário, e, esse valor é recuperado sempre que a sessão no painel é iniciada, podendo assim diferentes usuários utilizarem diferentes valores de 'Filtro Único' em um mesmo painel.

Introdução

Configuração

Para a configuração da Folha Tarefa / Associação Tarefa nos clientes deve-se criar as tabelas padrão, configurar o menu de Folha Tarefa e o de Atividade x Méticas, e configurar o link para a tela Atividade X Métrica, para que a funcionalidade seja utilizada da forma correta e sem problemas. Estes itens estão descritos em detalhes nos tópicos abaixo:

Criação das Tabelas

[!NOTE] OBS: As configurações descritas no código segue o modelo padrão de nomenclatura de tabelas, mas existem bases onde as tabelas tem o prefixo MO na nomenclatura, para que funcione é necessário configurar os nomes das tabelas na tabela nfs_core_par_parametros para subtituir na configuração original como no exemplo abaixo:

      NOME                                   CONTEUDO
 -----------------------------------------------------------------
 ASSOCIATION_TASK_OPER               |  MO_OPER
 -----------------------------------------------------------------
 ASSOCIATION_TASK_OPER_GRUPO         |  MO_OPER_GRUPO
 -----------------------------------------------------------------
 ASSOCIATION_TASK_OPER_GRUPO_METRICA |  MO_OPER_GRUPO_METRICA
 -----------------------------------------------------------------
 ASSOCIATION_TASK_OPER_OPER_GRUPO    |  MO_OPER_MO_OPER_GRUPO

1. app_oper

CREATE TABLE `app_oper` (
  `SEQ_DB` bigint unsigned NOT NULL AUTO_INCREMENT,
  `EMPRESA` int NOT NULL,
  `FILIAL` int NOT NULL,
  `LOCAL` int NOT NULL,
  `INS_DH` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `INS_USUARIO_SEQ_DB` bigint unsigned NOT NULL,
  `UPD_DH` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  `UPD_USUARIO_SEQ_DB` bigint unsigned DEFAULT NULL,
  `SOURCE` varchar(50) DEFAULT NULL,
  `ATIVO` tinyint(1) DEFAULT '1',
  `DELETED` tinyint(1) DEFAULT '0',
  `RO` tinyint(1) DEFAULT '0',
  `ID` int unsigned NOT NULL,
  `CODIGO` varchar(50) DEFAULT NULL,
  `DESCRICAO` varchar(250) DEFAULT NULL,
  `UNIDADE_MEDIDA_SEQ_DB` bigint unsigned DEFAULT NULL,
  `QIG_DISPLAY` tinyint unsigned DEFAULT '1',
  `REVISAO_DH` timestamp NULL DEFAULT NULL,
  `MO_APT_FKL` bigint unsigned DEFAULT NULL,
  `MO_APT_FKL_DH` timestamp NULL DEFAULT NULL,
  `MO_APT_PRODUCAO_FKL` bigint unsigned DEFAULT NULL,
  `MO_APT_PRODUCAO_FKL_DH` timestamp NULL DEFAULT NULL,
  `INDICE` decimal(11,2) DEFAULT NULL,
  PRIMARY KEY (`EMPRESA`,`FILIAL`,`LOCAL`,`ID`),
  UNIQUE KEY `SEQ_DB_UNIQUE` (`SEQ_DB`),
  KEY `fk_oper_unidade_medida` (`UNIDADE_MEDIDA_SEQ_DB`),
  CONSTRAINT `app_oper_ibfk_1` FOREIGN KEY (`UNIDADE_MEDIDA_SEQ_DB`) REFERENCES `app_unidade_medida` (`SEQ_DB`)
) ENGINE=InnoDB AUTO_INCREMENT=22439 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=COMPACT;

2. app_oper_grupo

CREATE TABLE `app_oper_grupo` (
  `SEQ_DB` bigint unsigned NOT NULL AUTO_INCREMENT,
  `EMPRESA` int NOT NULL,
  `FILIAL` int NOT NULL,
  `LOCAL` int NOT NULL,
  `INS_DH` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `INS_USUARIO_SEQ_DB` bigint unsigned NOT NULL,
  `UPD_DH` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  `UPD_USUARIO_SEQ_DB` bigint unsigned DEFAULT NULL,
  `SOURCE` varchar(50) DEFAULT NULL,
  `ATIVO` tinyint(1) DEFAULT '1',
  `DELETED` tinyint(1) DEFAULT '0',
  `RO` tinyint(1) DEFAULT '0',
  `ID` int unsigned NOT NULL,
  `CODIGO` varchar(100) DEFAULT NULL,
  `DESCRICAO` varchar(250) DEFAULT NULL,
  `TIPO` char(1) DEFAULT '',
  `OPER_GRUPO_SEQ_DB` bigint unsigned DEFAULT NULL,
  `REVISAO_DH` timestamp NULL DEFAULT NULL,
  `ETAPA_SEQ_DB` bigint unsigned DEFAULT NULL,
  `MO_APT_FKL` bigint unsigned DEFAULT NULL,
  `MO_APT_FKL_DH` timestamp NULL DEFAULT NULL,
  `MO_APT_PRODUCAO_FKL` bigint unsigned DEFAULT NULL,
  `MO_APT_PRODUCAO_FKL_DH` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`EMPRESA`,`FILIAL`,`LOCAL`,`ID`),
  UNIQUE KEY `SEQ_DB_UNIQUE` (`SEQ_DB`),
  KEY `FK_app_oper_grupo_app_oper_grupo` (`OPER_GRUPO_SEQ_DB`),
  KEY `fk_oper_grupo_etapa` (`ETAPA_SEQ_DB`),
  CONSTRAINT `FK_app_oper_grupo_app_oper_grupo` FOREIGN KEY (`OPER_GRUPO_SEQ_DB`) REFERENCES `app_oper_grupo` (`SEQ_DB`) ON DELETE RESTRICT,
  CONSTRAINT `fk_oper_grupo_etapa` FOREIGN KEY (`ETAPA_SEQ_DB`) REFERENCES `app_etapa` (`SEQ_DB`) ON DELETE RESTRICT
) ENGINE=InnoDB AUTO_INCREMENT=4865 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;

3. app_oper_grupo_metrica

CREATE TABLE `app_oper_grupo_metrica` (
  `SEQ_DB` bigint unsigned NOT NULL AUTO_INCREMENT,
  `EMPRESA` int NOT NULL,
  `FILIAL` int NOT NULL,
  `LOCAL` int NOT NULL,
  `INS_DH` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `INS_USUARIO_SEQ_DB` bigint unsigned NOT NULL,
  `UPD_DH` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  `UPD_USUARIO_SEQ_DB` bigint unsigned DEFAULT NULL,
  `SOURCE` varchar(50) DEFAULT NULL,
  `ATIVO` tinyint(1) DEFAULT '1',
  `DELETED` tinyint(1) DEFAULT '0',
  `RO` tinyint(1) DEFAULT '0',
  `ID` int unsigned NOT NULL,
  `OPER_SEQ_DB` bigint unsigned DEFAULT NULL,
  `OPER_GRUPO_SEQ_DB` bigint unsigned DEFAULT NULL,
  `FOLHA_TAREFA_SEQ_DB` bigint unsigned DEFAULT NULL,
  `METRICA1` decimal(10,2) DEFAULT NULL,
  `METRICA2` int DEFAULT NULL,
  PRIMARY KEY (`EMPRESA`,`FILIAL`,`LOCAL`,`ID`),
  UNIQUE KEY `SEQ_DB_UNIQUE` (`SEQ_DB`),
  KEY `fk_oper_grupo_metrica_folha_tarefa` (`FOLHA_TAREFA_SEQ_DB`),
  KEY `fk_oper_grupo_metrica_oper` (`OPER_SEQ_DB`),
  KEY `fk_oper_grupo_metrica_oper_grupo` (`OPER_GRUPO_SEQ_DB`),
  CONSTRAINT `fk_oper_grupo_metrica_folha_tarefa` FOREIGN KEY (`FOLHA_TAREFA_SEQ_DB`) REFERENCES `app_folha_tarefa` (`SEQ_DB`),
  CONSTRAINT `fk_oper_grupo_metrica_oper` FOREIGN KEY (`OPER_SEQ_DB`) REFERENCES `app_oper` (`SEQ_DB`),
  CONSTRAINT `fk_oper_grupo_metrica_oper_grupo` FOREIGN KEY (`OPER_GRUPO_SEQ_DB`) REFERENCES `app_oper_grupo` (`SEQ_DB`)
) ENGINE=InnoDB AUTO_INCREMENT=467 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;

4. app_oper_n_oper_grupo

CREATE TABLE `app_oper_n_oper_grupo` (
  `SEQ_DB` bigint unsigned NOT NULL AUTO_INCREMENT,
  `EMPRESA` int NOT NULL,
  `FILIAL` int NOT NULL,
  `LOCAL` int NOT NULL,
  `INS_DH` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `INS_USUARIO_SEQ_DB` bigint unsigned NOT NULL,
  `UPD_DH` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  `UPD_USUARIO_SEQ_DB` bigint unsigned DEFAULT NULL,
  `SOURCE` varchar(50) DEFAULT NULL,
  `ATIVO` tinyint(1) DEFAULT '1',
  `DELETED` tinyint(1) DEFAULT '0',
  `RO` tinyint(1) DEFAULT '0',
  `ID` int unsigned NOT NULL,
  `OPER_GRUPO_SEQ_DB` bigint unsigned DEFAULT NULL,
  `OPER_SEQ_DB` bigint unsigned DEFAULT NULL,
  `DESCRICAO` varchar(250) DEFAULT NULL,
  `REVISAO_DH` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`EMPRESA`,`FILIAL`,`LOCAL`,`ID`),
  UNIQUE KEY `SEQ_DB_UNIQUE` (`SEQ_DB`),
  KEY `FK_app_oper_n_oper_grupo_app_oper_grupo` (`OPER_GRUPO_SEQ_DB`),
  KEY `FK_app_oper_n_oper_grupo_app_oper` (`OPER_SEQ_DB`),
  CONSTRAINT `FK_app_oper_n_oper_grupo_app_oper` FOREIGN KEY (`OPER_SEQ_DB`) REFERENCES `app_oper` (`SEQ_DB`),
  CONSTRAINT `FK_app_oper_n_oper_grupo_app_oper_grupo` FOREIGN KEY (`OPER_GRUPO_SEQ_DB`) REFERENCES `app_oper_grupo` (`SEQ_DB`)
) ENGINE=InnoDB AUTO_INCREMENT=12044 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;

IMPORTANTE! Esse modelo de tabelas é o padrão, não podendo ser alterado, pois esta funcionalidade ainda não é customizável. {.is-danger}

Configuração dos menus Folha Tarefa e Atividade x Métricas

A configuração dos menus é feita na tabela nfs_core_menu, o exemplo abaixo é referente ao que foi cadastrado na base Egenko:

OBS:Não deve ser copiado e executado, pois em outras bases os campos podem não ser correspondentes fazendo com que não funcione como o esperado, este é apenas um exemplo.

  INSERT INTO {{minha base}}.nfs_core_menu (EMPRESA,FILIAL,`LOCAL`,DESCRICAO,`TYPE`,ATIVO,FATHER,MENUORDER,URL,`FILTER`,ICON) VALUES
    (9999,9999,9999,'Folha Tarefa','CRUD',1,3,5,NULL,'{"allowed":[
    "FOLHA_TAREFA",
    "OPER_GRUPO_METRICA"
    ]
  }','fa fa-users'),
    (9999,9999,9999,'Folha Tarefa','LINK',1,500,NULL,'reports/parte_diaria/filters',NULL,'fa fa-file-text-o'),
    (9999,9999,9999,'Folha Tarefa','CRUD',1,3,5,NULL,'{"allowed":[
    "FOLHA_TAREFA"
    ]
  }','fa fa-users'),
    (9999,9999,9999,'Atividades','CRUD',1,3,4,NULL,'{"allowed":[
    "OPER",
    "UNIDADE_MEDIDA",
    "OPER_GRUPO",
    "OPER_OPER_GRUPO",
    "ETAPA"
  ]}',NULL);

Configuração do Link Atividade x Métricas

Para configurar o link Atividade x Métrica deve-se inserir na tabela nfs_core_ds_tabela_campo a configuração abaixo:

  INSERT INTO {{minha base}}.nfs_core_ds_tabela_campo (TABELA_NOME,NOME,SEQ,SYS,SEND_XMOVA,GRID,GRID_MOBILE,DESCRICAO,DESCRICAO_RESUMIDA,TIPO,OBRIGATORIO,TAMANHO,TAMANHO_DECIMAL,MASCARA,INSERT_UPDATE,DISABLED,HINT,LINK,VALIDACAO,OPCOES,VALOR_DEFAULT,VALIDACAO_VIEW,FILTRO_VIEW,TOOLTIP_MESSAGE,PROPERTIES) VALUES
    ('FOLHA_TAREFA','LINK_ATIVIDADE_METRICAS',11,0,1,0,1,'Métricas Folha Tarefa','Métricas Folha Tarefa','LINK',NULL,NULL,NULL,NULL,'IU','0',NULL,'FOLHA_TAREFA','','type:GET_URL;url:associationtask;p1:taskSheet;v1:SEQ_DB;icon:fa fa-link',NULL,NULL,NULL,NULL,NULL);

Lembrando que no campo OPCOES deve conter o seguinte : {.is-warning}

type:GET_URL;url:associationtask;p1:taskSheet;v1:SEQ_DB;icon:fa fa-link

Após realizar os passos acima corretamente o ambiente já estará configurado para utilizar a ferramenta Folha Tarefa/Atividade x Métricas.

Associação de tarefas (custom)

Podemos fazer a configuração das tabelas que irão corresponder a cada nivel/camada da “arvore de atividades” na associação de tarefas, para utilizar basta configurar o parâmetro na tabela nfs_core_par_parametros e criar o link através da tabela nfs_core_ds_tabela_campo. exemplo de arquivo de configuração na tabela nfs_core_par_parametros:

INSERT INTO nfs_homol_civilmaster_mo_construmobil.nfs_core_par_parametros
(EMPRESA, FILIAL, `LOCAL`, NOME, CONTEUDO, TIPO)
VALUES(9999, 9999, 9999, 'TREE_VIEW', '{
   "layers":{
      "1":{
         "table":"AREA",
         "associationTable":"DISTRIBUICAO_OBJETO"
      },
      "2":{
         "table":"OBJETO",
         "associationTable":"DETALHAMENTO_OBJETO"
      },
      "3":{
         "table":"OPER",
         "isTask":true
      }
   }
}', 1);

agora basta adicionar o link ao registro:

INSERT INTO nfs_homol_milplanengenharia_construmobil.nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('FOLHA_TAREFA', 'LINK_ATIVIDADE_METRICAS', 11, 0, 1, 0, 1, 'Métricas Folha Tarefa', 'Métricas Folha Tarefa', 'LINK', NULL, NULL, NULL, NULL, 'IU', '0', NULL, 'FOLHA_TAREFA', '', 'type:GET_URL;url:associationtask;p1:taskSheet;v1:SEQ_DB;icon:fa fa-link', NULL, NULL, NULL, NULL, NULL);

Configuração

config.png Legendas:

  1. layers(camadas): Atributo onde serão definidas as tabelas de cada camada, bem como a tabela que fara a associação com os níveis “filho”(subnivel do nivel atual), quando for uma task, usaremos o propriedade isTask: true no lugar ta tabela de associação;

  2. niveis: Os niveis sempre irão contar no menos para o maior, na estrutura de arvore o nivel 1 seria a raiz(ou root), não possuindo pai e de onde os proximos niveis serão ramificados;

  3. nivel: Um nível corresponde a alguma tabela que será usada para para relacionar um nivel aos demais, fazendo a ligação entre os nós “pai”(que antecedem) e “filhos”(que sucedem);

  4. Campos que serão utilizados como métricas.

  5. valores padrão(default): permite difinir um valor padrão para as metricas, possuindo os seguintes tipos:

    • value: um valor fixo que será utilizado ao abrir o modal para inserção das metricas;
    • field: o nome de um campo da tabela de atividade;
    • taskSheet: o nome de um campo correspondente aos campos da folha tarefa.

Caso queira definir a tabela final onde sera salva a metrica, basta adicionar a propriedade metricAssociationTable desta forma:

{ 
	 'metricAssociationTable':"OPER_GRUPO_METRICA",
   'layers':{
      "1":{
         "table":"AREA",
         "associationTable":"DISTRIBUICAO_OBJETO"
      },
      "2":{
         "table":"OBJETO",
         "associationTable":"DETALHAMENTO_OBJETO"
      },
      "3":{
         "table":"OPER",
         "isTask":true
      }
   }
   ...
}

Associação de tarefas

Podemos fazer a configuração das tabelas que irão corresponder a cada nivel/camada da “arvore de atividades” na associação de tarefas, para utilizar basta configurar o parâmetro na tabela nfs_core_par_parametros e criar o link através da tabela nfs_core_ds_tabela_campo. exemplo de arquivo de configuração na tabela nfs_core_par_parametros:

INSERT INTO nfs_core_par_parametros(EMPRESA, FILIAL, `LOCAL`, NOME, CONTEUDO, TIPO)
VALUES(9999, 9999, 9999, 'TREE_VIEW', '{
   "layers":{
      "1":{
         "table":"AREA",
         "associationTable":"DISTRIBUICAO_OBJETO"
      },
      "2":{
         "table":"OBJETO",
         "associationTable":"DETALHAMENTO_OBJETO"
      },
      "3":{
         "table":"OPER",
         "isTask":true
      }
   }
}', 1);

agora basta adicionar o link ao registro:

INSERT INTO nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('FOLHA_TAREFA', 'LINK_ATIVIDADE_METRICAS', 11, 0, 1, 0, 1, 'Métricas Folha Tarefa', 'Métricas Folha Tarefa', 'LINK', NULL, NULL, NULL, NULL, 'IU', '0', NULL, 'FOLHA_TAREFA', '', 'type:GET_URL;url:associationtask;p1:taskSheet;v1:SEQ_DB;icon:fa fa-link', NULL, NULL, NULL, NULL, NULL);

Configuração

config.png

Legendas:

  1. layers(camadas): Atributo onde serão definidas as tabelas de cada camada, bem como a tabela que fara a associação com os níveis “filho”(subnivel do nivel atual), quando for uma task, usaremos o propriedade isTask: true no lugar ta tabela de associação;

  2. niveis: Os niveis sempre irão contar no menos para o maior, na estrutura de arvore o nivel 1 seria a raiz(ou root), não possuindo pai e de onde os proximos niveis serão ramificados;

  3. nivel: Um nível corresponde a alguma tabela que será usada para para relacionar um nivel aos demais, fazendo a ligação entre os nós “pai”(que antecedem) e “filhos”(que sucedem);

  4. Campos que serão utilizados como métricas.

  5. valores padrão(default): permite difinir um valor padrão para as metricas, possuindo os seguintes tipos:

    • value: um valor fixo que será utilizado ao abrir o modal para inserção das metricas;
    • field: o nome de um campo da tabela de atividade;
    • taskSheet: o nome de um campo correspondente aos campos da folha tarefa.

Caso queira definir a tabela final onde sera salva a metrica, basta adicionar a propriedade metricAssociationTable desta forma:

{ 

  "metricAssociationTable":"OPER_GRUPO_METRICA",
  "layers":{
    "1":{
        "table":"AREA",
        "associationTable":"DISTRIBUICAO_OBJETO"
    },
    "2":{
        "table":"OBJETO",
        "associationTable":"DETALHAMENTO_OBJETO"
    },
    "3":{
        "table":"OPER",
        "isTask":true
    }
  }
}

Associação de tarefa x métrica por grupo:

Uma propriedade adicional foi criada para tornar os grupos elementos “arrastáveis”

Configuração:

group_association;

draggable indica que o nó na arvore de associação pode ser movido(através do evento "drag") para a zona de atividades associadas.

group_association_folder;

A pasta colorida indica que o elemento pode ser "arrastado".

Ao arrastar um grupo para a zona de associação, o usuário devera escolher uma das duas opções disponiveis:

group_association_folder;

  • GRUPO: As métricas definidas serão aplicadas para todas as atividades do grupo;
  • ATIVIDADE: O usuário deverá definir manualmente a métrica de cada atividade do grupo.

Caso opte opção “GRUPO” e a atividade ja possua uma métrica associada, esta métrica sera sobrescrita pela métrica definida para o GRUPO

Introdução

O Dashboard Personalizado (Generic Dashboard) é uma ferramenta que permite montar dashboards de acordo com a necessidade dos clientes sem a intervenção do Core. Nessa ferramenta são usados os componentes HighCharts, responsável pela geração dos gráficos e o gridstack.js para a organização do Layout, possibilitando:

  1. Cria Dashboards específicos e seus respectivos gráficos;
  2. Obter dados específicos usando consultas anônimas (não foi adotada a classe EntryPoint do NFS Core);
  3. Salvar as configuração dos gráficos, permitindo alterá-los a qualquer momento;
  4. Montar Gráficos usando as configurações e os dados obtidos;
  5. Permitir alterar a disposição/tamanho dos gráficos.

Pré-requesitos

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

  1. Módulo "Rotas Personalizada" habilitado e rota personalizada para "/custom/genericDashboad/" criada (veja como);
  2. Tabelas "nfs_generic_dashboard" e "nfs_generic_chart" criadas na seguinte estrutura:
CREATE TABLE `nfs_generic_dashboard` (
  `SEQ_DB` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `EMPRESA` int(11) DEFAULT NULL,
  `FILIAL` int(11) DEFAULT NULL,
  `LOCAL` int(11) DEFAULT NULL,
  `TITLE` varchar(100) DEFAULT NULL,
  `FILTERS` text,
  `OPTIONS` text,
  `LAYOUT` text,
  `ENABLED` int(1) DEFAULT '1',
  UNIQUE KEY `SEQ_DB_UNIQUE` (`SEQ_DB`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;

CREATE TABLE `nfs_generic_chart` (
  `SEQ_DB` bigint(20) NOT NULL AUTO_INCREMENT,
  `DASHBOARD_SEQ_DB` bigint(20) NOT NULL,
  `EMPRESA` int(11) NOT NULL,
  `FILIAL` int(11) NOT NULL,
  `LOCAL` int(11) NOT NULL,
  `TITLE` varchar(100) NOT NULL,
  `CONFIG` text NOT NULL,
  `FILTERS` text,
  `QUERIES` text,
  `INS_DH` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `ENABLED` int(11) DEFAULT '1',
  `DELETED` int(11) DEFAULT '0',
  `OPTIONS` json NULL,
  `TYPE` enum('H','E','G') DEFAULT 'H',
  `CHART_ORDER` int(11) DEFAULT 999 NULL,
  UNIQUE KEY `SEQ_DB_UNIQUE` (`SEQ_DB`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;

Configurações Iniciais

Tabela nfs_generic_dashboard

Possui as definições principais do dashboard, sendo:

  1. TITLE: Título do Dashboard exibido no topo do módulo;
TITLE='Generic Dashboard'
  1. FILTERS: JSON com definições dos filtros;
{
  "inicio": {
    "type": "Date",
    "options": {
      "label": "De",
      "required": false,
      "language": "pt-BR"
    }
  },
  "fim": {
    "type": "Date",
    "options": {
      "label": "Até",
      "required": false,
      "language": "pt-BR"
    }
  },
  "equipamento": {
    "type": "Table",
    "options": {
      "label": "Equipamento",
      "required": true,
      "multiple": true
    }
  }
}
  1. OPTIONS: coluna criada para futuras personalizações.

  2. LAYOUT: coluna usada para salvar as definições de layout através do gridstack.js.

Tabela nfs_generic_chart

Tabela usada para armazenar as configurações dos gráficos dos dashboards, sendo:

  1. DASHBOARD_SEQ_DB: FK da tabela nfs_generic_dashboard;
DASHBOARD_SEQ_DB=1
  1. TITLE: Título do Gráfico;
TITLE='Generic Dashboard'
  1. CONFIG: Configuração do gráfico, variando de acordo com o tipo e opções do gráfico;
{
    "chart": {
        "type": "bar"
    },
    "title": {
        "text": "Atividades por Modelos"
    },
    "xAxis": {
        "categories": this.vals.categories
    },
    "yAxis": {
        "min": 0,
        "title": {
            "text": "Atividades por Modelos"
        }
    },
    "legend": {
        "reversed": true
    },
    "plotOptions": {
        "series": {
        "stacking": "normal"
        }
    },
    "series": this.vals.series
}
'

Os valores expressos por this.vals.categories e this.vals.series são usando como placeholders dentro das configurações do gráfico. Eles serão substituídos por dados obtidos das queries do gráfico. Todos os placeholders devem usar o préfixo "this.vals", seguido do nome da série, p.e. this.vals.<nome-da-serie>, sempre atentando para caixa baixa. As definições dos placeholders serão definidos na coluna QUERIES mais adiante. {.is-danger}

  1. FILTERS: JSON com definições dos filtros (similar ao usado em nfs_generic_dashboard, exceto que filtra apenas o gráfico ao qual pertence);
{
  "inicio": {
    "type": "Date",
    "options": {
      "label": "De",
      "required": false,
      "language": "pt-BR"
    }
  },
  "fim": {
    "type": "Date",
    "options": {
      "label": "Até",
      "required": false,
      "language": "pt-BR"
    }
  },
  "equipamento": {
    "type": "Table",
    "options": {
      "label": "Equipamento",
      "required": true,
      "multiple": true
    }
  }
}
  1. QUERIES: JSON contendo as definições dos placeholders especificados na coluna CONFIG (no exemplo, this.vals.categories e this.vals.series). Obs: Formato abaixo não é recomendado.
{
  "categories": {
    "CODE_TYPE": "PHP",
    "CONTENT": "$user = \\core\\xDS::getUser();\r\n$empresa = $user->EMPRESA_ATUAL;\r\n$local = $user->LOCAL_ATUAL;\r\n$filial = $user->FILIAL_ATUAL;\r\n$timezone = $this->user->TIMEZONE ?? date_default_timezone_get();\r\n\r\n$dIni = (empty($inicio)) ? new DateTime('-2 months', new DateTimeZone($timezone)) : DateTime::createFromFormat('d\\/m\\/Y', $inicio);\r\n$dFim = (empty($fim)) ? new DateTime('now', new DateTimeZone($timezone)) : DateTime::createFromFormat('d\\/m\\/Y', $fim);\r\n$comeco = date_format($dIni,'Y-m-d');\r\n$final = date_format($dFim,'Y-m-d');\r\n\r\nif (!empty($modelo)) {\r\n\t$modelos = (is_array($modelo)) ? ' and m.SEQ_DB in (' . implode(',', $modelo) . ') ' : \" and m.SEQ_DB = {$modelo} \";\r\n} else {\r\n\t$modelos = '';\t\r\n}\r\n\r\n$sql = \"SELECT \r\n\tdistinct m.DESCRICAO Modelo\r\n\tFROM app_apontamento_maquina a\r\n\tINNER JOIN app_equipamento e ON a.EQUIPAMENTO_SEQ_DB = e.SEQ_DB\r\n\tINNER JOIN app_modelo m ON e.MODELO_SEQ_DB = m.SEQ_DB\r\n\tWHERE DATE(a.INI_DH) between '{$comeco}' and '{$final}' \r\n\tand a.empresa = {$empresa} and a.local in ({$local},9999) and a.filial in ({$filial},9999) and a.ativo = 1 and a.deleted = 0\r\n\t{$modelos} GROUP BY m.SEQ_DB;\";\r\n\r\n$todosModelos = \\core\\ConexaoDB::query($sql, $params = []);\r\n$series = [];\r\nforeach ($todosModelos as $model){\r\n\tarray_push($series,$model['Modelo']);\r\n}\r\nreturn $series;"
  },
  "series": {
    "CODE_TYPE": "PHP",
    "CONTENT": "$user = \\core\\xDS::getUser();\r\n$empresa = $user->EMPRESA_ATUAL;\r\n$local = $user->LOCAL_ATUAL;\r\n$filial = $user->FILIAL_ATUAL;\r\n\r\n$timezone = $this->user->TIMEZONE ?? date_default_timezone_get();\r\n$dIni = (empty($inicio)) ? new DateTime('-2 months', new DateTimeZone($timezone)) : DateTime::createFromFormat('d\\/m\\/Y', $inicio);\r\n$dFim = (empty($fim)) ? new DateTime('now', new DateTimeZone($timezone)) : DateTime::createFromFormat('d\\/m\\/Y', $fim);\r\n$comeco = date_format($dIni,'Y-m-d');\r\n$final = date_format($dFim,'Y-m-d');\r\n\r\nif (!empty($modelo)) {\r\n\t$modelos = (is_array($modelo)) ? ' and m.SEQ_DB in (' . implode(',', $modelo) . ') ' : \" and m.SEQ_DB = {$modelo} \";\r\n} else {\r\n\t$modelos = '';\t\r\n}\r\n\r\n$series = [];\r\n$tipoAtividades = \"SELECT\r\n\tDISTINCT (case when (ati.FLAG_PRODUTIVA = 1) then 'Produtiva' \r\n\twhen (ati.FLAG_PRODUTIVA = 0 AND ati.FLAG_MANUTENCAO = 0 AND ati.FLAG_ABASTECIMENTO = 0 AND ga.FLAG_PARADA_PROCESSO = 0 AND ga.FLAG_PARADA_OPERACIONAL = 0) then 'Improdutiva' \r\n\twhen (ati.FLAG_PRODUTIVA = 0 AND ati.FLAG_MANUTENCAO = 1 AND ati.FLAG_ABASTECIMENTO = 0 AND ga.FLAG_PARADA_PROCESSO = 0 AND ga.FLAG_PARADA_OPERACIONAL = 0) then 'Paradas de Manutencao'\r\n\twhen (ati.FLAG_PRODUTIVA = 0 AND ati.FLAG_MANUTENCAO = 0 AND ati.FLAG_ABASTECIMENTO = 1 AND ga.FLAG_PARADA_PROCESSO = 0 AND ga.FLAG_PARADA_OPERACIONAL = 0) then 'Abastecimento'\r\n\twhen (ati.FLAG_PRODUTIVA = 0 AND ati.FLAG_MANUTENCAO = 0 AND ati.FLAG_ABASTECIMENTO = 0 AND ga.FLAG_PARADA_PROCESSO = 1 AND ga.FLAG_PARADA_OPERACIONAL = 0) then 'Paradas de Processo'\r\n\twhen (ati.FLAG_PRODUTIVA = 0 AND ati.FLAG_MANUTENCAO = 0 AND ati.FLAG_ABASTECIMENTO = 0 AND ga.FLAG_PARADA_PROCESSO = 0 AND ga.FLAG_PARADA_OPERACIONAL = 1) then 'Paradas Operacionais'\r\n\tEND) Atividades\r\n\tFROM app_apontamento_maquina a\r\n\tINNER JOIN app_equipamento e ON a.EQUIPAMENTO_SEQ_DB = e.SEQ_DB\r\n\tINNER JOIN app_modelo m ON e.MODELO_SEQ_DB = m.SEQ_DB\r\n\tINNER JOIN app_atividade ati ON a.ATIVIDADE_SEQ_DB = ati.SEQ_DB\r\n\tINNER JOIN app_grupo_atividade ga ON ati.GRUPO_ATIVIDADE_SEQ_DB = ga.SEQ_DB\r\n\tWHERE DATE(a.INI_DH) BETWEEN '{$comeco}' AND '{$final}' and a.empresa = {$empresa} and a.local in ({$local},9999) and a.filial in ({$filial},9999) and a.ativo = 1 and a.deleted = 0\r\n     {$modelos};\";\r\n\r\n$grupoAtividades = \\core\\ConexaoDB::query($tipoAtividades);\r\nforeach ($grupoAtividades as $Atividades){\r\n\tif ($Atividades['Atividades'] == 'Abastecimento') {\r\n\t\tcontinue;\r\n\t} else {\r\n\t\t$valores['name'] = [];\r\n\t\t$valores['data'] = [];\r\n\t\t$valores['name'] = $Atividades['Atividades'];\r\n\t\t$variavel = $Atividades['Atividades'];\r\n\t\t$dados = \"\r\n\t\tSELECT DISTINCT m.DESCRICAO fabricante, \r\n\t\t\tif('{$Atividades['Atividades']}' = 'Produtiva',(SUM(IF(ati.FLAG_PRODUTIVA = 1,a.INI_FIM_DIFF_SEC,0))/ SUM(IF(DATE(a.INI_DH) BETWEEN '{$comeco}' AND '{$final}', a.INI_FIM_DIFF_SEC,0)))*100,0)'Produtiva',\r\n\t\t\tif('{$Atividades['Atividades']}' = 'Improdutiva',(SUM(IF(ati.FLAG_PRODUTIVA = 0 AND ati.FLAG_MANUTENCAO = 0 AND ati.FLAG_ABASTECIMENTO = 0 AND ga.FLAG_PARADA_PROCESSO = 0 AND ga.FLAG_PARADA_OPERACIONAL = 0,a.INI_FIM_DIFF_SEC,0))/ SUM(IF(DATE(a.INI_DH) BETWEEN '{$comeco}' AND '{$final}', a.INI_FIM_DIFF_SEC,0)))*100,0) 'Improdutiva',\r\n\t\t\tif('{$Atividades['Atividades']}' = 'Paradas de Processo',(SUM(IF(ga.FLAG_PARADA_PROCESSO = 1,a.INI_FIM_DIFF_SEC,0))/ SUM(IF(DATE(a.INI_DH) BETWEEN '{$comeco}' AND '{$final}', a.INI_FIM_DIFF_SEC,0)))*100,0) 'Paradas de Processo', \r\n\t\t\tif('{$Atividades['Atividades']}' = 'Paradas Operacionais',(SUM(IF(ga.FLAG_PARADA_OPERACIONAL = 1,a.INI_FIM_DIFF_SEC,0))/ SUM(IF(DATE(a.INI_DH) BETWEEN '{$comeco}' AND '{$final}', a.INI_FIM_DIFF_SEC,0)))*100,0) 'Paradas Operacionais', \r\n\t\t\tif('{$Atividades['Atividades']}' = 'Paradas de Manutencao',(SUM(IF(ati.FLAG_MANUTENCAO = 1,a.INI_FIM_DIFF_SEC,0))/ SUM(IF(DATE(a.INI_DH) BETWEEN '{$comeco}' AND '{$final}', a.INI_FIM_DIFF_SEC,0)))*100,0) 'Paradas de Manutencao'\r\n\t\t\tFROM app_apontamento_maquina a\r\n\t\t\tINNER JOIN app_equipamento e ON a.EQUIPAMENTO_SEQ_DB = e.SEQ_DB\r\n\t\t\tINNER JOIN app_modelo m ON e.MODELO_SEQ_DB = m.SEQ_DB\r\n\t\t\tINNER JOIN app_atividade ati ON a.ATIVIDADE_SEQ_DB = ati.SEQ_DB\r\n\t\t\tINNER JOIN app_grupo_atividade ga ON ati.GRUPO_ATIVIDADE_SEQ_DB = ga.SEQ_DB\r\n\t\t\tWHERE DATE(a.INI_DH) BETWEEN '{$comeco}' AND '{$final}' and a.empresa = {$empresa} and a.local in ({$local},9999) and a.filial in ({$filial},9999) and a.ativo = 1 and a.deleted = 0\r\n{$modelos} GROUP BY m.SEQ_DB;\";\r\n\r\n\t\t$dadosSomados = \\core\\ConexaoDB::query($dados);\r\n\t\tforeach ($dadosSomados as $resultados){\r\n\t\t\tarray_push($valores['data'], floatval($resultados[$variavel]));\r\n\t\t}\r\n\t\tarray_push($series, $valores);\r\n\t}\r\n}\r\nreturn $series;"
  }
}

Onde:

  • Cada série refere-se a um placeholder na configuração;
  • CODE_TYPE: tipo de interpretação dos dados. Entre as interpretações estão;
    1. CONST: retorna apenas um valor, sendo constante (número, string, boleano) ou variável;
    2. PHP: bloco de código PHP para obtenção dos dadostrução de código.

Exemplo com CODE_TYPE = ENTRY_POINT

/*
Dados enviados pelo core:
    $entryPointsDashboards->param = $filtersConfigAndValues;
    $entryPointsDashboards->parameters = $filtersConfigAndValues;
    $entryPointsDashboards->param['inputValues'] = $filtersConfigAndValues;
    $entryPointsDashboards->processEntryPoint();

O core recebe a propriedade 'data' para enviar ao gráfico JS:

    $returnContent = $entryPointsDashboards->data;
*/


$this->queryData['apontamentosPorHora'] = $apontamentosPorHora;
$this->queryData['apontamentos'] = $apontamentosList;
$this->queryData['equipamentos'] = $equipamentos;s. É possível acessar o resultado dos filtros aplicados.
  3. JSON: Retora um JSON válido
  4. ENTRY_POINT: Executa um Entry Point DASHBOARD com nome = CHART.NAME
- **CONTENT**: o valor ou nome da variável (se o **CODE_TYPE** for **CONST**) ou o bloco de in
  1. CHART_ORDER: INT que irá armazenar a posição padrão do grafico dentro do dashboard, sendo o valor 1 para o topo da exibição, 2 para a segunda posição e assim por diante, o campo tem inicialmente o valor padrão de 999 para que gráficos cujo a ordenação não for configurada sejam exibidos após os graficos com ordenação configurada.

IMPORTANTE Quando o CODE_TYPE for PHP é necessário que o código esteja tratado (escaped), uma vez que se trata de um bloco JSON. Para tanto, existem ferramentas online, como freeformatter.com. Tomando o exemplo acima, temos o seguinte código para a série categories: {.is-danger}

$user = \core\xDS::getUser();
$empresa = $user->EMPRESA_ATUAL;
$local = $user->LOCAL_ATUAL;
$filial = $user->FILIAL_ATUAL;
$timezone = $this->user->TIMEZONE ?? date_default_timezone_get();

$dIni = (empty($inicio)) ? new DateTime('-2 months', new DateTimeZone($timezone)) : DateTime::createFromFormat('d\/m\/Y', $inicio);
$dFim = (empty($fim)) ? new DateTime('now', new DateTimeZone($timezone)) : DateTime::createFromFormat('d\/m\/Y', $fim);
$comeco = date_format($dIni,'Y-m-d');
$final = date_format($dFim,'Y-m-d');

if (!empty($modelo)) {
	$modelos = (is_array($modelo)) ? ' and m.SEQ_DB in (' . implode(',', $modelo) . ') ' : " and m.SEQ_DB = {$modelo} ";
} else {
	$modelos = '';
}

$sql = "SELECT
	distinct m.DESCRICAO Modelo
	FROM app_apontamento_maquina a
	INNER JOIN app_equipamento e ON a.EQUIPAMENTO_SEQ_DB = e.SEQ_DB
	INNER JOIN app_modelo m ON e.MODELO_SEQ_DB = m.SEQ_DB
	WHERE DATE(a.INI_DH) between '{$comeco}' and '{$final}'
	and a.empresa = {$empresa} and a.local in ({$local},9999) and a.filial in ({$filial},9999) and a.ativo = 1 and a.deleted = 0
	{$modelos} GROUP BY m.SEQ_DB;";

$todosModelos = \core\ConexaoDB::query($sql, $params = []);
$series = [];
foreach ($todosModelos as $model){
	array_push($series,$model['Modelo']);
}
return $series;

Gráficos

Vídeos para ajuda: Criando gráficos para dashboard no NFS

Parte 1 de 5

Parte 2 de 5

Parte 3 de 5

Parte 4 de 5

Parte 5 de 5

Configuração de Reload No Dashboard

A configuração abaixo deve ser inserida quando o objetivo é atualizar os dados de todos os gráficos da tela ao mesmo tempo.

Obs: O tempo de reload a ser devinido está em segundos, logo se for a cada 1 minuto o valor deve ser 60 (segundos). O valor deve ser maior ou igual a 60 (1 minuto), caso contrário não será considerado

A configuração deve ser inserida no campo OPTIONS da tabela nfs_generic_dashboard

Exemplo: Reload a cada 1 minutos

{
  "reloadTime": 60
}

Configuração de Reload individual por gráfico/relatorio

Aviso! Se for adicionado o reloadTime individual em algum gráfico ou relatório, o reload configurado na tabela nfs_generic_dashboard será ignorado {.is-warning}

Para configurar o reload automático individual em um gráfico, basta adicionar no campo OPTIONS da tabela nfs_generic_chart o item reloadTime, conforme o exemplo abaixo:

Exemplo: Reload a cada 3 minutos

{
  "reloadTime": 180
}

Obs: O tempo de reload deve ser inserido em segundos!

Exibir Relatório no Dashboard

Para exibir um relatório na tela de Dashboard deve ser inserido no campo OPTIONS da tabela nfs_generic_chart o nome do relatório, que pode ser consultado na tabela nfs_reports no campo NAME.

No exemplo abaixo temos um relatório com o NAME = planejamento_diario_trato_madeira, logo, deve ser inserido no item report da configuração, como no exemplo abaixo:

{
  "report": "planejamento_diario_trato_madeira"
}

Para adicionar o reload automático no relatório basta adicionar à configuração o item reloadTime, como no exemplo a seguir:

{
  "report": "planejamento_diario_trato_madeira", 
  "reloadTime": 180
}

Obs: O tempo de reload deve ser inserido em segundos!

Aviso! Se for adicionado o reloadTime individual em algum gráfico ou relatório, o reload configurado na tabela nfs_generic_dashboard será ignorado {.is-warning}

Gerar imagem PNG de um gráfico

É possível gerar uma imagem PNG de um gráfico configurado na tabela nfs_generic_chart ou passar diretamente uma configuração de um gráfico no modelo Highcharts (modelos: https://www.highcharts.com/demo)

Exemplo:

 
  //['chartConfig'] -> enviando uma configuração highcarts diretamente 
  
  $args['chartConfig'] = '{"title": {"text": "Steep Chart"}, "xAxis": {"categories": ["Jan", "Feb", "Mar"]}, "series": [{"data": [29.9, 71.5, 106.4]}]}'; 
  
  //ou 
  
  //['seqDb'] -> pegar uma configuração da tabela nfs_generic_chart 
  $seqdb = 161; //seqdb da tabela nfs_generic_chart.
  $args['seqDb'] = $seqdb;

  $imgPng = ImageService::getHighchartsImage($args);

  echo '<img src="'.$imgPng.'">';

Inserir a imagem de um grafico em um relatorio

Para inserir a imagem de um gráfico no relatório basta definir como no exemplo acima se o gráfico virá de uma configuração ja feita na tabela nfs_generic_chart (SEQ_DB) ou se será gerada manualmente (ChartConfig).

Passo 01 - Inserir o código no entrypoint e enviar através do inputValues como no exemplo abaixo:

Imagem gerada a partir de uma configuração pronta na tabela nfs_generic_chart:


//['seqDb'] -> pegar uma configuração da tabela nfs_generic_chart 
  $seqDb = 161; //seqdb da tabela nfs_generic_chart.
  $args['seqDb'] = $seqDb;

  $imgPng = ImageService::getHighchartsImage($args);

  if (empty($img)){
    //salva no logs o erro, caso ocorra falha
    NFSLogger::error('Falha ao tentar pegar imagem de um grafico da tabela nfs_generic_chart, seqDB = '.$seqDb, 'EntryPoint', $imgPng);
  } else {
    //envia o base64 gerado para o relatório na variavel chartBase64
    $this->queryData['chartBase64'] = $imgPng;
  }
  

ou

Imagem gerada a partir de uma configuração feita manualmente

 
  //['chartConfig'] -> enviando uma configuração highcarts diretamente 
  
  $args['chartConfig'] = '{"title": {"text": "Steep Chart"}, "xAxis": {"categories": ["Jan", "Feb", "Mar"]}, "series": [{"data": [29.9, 71.5, 106.4]}]}'; 

  $imgPng = ImageService::getHighchartsImage($args);

  if (empty($img)){
    //salva no logs o erro, caso ocorra falha
    NFSLogger::error('Falha ao tentar pegar imagem de um grafico', 'EntryPoint', 'erro ao obter imagem do gráfico');
  } else {
    //envia o base64 gerado para o relatório na variavel chartBase64
    $this->queryData['chartBase64'] = $imgPng;
  }
  

Gerar imagem de um grafico no MOBILE

Exemplo de configuração do gráfico:

<body>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.7/css/all.css">

  <div>
    <table>
      <tr>
        <td>
          <figure class="highcharts-figure">
            <div id="tmo" class="chart-container"></div>
          </figure>
        </td>
    </table>
  </div>
  <script>
    var gaugeOptions = {
      chart: {
        type: 'gauge',
        plotBackgroundColor: null,
        plotBackgroundImage: null,
        plotBorderWidth: 0,
        plotShadow: false
        },
      title: null,
      pane: {
        startAngle: -100,
        endAngle: 100,
        background: [{
          backgroundColor: {
          linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
          stops: [
            [0, '#FFF'],
            [1, '#333']
          ]
          },
          borderWidth: 0,
          outerRadius: '109%'
        }, {
          backgroundColor: {
          linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
          stops: [
            [0, '#333'],
            [1, '#FFF']
          ]
          },
          borderWidth: 1,
          outerRadius: '107%'
        }, {
          // default background
        }, {
          backgroundColor: '#DDD',
          borderWidth: 0,
          outerRadius: '105%',
          innerRadius: '103%'
        }]
        },
      exporting: {
        enabled: false
      },
      tooltip: {
        enabled: false
      },
      // the value axis
      yAxis: {
        minorTickInterval: 'auto',
        minorTickWidth: 1,
        minorTickLength: 10,
        minorTickPosition: 'inside',
        minorTickColor: '#666',

        tickPixelInterval: 30,
        tickWidth: 2,
        tickPosition: 'inside',
        tickLength: 10,
        tickColor: '#666',
        labels: {
          step: 2,
          rotation: 'auto'
        },
      },
      credits: {
        enabled: false
      }
    };

  var columnBasic = Highcharts.chart('tmo', Highcharts.merge(gaugeOptions, {
    chart: {
      type: 'column'
    },
    xAxis: {
      categories: [
        'TMO'
      ],
      crosshair: true
    },
    yAxis: {
      min: 0,
      title: {
        text: 'Tempo (segundos)'
      }
    },
    tooltip: {
      headerFormat: '<span style="font-size:10px">{point.key}</span><table>',
      pointFormat: '<tr><td style="color:{series.color};padding:0">{series.name}: </td>' +
        '<td style="padding:0"><b>{point.y:.1f} mm</b></td></tr>',
      footerFormat: '</table>',
      shared: true,
      useHTML: true
    },
    plotOptions: {
      column: {
        pointPadding: 0.2,
        borderWidth: 0
      }
    },
    series: []
  }));

  function loadData() {
    xMovaJsApi.getData('onDataReceived');
    }

  function onDataReceived(result) {
    if (result) {
      //xMovaJsApi.toast(result);
      result = JSON.parse(result);
      if (columnBasic) {


        /* //OK - MAS OS REGISTROS PRECISAM EXISTIR EM SERIES PARA SEREM ATUALIZADOS
        let tempo_padrao = result[0].value;
        let tempo_apontado = result[1].value;

        let point = columnBasic.series[0].points[0];
        point.update(parseFloat(tempo_padrao));

        let point2 = columnBasic.series[1].points[0];
        point2.update(parseFloat(tempo_apontado));
        */


        /* //OK - MAS NÃO ATUALIZA O NAME
        var count = 0;
        result.forEach(function(element) {
          let data = element.value;
          let name = element.desc;

          let point = columnBasic.series[count].points[0];
          point.update(parseFloat(data));

          count ++;
        });
        */

        //LIMPAR DADOS DA SERIES
        var series_ids = [];
        for (var s in columnBasic.series) {
          series_ids.push(columnBasic.series[s].options.id);
        }
        for (var id in series_ids) {
          columnBasic.get(id).remove(false);
        }

        //ADICIONAR DADOS NA SERIES
        result.forEach(function (item) {
          columnBasic.addSeries({                                        
            name: item.desc,
            data: [parseFloat(item.value)]
          }, false);

        });
        columnBasic.redraw();

      }
    }

    setTimeout(function () {
      loadData();
    }, 10000);
  }
  loadData();
  </script>
</body>

Indicadores

No RPO ou EntryPoint busque ou crie um entry point com número 502 e com name de home_indicator . Exemplo:

INSERT INTO nfs_cloud.nfs_rpo ( `NAME`, `FILE_OR_DOMAIN`, `ACTION`, `XMOVA_INSTALLCODE`, `XMOVA_INSTALLCODE_VERSION`, `TABELA`, `ENTRY_NUM`, `CODE`, `VERSION`, `BUILD`, `ATIVO`, `CODETYPE`, `FIELD`, `DESCRIPTION`, `INS_DH`, `UPD_DH`) VALUES('home_indicator', 'SYSTEM', 'IndicatorHome', NULL, NULL, NULL, 502, '{
  "osIntegracao": {
    "label": "OS''s com Faturamento liberado",
    "color": "red-mint",
    "icon": "fa fa-file",
    "display": "[result]",
    "queryBuilder": {
      "osIntegracao": {
        "select": [
          "count(c.seq_db) result"
        ],
        "from": "os os",
        "inner_join": [
          [
            "os",
            "status_os",
            "st",
            "st.SEQ_DB = os.STATUS_OS_SEQ_DB"
          ],
          [
            "os",
            "cliente",
            "c",
            "c.SEQ_DB = os.CLIENTE_SEQ_DB"
          ]
        ],
        "where": [
          "st.DESCRICAO in (''Liberada Faturamento'')"
        ]
      }
    },
    "queryBuilderData": {
      "osIntegracao": {
        "select": [
          "os.seq_db seq_db",
          "os.codigo Codigo",
          "c.DESCRICAO Cliente",
          "st.DESCRICAO Status"
        ],
        "from": "os os",
        "inner_join": [
          [
            "os",
            "status_os",
            "st",
            "st.SEQ_DB = os.STATUS_OS_SEQ_DB"
          ],
          [
            "os",
            "cliente",
            "c",
            "c.SEQ_DB = os.CLIENTE_SEQ_DB"
          ]
        ],
        "where": [
          "st.DESCRICAO in (''Liberada Faturamento'')"
        ]
      }
    },
    "report":{
      "displayColumns": [
        "Codigo",
        "Cliente",
        "Status"
      ],
      "links" : {
        "Codigo" : "/t/os/edit/%seq_db%"
      }
    }
  },
  "mais9Horas": {
    "label": "Boletim com mais de 9 Horas",
    "color": "yellow-gold",
    "icon": "fa fa-clock-o",
    "display": "[result]",
    "queryBuilder": {
      "mais9Horas": {
        "select": [
          "count(b.seq_db) result"
        ],
        "from": "boletim b",
        "where": [
          "b.INI_DH > date_sub(now(), interval 180 DAY)",
          "b.INI_FIM_DIFF_SEC >= ((3600) * 9)"
        ]
      }
    }, "queryBuilderData": {
      "mais9Horas": {
        "select": [
          "b.SEQ_DB seq_db",
          "b.FUNCIONARIO_SEQ_DB funcionario_seq_db",
          "concat(f.CRACHA, '' :: '',f.NOME) Funcionário",
          "b.INI_DH Início",
          "b.FIM_DH Término",
          "(b.INI_FIM_DIFF_SEC)/3600  Total"
        ],
        "from": "boletim b",
        "inner_join": [
          [
            "b",
            "funcionario",
            "f",
            "f.SEQ_DB = b.FUNCIONARIO_SEQ_DB"
          ]
        ],
        "where": [
          "b.INI_DH > date_sub(now(), interval 180 DAY)",
          "b.INI_FIM_DIFF_SEC >= ((3600) * 9)"
        ]
      }
    },
    "report":{
      "displayColumns": [
        "Funcionário",
        "Início",
        "Término",
        "Total"
      ],
      "links" : {
        "Funcionário" : "/details/1/%funcionario_seq_db%/1"
      }
    }
  }
}', 1, 16, 1, 'JSON', NULL, NULL, '2019-07-02 17:11:04.000', '2019-08-15 16:01:42.000');

Como configurar indicadores

Gráfico

No RPO ou noEntryPoint Local buscar pelo entry point com número 500 e name igual a home_chart, que é a configuração do gráfico de acordo com Highcharts e o 501 e name igual a home_chart_data que é os dados que serão exibidos e usados no 500. Exemplo:

INSERT INTO nfs_cloud.nfs_rpo (`NAME`, `FILE_OR_DOMAIN`, `ACTION`, `XMOVA_INSTALLCODE`, `XMOVA_INSTALLCODE_VERSION`, `TABELA`, `ENTRY_NUM`, `CODE`, `VERSION`, `BUILD`, `ATIVO`, `CODETYPE`, `FIELD`, `DESCRIPTION`, `INS_DH`, `UPD_DH`) VALUES('home_chart_data', 'SYSTEM', 'ChartHomeData', NULL, NULL, NULL, 501, '
$data = [];
$data["categories"] = [''Jan'', ''Feb'', ''Mar'',''Apr'', ''May'']; 
$data["servico"] = [5216, 4380, 3176, 3798, 3109]; 
$data["parada"] =  [2745, 3345, 3546, 3390, 2905];
$data["total"] =   [7962, 7725, 6722, 7189, 6014];
$data["series"] =[
      [ "x"=> 95, "y"=> 95, z=> 13.8, "name"=> ''BE'', "country"=> ''Belgium''],
      ["x"=> 86.5, "y"=> 102.9, z=> 14.7, "name"=> ''DE'', "country"=> ''Germany''],
      ["x"=> 80.8, "y"=> 91.5, z=> 15.8, "name"=> ''FI'', "country"=> ''Finland''],
      ["x"=> 80.4, "y"=> 102.5, z=> 12, "name"=> ''NL'', "country"=> ''Netherlands''],
      ["x"=> 80.3, "y"=> 86.1, z=> 11.8, "name"=> ''SE'', "country"=> ''Sweden''],
      ["x"=> 78.4, "y"=> 70.1, z=> 16.6, "name"=> ''ES'', "country"=> ''Spain''],
      ["x"=> 74.2, "y"=> 68.5, z=> 14.5, "name"=> ''FR'', "country"=> ''France''],
      ["x"=> 73.5, "y"=> 83.1, z=> 10, "name"=> ''NO'', "country"=> ''Norway''],
      ["x"=> 71, "y"=> 93.2, z=> 24.7, "name"=> ''UK'', "country"=> ''United Kingdom''],
      ["x"=> 69.2, "y"=> 57.6, z=> 10.4, "name"=> ''IT'', "country"=> ''Italy''],
      ["x"=> 68.6, "y"=> 20, z=> 16, "name"=> ''RU'', "country"=> ''Russia''],
      ["x"=> 65.5, "y"=> 126.4, z=> 35.3, "name"=> ''US'', "country"=> ''United States''],
      ["x"=> 65.4, "y"=> 50.8, z=> 28.5, "name"=> ''HU'', "country"=> ''Hungary''],
      ["x"=> 63.4, "y"=> 51.8, z=> 15.4, "name"=> ''PT'', "country"=> ''Portugal''],
      ["x"=> 64, "y"=> 82.9, z=> 31.3, "name"=> ''NZ'', "country"=> ''New Zealand'']
];
$data["pointFormatData"] = "<tr><th colspan=''2''><h3>{point.country}</h3></th></tr>
            <tr><th>Fat intake:</th><td>{point.x}g</td></tr>
            <tr><th>Sugar intake:</th><td>{point.y}g</td></tr>
            <tr><th>Obesity (adults):</th><td>{point.z}%</td></tr>";
            
$this->param = $data;', 1, 10, 1, 'PHP', NULL, NULL, '2019-03-01 15:48:16.000', '2019-07-18 11:56:44.000');
INSERT INTO nfs_cloud.nfs_rpo (`NAME`, `FILE_OR_DOMAIN`, `ACTION`, `XMOVA_INSTALLCODE`, `XMOVA_INSTALLCODE_VERSION`, `TABELA`, `ENTRY_NUM`, `CODE`, `VERSION`, `BUILD`, `ATIVO`, `CODETYPE`, `FIELD`, `DESCRIPTION`, `INS_DH`, `UPD_DH`) VALUES('home_chart', 'SYSTEM', 'SetupChart', NULL, NULL, NULL, 500, '{
    "chart": {
        "type": "bubble",
        "plotBorderWidth": 1,
        "zoomType": "xy",
        "borderColor": "#EBBA95",
        "borderWidth": 2,
        "plotBorderWidth": 1
    },
    "legend": {
        "enabled": false
    },
    "title": {
        "text": "Eficiência"
    },
    "xAxis": {
        "min": 40,
        "max": 160,
        "tickInterval": 10,
        "minorTickInterval": 1,
        "minorTickColor": "#BCBCBC",
        "gridLineWidth": 1,
        "gridLineColor": "rgba(0,0,0,0.5)",
        "minorGridLineColor": "rgba(188,188,188,0.5)",
        "title": {
            "text": "Eficiência"
        },
        "labels": {
            "format": "{value} %"
        },
        "plotLines": [{
            "color": "rgba(0,0,0,0.5)",
            "dashStyle": "solid",
            "width": 2,
            "value": 100,
            "zIndex": 3,
            "label": {
                "rotation": 0,
                "y": 15,
                "style": {
                    "fontStyle": "italic"
                },
                "text": "100% de Eficiência"
            }
        }] 
    },
    "yAxis": {
        "min": 25,
        "max": 100,
        "tickInterval": 5,
        "tickColor": "#000000",
        "tickWidth": 1,
        "minorTickInterval": 1,
        "minorTickColor": "#BCBCBC",
        "gridLineColor": "rgba(0,0,0,0.5)",
        "gridLineWidth": 1,
        "minorGridLineColor": "rgba(188,188,188,0.5)",
        "startOnTick": false,
        "endOnTick": false,
        "title": {
            "text": "Produtividade"
        },
        
        "labels": {
            "format": "{value} %"
        },
        "maxPadding": 1,
        "plotLines": [{
            "color": "rgba(0,0,0,0.5)",
            "dashStyle": "solid",
            "width": 2,
            "value": 85,
            "zIndex": 3,
            "label": {
                "align": "right",
                "style": {
                    "fontStyle": "italic"
                },
                "text": "85% de Produtividade",
                "x": -10
            }
        }]
    },
    "tooltip": {
        "useHTML": true,
        "headerFormat": "<table>",
        "pointFormat": "%pointFormatData%",
        "footerFormat": "</table>",
        "followPointer": true
    },
    "plotOptions": {
        "bubble": {
            "minSize": 30
        },
        "series": {
            "dataLabels": {
                "enabled": true,
                "allowOverlap": true,
                "format": "{point.name}"
            }
        }
    },
    "series": [{
        "data": "%series%"
    }]

}', 1, 36, 1, 'JS', NULL, NULL, '2019-03-01 15:48:16.000', '2019-07-18 15:04:41.000');

Logo ou nome da Empresa no div header/topo

Se o campo nfs_org_empresa.LOGO_EMPRESA não contém uma imagem base64 começando com "data:", será apresentado o conteúdo do campo NOME (do cliente) no lugar do logo.

QRCode dos SimovaApps

Adicionar o parâmetro CORE_SIMOVAAPPS_LIST na tabela nfs_core_par_parametros

INSERT INTO `nfs_core_par_parametros` (`EMPRESA`, `FILIAL`, `LOCAL`, `NOME`, `CONTEUDO`, `TIPO`) 
    VALUES (1, 9999, 9999
    , 'CORE_SIMOVAAPPS_LIST'
    , '[{"item":"BoB.Agro",\r\n    "desc" :"Módulo Equipamento",\r\n    "installCodeStr": "101 000 000",\r\n    "qrcode":"{\\"installCode\\":\\"20603\\",\\"idEmpresa\\":\\"1\\",\\"descricaoEmpresa\\":\\"BEVAP\\",\\"idFilial\\":\\"1\\",\\"idLocal\\":\\"1\\",\\"idChaveAcesso\\":\\"123456\\"}"\r\n},{"item":"MECANICA",\r\n    "desc" :"Módulo INSUMOS",\r\n    "installCodeStr": "101 ASASA ASASASA",\r\n    "qrcode":"{\\"installCode\\":\\"20603\\",\\"idEmpresa\\":\\"1\\",\\"descricaoEmpresa\\":\\"BEVAP\\",\\"idFilial\\":\\"1\\",\\"idLocal\\":\\"1\\"}"\r\n},{"item":"SMARTOS",\r\n    "desc" :"Módulo INSUMOS",\r\n    "installCodeStr": "101 ASASA ASASASA",\r\n    "qrcode":"{\\"installCode\\":\\"20603\\",\\"idEmpresa\\":\\"1\\",\\"descricaoEmpresa\\":\\"BEVAP\\",\\"idFilial\\":\\"1\\",\\"idLocal\\":\\"1\\"}"\r\n}]'
    , 1);

O JSON deve seguir o modelo abaixo.

ATENÇÃO!!! O conteúdo do campo qrcode deve ser JSON com encode (usar ferramenta no NFS->Admin Console). Os parâmetros do JSON do qrcode devem representar as informações esperadas pelo AuthInput do fluxo SimovaApps. {.is-danger}

Exemplo para configurar

[{"item":"BoB.Agro",
    "desc" :"Módulo Equipamento",
    "installCodeStr": "101 000 000",
    "qrcode":"{\"installCode\":\"20603\",\"idEmpresa\":\"1\",\"descricaoEmpresa\":\"BEVAP\",\"idFilial\":\"1\",\"idLocal\":\"1\",\"idChaveAcesso\":\"123456\"}"
},{"item":"MECANICA",
    "desc" :"Módulo INSUMOS",
    "installCodeStr": "101 ASASA ASASASA",
    "qrcode":"{\"installCode\":\"20603\",\"idEmpresa\":\"1\",\"descricaoEmpresa\":\"BEVAP\",\"idFilial\":\"1\",\"idLocal\":\"1\"}"
},{"item":"SMARTOS",
    "desc" :"Módulo INSUMOS",
    "installCodeStr": "101 ASASA ASASASA",
    "qrcode":"{\"installCode\":\"20603\",\"idEmpresa\":\"1\",\"descricaoEmpresa\":\"BEVAP\",\"idFilial\":\"1\",\"idLocal\":\"1\"}"
}]

Galeria de Fotos - EntryPoint 400

Criar um EntryPoint 400, tipo PHP

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`) VALUES ('SYSTEM', NULL, NULL, NULL, 9999, 9999, 9999, NULL, 400, '$fotos = \\core\\TabelaBO::getFilteredToArray(\'APONTAMENTO_FOTO\', "INI_DH DESC", "", "SEQ_DB",10);\n$itens = [];\n$count = 0;\nforeach ($fotos as $seqBoletim => $foto) {	\n	if (!empty($foto[\'FOTO\'])) {\n		$itens[$seqBoletim][\'image\'] = $foto[\'FOTO\'];\n		$itens[$seqBoletim][\'text\'] = $foto[\'INI_DH_STR\'].\' OS:\'.$foto[\'CODIGO_OS\'];\n		$itens[$seqBoletim][\'link\'] = \'/t/boletim/edit/\'.$seqBoletim;\n		\n		//if ($count == 2) {\n		//	$itens[$seqBoletim][\'style\'] = "max-height: 500px;";\n		//}\n	}\n	$count++;\n}\n\n$gallery = new \\stdClass;\n$gallery->imgStyle = "height: 500px;";\n$gallery->title = "Últimas 10 fotos";\n$gallery->itens = $itens;\n\n$this->param = $gallery;\n', 0, 22, 1, 'PHP', NULL, NULL, '2018-05-09 09:50:01', '2018-05-09 15:25:00');

Opções

É possível definir o tamanho de uma imagem específica:

    $itens[$seqBoletim]['style'] = "max-height: 500px;";

ou para toda a galeria de fotos (RECOMENDÁVEL)

    $gallery->imgStyle = "height: 500px;";

Exemplo (SmartOS Dealer JD)

Registros da tabela apontamento_foto e apresentando as 10 (último parâmetro do método) fotos mais recentes (INI_DH DESC) enviadas pelos técnicos e qual o número de OS. Intenção é fazer um link para OS.

$fotos = \core\TabelaBO::getFilteredToArray('APONTAMENTO_FOTO', "INI_DH DESC", "", "SEQ_DB",10);
$itens = [];
$count = 0;
foreach ($fotos as $seqBoletim => $foto) {	
	if (!empty($foto['FOTO'])) {
		$itens[$seqBoletim]['image'] = $foto['FOTO'];
		$itens[$seqBoletim]['text'] = $foto['INI_DH_STR'].' OS:'.$foto['CODIGO_OS'];
		$itens[$seqBoletim]['link'] = '/t/boletim/edit/'.$seqBoletim;
		
		//if ($count == 2) {
		//	$itens[$seqBoletim]['style'] = "max-height: 500px;";
		//}
	}
	$count++;
}

$gallery = new \stdClass;
$gallery->imgStyle = "height: 500px;";
$gallery->title = "Últimas 10 fotos";
$gallery->itens = $itens;

$this->param = $gallery;

Carrossel de Informações/Dicas

A funcionalidade 'CARROSSEL DE INFORMAÇÕES/DICAS' quando habilitada permite a inserção de Cards de Dicas que irão aparecer na Home Page da aplicação.

Configuração:

Para habilitar a funcionalidade na aplicação do cliente, é preciso definir o parâmetro 'TIPS_CAROUSEL' na tabela 'nfs_core_par_parametros', onde na coluna "CONTEUDO" o seguinte json deve ser informado: { "enabled": true, "time_interval": 3.5 } , de forma que a key "enabled" habilita a feature do carressel na págin home do cliente, e, a key "time_interval" define a velocidade do carrosel, estipulando o tempo em segundos da exibição de cada card.

Abaixo segue um exemplo de SQL INSERT de como configurar a tabela 'nfs_core_par_parametros':

INSERT INTO nfs_homol_abengoa_construmobil.nfs_core_par_parametros
(EMPRESA, FILIAL, `LOCAL`, NOME, CONTEUDO, TIPO)
VALUES(9999, 9999, 9999, 'TIPS_CAROUSEL', 
  '{
    "enabled": true,
    "time_interval": 3.5
  }',
1);

Cards de dicas:

Para ter acesso a tabela de configuração dos cards, é precisar acessar e rodar em 'Admin console -> NFS ENVIRONMENT > DS/DDL (Full)' para criação da tabela 'APP_TIPS_CAROUSEL' e realizar a configuração dos novos cards.

Ao inserir um novo registro na tabela 'APP_TIPS_CAROUSEL', um novo card será renderizado na página Home da aplicação, caso este esteja marcado como 'ATIVO'.

Segue abaixo um exemplo simplificado:

INSERT INTO nfs_homol_abengoa_construmobil.app_tips_carousel
(SEQ_DB, EMPRESA, FILIAL, `LOCAL`, INS_DH, INS_USUARIO_SEQ_DB, UPD_DH, UPD_USUARIO_SEQ_DB, ATIVO, DELETED, ID, TITULO, DESCRICAO, IMAGEM, LINK, COR, SOURCE, IMAGEM_SEQ_DB, RO)
VALUES(5, 1, 1, 1, '2024-02-16 15:24:05', 3, '2024-02-16 15:56:40', 3, 1, 0, 4, 'CARD TESTE', 'TESTANDO CARD - AVISO RANDOM', NULL, 'https://simova.atlassian.net/browse/NFSCORE-666', '#efdada', NULL, 4998, 0);

Os CARDs possuem um layout padrão e fixo, novas colunas se criadas não serão utilizadas/renderizadas.

É possível adicionar uma imagem diretamente através de uma string base64 no campo IMAGEM, porém, se houver um valor no campo IMAGEM_SEQ_DB referenciando uma imagem adicionada via upload, esta terá prioridade na renderização;

As colunas 'SOURCE', 'IMAGEM_SEQ_DB' e 'RO' são criadas/utilizadas pelo CORE quando configurado o tipo UPLOAD para inserção de IMAGENS na tabela nfs_core_ds_tabela_campo;

Notas:

  • O campo 'ID' é único para cada card dentro de uma mesma 'EMPRESA-FILIAL-LOCAL';

  • O campo 'IMAGEM' é usado como uma Thumbnail para representar a informação do card. Armazena uma imagem Base64, podendo ser inserida manualmente ou via UPLOAD caso a tabela seja configurada no CRUD.

  • O campo 'COR' define o backgroud-color do Card. Recebe uma string contendo o código HEXADECIMA, ou via COLOR caso a tabela seja configurada no CRUD.

  • O campo 'LINK' define a URL do vídeo que contem mais detalhes sobre a informação do card, uma janela popup é aberta ao ser clickado.

  • O carrosel somente é renderizado se o parâmetro 'TIPS_CAROUSEL' estiver devidamente configurado e se houver cards preenchidos e sinalizados como 'ATIVO'.

Introdução

A partir da versão 2.55.0 de 20 de Dezembro de 2021, foi incorporado ao NFS recursos de Internacionalização. Assim, passa a ser possível usar o ambiente NFS em idiomas além do Nativo.

Pré-requesitos

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

  • Existência das Tabelas "app_core_i18n" e "app_core_i18n_language", que são criadas pelo CORE.

Configurações

Essa funcionalidade não requer configurações, devendo apenas atentar para o devido preenchimento das tabelas relacionadas às traduções.

Orientações Gerais

  1. Por padrão, apenas o Idioma Nativo está habilitado;
  2. Para a criação de novos idiomas é necessário a intervenção do Core;
  3. Nessa versão não existem internacionalização para Números ou Datas;
  4. Para traduzir termos em entryPoints (PHP) deve-se recorrer ao método core\xDS::translate('expressao');;
  5. Para traduzir termos em Templates Twigs deve-se recorrer à macro i18n: {{ 'termo'|i18n }} ou {{ variavel|i18n }};
  6. Para traduzir termos em JavaScript deve-ser recorrer aos métodos window.i18n.get('termo'); ou App.translate('termo');.

Configurar novo idioma

Para configurar um novo idioma basta acessar o menu Sisitema -> I18n Internacionalização -> Idioma, ao acessá-lo bata adicionar o novo idioma conforme o exemplo na imagem abaixo:

internacionalizacao-idioma.png

Após concluir a configuração, ao acessar o sistema já será possível visualizar a bandeira do idioma configurado, como na imagem abaixo:

internacionalização-tela_inicial.png

Mapa de Apontamentos - GenericMap

O Mapa de Apontamentos tem como objetivo principal exibir Dados de Apontamentos referentes a uma Entidade Principal.

Pré-requisitos

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

  1. Painel para exibir os detalhes dos Apontamentos;
  2. Tabelas "nfs_mapa_generico" e "nfs_mapa_generico_relatorio" criadas na seguinte estrutura:
CREATE TABLE `nfs_mapa_generico` (
  `SEQ_DB` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `EMPRESA` int(11) DEFAULT NULL,
  `FILIAL` int(11) DEFAULT NULL,
  `LOCAL` int(11) DEFAULT NULL,
  `TABLENAME` varchar(100) DEFAULT NULL,
  `TITLE` varchar(100) DEFAULT NULL,
  `MAIN_TABLE` varchar(200) DEFAULT NULL,
  `DISPLAY_FIELDS` varchar(1000) DEFAULT NULL,
  `OPTIONS` varchar(500) DEFAULT NULL,
  `REPORT_DAYS` varchar(500) DEFAULT NULL,
  `REPORT_TYPES` varchar(500) DEFAULT NULL,
  `QUERY_STATEMENTS` text,
  `SUBQUERY_STATEMENTS` text,
  `JORNADA_QUERY` text,
  `FILTERS` varchar(1000) DEFAULT NULL,
  `LEGENDS` varchar(1000) DEFAULT NULL,
  `INDICATORS` varchar(1000) DEFAULT NULL,
  `ENABLED` int(1) DEFAULT NULL,
  PRIMARY KEY (`SEQ_DB`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `nfs_mapa_generico_relatorio` (
  `SEQ_DB` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `EMPRESA` int(11) DEFAULT NULL,
  `FILIAL` int(11) DEFAULT NULL,
  `LOCAL` int(11) DEFAULT NULL,
  `MAPA_GENERICO_SEQ_DB` bigint(20) unsigned DEFAULT NULL,
  `TIPO_RELATORIO` varchar(20) DEFAULT NULL,
  `QUERY_STATEMENTS` text,
  `DISPLAY_FIELDS` varchar(1000) DEFAULT NULL,
  `SUBQUERY_STATEMENTS` text,
  `ENABLED` int(1) DEFAULT NULL,
  PRIMARY KEY (`SEQ_DB`),
  KEY `idx_nfs_mapa_generico_relatorio` (`SEQ_DB`),
  KEY `fk_nfs_mapa_generico_relatorio_seq_db` (`MAPA_GENERICO_SEQ_DB`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  1. Caso não esteja habilitado, habilitar módulo "Rotas Personalizada";
  2. *Criar rota personalizada para "/custom/mapa/<tablename>";

*devemos passar como tablename o nome que definimos como TABLENAME para o mapa genérico na tabela nfs_mapa_generico ; { .is-warning }

Construção da consulta

Todo o processo de construção da consulta para este módulo usou como base a consulta do BoBAgro, que consiste do resultado de 3 subconsultas, sendo:

  1. A primeira subconsulta obtem os dados dos apontamentos que são iniciados e finalizados no mesmo dia:
-- 1ª Subconsulta
INI_DH = '01/01/2019 08:00:00'
FIM_DH = '01/01/2019 16:59:59'
  1. A segunda e terceira subconsultas obtem os dados dos apontamentos que são iniciados e terminados em dias diferentes. Assim a segunda subconsulta obtem os dados parciais do apontamento até as 23h59'59 do mesmo dia, e a terceira subconsulta os dados parcias restantes do mesmo apontamento:
DT_INI = '01/01/2019 22:00:00'
DT_FIM = '02/01/2019 06:00:00'

-- 2ª Subconsulta
DT_INI = '01/01/2019 22:00:00' e DT_FIM = '01/01/2019 23:59:59'
-- 3ª Subconsulta
DT_INI = '02/01/2019 00:00:00' e DT_FIM = '02/01/2019 06:00:00'

Tabela nfs_mapa_generico

Possui as definições principais do ambiente, sendo:

1. TABLENAME:

Apelido/Nome da entidade definido para o mapa genérico; (sera usado para acessar o mapa generico como descrito no 4º item dos pré-requisitos desse módulo)

TABLENAME ='EQUIPAMENTO'

2. TITLE:

Título do Mapa exibido no topo do módulo;

TITLE ='Mapa de Apontamento Máquina'

3. MAIN_TABLE:

Nome da tabela principal (usada como link entre para o Painel QIG);

MAIN_TABLE ='EQUIPAMENTO'

4. DISPLAY_FIELDS:

Configuração JSON de exibição dos dados principais;

{
  "KEY": "idEquipamento",
  "TITLE": "codigoEquipamento",
  "FIELD_LINK" : "idEquipamento",
  "DATAS": {
    "INI": "dataAbertura",
    "FIM": "dataFinal"
  },
  "COLUNAS": {
    "descricaoModelo": "Modelo",
    "codigoEquipamento": "Equipamento"
  },
  "INDICADOR": "produtiva",
  "TOOLTIP": {
    "codigoOperacao": "Código Atividade",
    "descOperacao": "Descrição Atividade",
    "codigoOperador": "Código Operador",
    "nomeOperador": "Nome Operador"
  }
}

Onde:

  • KEY: chave principal da consulta;
  • TITLE: título do gráfico gerado;
  • FIELD_LINK: Usado para quando o Detalhes não é o mesmo dados que KEY, no caso se for um Mapa de Apontamentos de Mão de Obra, onde o Boletim é por Encarregado e o Mapa deve ser pelo operador, com isso para não ficar o LINK para o Operador, deve ser passado o SEQ_DB do encarregado para poder abrir seu Detalhes;
  • DATAS: alias das colunas INI_DH e FIM_DH das subconsultas;
  • COLUNAS: labels exibidas nas tabela de resultados;
  • INDICADOR: atividade Produtiva ou Improdutiva;
  • TOOLTIP: colunas:labels exibidas no tooltip do gráfico.
  1. OPTIONS: dados opcionais (por ora, usado apenas um valor de Jornada de Trabalho Padrão);
{
  "JORNADA_PADRAO": 12
}

6. REPORT_DAYS:

Configuração JSON para perído de dias processados no relatório (esse valor usa a data fornecida como referência);

{
  "5": {
    "label": "-5 dias",
    "value": 5,
    "selected": true
  },
  "10": {
    "label": "-10 dias",
    "value": 10,
    "selected": false
  },
  "15": {
    "label": "-15 dias",
    "value": 15,
    "selected": false
  }
}

Onde:

  • label: label exibida no drop-down;
  • value: quantidade de dias;
  • selected: se pré-selecionado ou não.

7. REPORT_TYPES:

Configuração JSON para tipos de relatório gerado. Cada tipo especificado requer de um registro na tabela nfs_mapa_generico_relatorio com detalhes do relatório gerado);

{
  "tipoPorcentagem": {
    "label": "Porcentagem",
    "value": "porcentagem",
    "selected": true
  },
  "tipoHoras": {
    "label": "Horas",
    "value": "horas",
    "selected": false
  },
  "tipoProducao": {
    "value": "producao",
    "label": "Produção",
    "selected": false
  }
}

Onde:

  • label: label do drop-down;
  • value: link com tipo de relatório em nfs_mapa_generico_relatorio;
  • selected: se pré-selecionado ou não.

8. QUERY_STATEMENTS:

Statements usados para montar a consulta principal (comentados inline para melhor entendimento);

{
  "SELECT": [
    "e.SEQ_DB as idEquipamento", // chave principal da consulta
    "e.CODIGO as codigoEquipamento", // colunas exibidas nas tabela de resultados
    "m.SEQ_DB as idModelo",
    "m.DESCRICAO as descricaoModelo", // colunas exibidas nas tabela de resultados
    "dados.dataAbertura", // datas de referência
    "dados.dataFinal",
    "codigoOperacao", // colunas usadas no tooltip e indicador
    "descOperacao",
    "produtiva",
    "codigoOperador",
    "nomeOperador"
  ],
  "FROM": [], // consulta baseada em subquery
  "SUBQUERY": ["dados", "idEquipamento"], // resultado da subquery com chave
  "JOIN": [
    "right join app_equipamento e on e.SEQ_DB = dados.idEquipamento",
    "inner join app_classe_operacional c on e.CLASSE_OPERACIONAL_SEQ_DB = c.SEQ_DB",
    "inner join app_modelo m on e.MODELO_SEQ_DB = m.SEQ_DB"
  ],
  "WHERE": [
    "e.ATIVO = 1 and e.DELETED = 0 and e.EMPRESA in (:empresa, 9999) and e.FILIAL in (:filial, 9999) and e.LOCAL in (:local, 9999)",
    "c.ATIVO = 1 and c.DELETED = 0 and c.EMPRESA in (:empresa, 9999) and c.FILIAL in (:filial, 9999) and c.LOCAL in (:local, 9999)",
    "m.ATIVO = 1 and m.DELETED = 0 and m.EMPRESA in (:empresa, 9999) and m.FILIAL in (:filial, 9999) and m.LOCAL in (:local, 9999)"
  ],
  "ORDER_BY": [
    "dados.idModelo",
    "dados.codigoEquipamento",
    "dados.dataAbertura"
  ]
}

9. SUBQUERY_STATEMENTS:

Statements usados para montar subconsultas (comentados inline para melhor entendimento);

{
  "SELECT": [
    // resultado das subquery's
    "eq.SEQ_DB as idEquipamento",
    "eq.CODIGO as codigoEquipamento",
    "cl.SEQ_DB as idClasse",
    "mo.SEQ_DB as idModelo",
    "mo.DESCRICAO as descricaoModelo",
    "se.SEQ_DB as idRegional",
    "coalesce(atv.CODIGO, '') as codigoOperacao",
    "coalesce(atv.DESCRICAO, '') as descOperacao",
    "coalesce(atv.FLAG_PRODUTIVA, 0) as produtiva",
    "coalesce(op.CRACHA, '') as codigoOperador",
    "coalesce(op.NOME, '') as nomeOperador"
  ],
  "FROM": [
    // tabela principal das subquery's
    ["app_apontamento_maquina", "ap"]
  ],
  "JOIN": [
    // relacionamentos de cada subquery
    "inner join app_boletim_maquina bo on bo.SEQ_DB = ap.SEQ_DB_DEVICE_MASTER_SEQ_DB",
    "inner join app_equipamento eq on eq.SEQ_DB = bo.EQUIPAMENTO_SEQ_DB",
    "inner join app_modelo mo on mo.SEQ_DB = eq.MODELO_SEQ_DB",
    "inner join app_classe_operacional cl on cl.SEQ_DB = eq.CLASSE_OPERACIONAL_SEQ_DB",
    "inner join app_setor se on se.SEQ_DB = ap.SETOR_SEQ_DB",
    "left join app_atividade atv on atv.SEQ_DB = ap.ATIVIDADE_SEQ_DB",
    "left join app_operador op on op.SEQ_DB = bo.OPERADOR_SEQ_DB"
  ],
  "WHERE": [
    // filtros (só apontamentos fechados)
    "ap.ATIVO = 1 and ap.DELETED = 0 and ap.EMPRESA in (:empresa, 9999) and ap.FILIAL in (:filial, 9999) and ap.LOCAL in (:local, 9999) and ap.FIM_DH is not null"
  ],
  "GROUP_BY": [
    "idEquipamento",
    "codigoEquipamento",
    "idClasse",
    "idModelo",
    "descricaoModelo",
    "ap.FIM_DH",
    "ap.INI_DH",
    "idRegional",
    "codigoOperacao",
    "descOperacao",
    "produtiva",
    "codigoOperador",
    "nomeOperador"
  ],
  "UNION": [
    {
      // diferenças entre as subquery's
      "SELECT": [
        // subquery 1: apontamentos iniciados e finalizados no mesmo dia
        "ap.INI_DH as dataAbertura",
        "ap.FIM_DH as dataFinal"
      ],
      "WHERE": [
        "ap.INI_DH between concat(:de, ' 00:00:00') and concat(:ate, ' 23:59:59')",
        "date(ap.INI_DH) = date(ap.FIM_DH)"
      ]
    },
    {
      "SELECT": [
        // subquery 2: apontamentos iniciados e finalizados no próximo dia (até fim do dia)
        "ap.INI_DH as dataAbertura",
        "concat(date(ap.INI_DH), ' 23:59:59') as dataFinal"
      ],
      "WHERE": [
        "(ap.INI_DH between concat(:de, ' 00:00:00') and concat(:ate, ' 23:59:59') or ap.FIM_DH between concat(:de, ' 00:00:00') and concat(:ate, ' 23:59:59'))",
        "ap.INI_DH >= concat(:de, ' 00:00:00')",
        "date(ap.INI_DH) < date(ap.FIM_DH)"
      ]
    },
    {
      "SELECT": [
        // subquery 2: apontamentos iniciados e finalizados no próximo dia (0h em diante)
        "concat(date_add(date(ap.INI_DH), interval 1 day), ' 00:00:00') as dataAbertura",
        "ap.FIM_DH as dataFinal"
      ],
      "WHERE": [
        "ap.INI_DH >= concat(:de, ' 00:00:00')",
        "ap.FIM_DH <= concat(:ate, ' 23:59:59')",
        "date(ap.INI_DH) < date(ap.FIM_DH)"
      ]
    }
  ]
}

10. JORNADA_QUERY:

QueryBuilder para obter jornada de trabalho da entidade (são retornadas as jornadas de trabalho de todos os dias);

{
  "SELECT": ["ej.EQUIPAMENTO_SEQ_DB AS ID", "jor.*"],
  "FROM": [["equipamento_jornada", "ej"]],
  "JOIN": [["ej", "jornada", "jor", "jor.SEQ_DB = ej.JORNADA_SEQ_DB"]],
  "WHERE": [
    "ej.EQUIPAMENTO_SEQ_DB in (:lista)",
    "ej.INI_VIGENCIA <= :data",
    "(ej.FIM_VIGENCIA >= :data or ej.FIM_VIGENCIA is null)"
  ]
}

IMPORTANTE: As cláusulas (SELECT, FROM etc.) devem está em CAIXA ALTA; em futuras versões esses parâmetros serão normalizados, seguindo as definições do QueryBuilder. {.is-danger}

Caso queira utilizar algum campo em especifico ao invés de utilizar uma tabela de jornadas, podemos especificar esse campo definindo no registro(s) o valor JORNADA_POR_CAMPO igual a 1 e adicionando o alias JORNADA_VALOR no campo a ser usado como o valor de jornada, exemplo:

{
    "SELECT": [
        "eqp.SEQ_DB AS ID",
        "eqp.DISPONIBILIDADE AS JORNADA_VALOR",
        "1 as JORNADA_POR_CAMPO"
    ],
    "FROM": [
        ["eqp", "eqp"]
    ],
    "WHERE": [
        "eqp.SEQ_DB in (:lista)"
    ]
}

11. FILTERS:

Filtros disponíveis para composição do relatório;

{
  "classe_operacional": {
    "label": "Classe Operacional",
    "query_alias": "c",
    "subquery_alias": "cl",
    "id": "SEQ_DB",
    "descricao": "DESCRICAO",
    "multiple": true,
    "subfilter": "equipamento"
  },
  "modelo": {
    "label": "Modelo",
    "query_alias": "m",
    "subquery_alias": "mo",
    "id": "SEQ_DB",
    "descricao": "DESCRICAO",
    "multiple": true,
    "subfilter": "equipamento"
  },
  "equipamento": {
    "label": "Equipamento",
    "query_alias": "e",
    "subquery_alias": "eq",
    "id": "SEQ_DB",
    "descricao": "DESCRICAO",
    "multiple": true,
    "subfilter": ""
  },
  "setor": {
    "label": "Setor / Regional",
    "query_alias": "",
    "subquery_alias": "se",
    "id": "SEQ_DB",
    "descricao": "DESCRICAO",
    "multiple": true,
    "subfilter": ""
  }
}

Onde:

  • label: label do formulário de filtro;
  • query_alias: alias para consulta principal (preenchido se aplicável);
  • subquery_alias: alias para subquery (preenchido se aplicável);
  • id: valor para filtro;
  • descricao: descrição para filtro;
  • multiple: se permite filtrar por mais de um valor;
  • subfilter: preenchido se valores filtram outros filtros.

Se as chaves query_alias e subquery_alias forem preenchidas os filtros serão aplicados na consulta principal e/ou na subquery, respectivamente. {.is-danger}

12. LEGENDS:

Legendas principais para apontamentos em relação à jornada de trabalho da entidade;

{
  "zero": {
    "label": "0%", // label da legenda
    "color": "#FF5050", // cor da legenda
    "eval": "[percent] == 0" // operção lógica sobre percentual obtido na consulta
  },
  "insatisfatorio": {
    "label": "> 0%",
    "color": "#E59400",
    "eval": "[percent] > 0 && [percent] < 50"
  },
  "satisfatorio": {
    "label": ">= 50%",
    "color": "#ffff00",
    "eval": "[percent] >= 50 && [percent] < 90"
  },
  "otimo": {
    "label": ">= 90%",
    "color": "#92D050",
    "eval": "[percent] >= 90"
  }
}

Onde:

  • label: label da legenda;
  • color: cor da legenda.
  • eval: operção lógica sobre percentual obtido na consulta.
  1. INDICATORS: Indicadores exibidos no gráfico (atividade produtiva ou não, ou demais indicadores).
{
  "produtiva": {
    "value": 1,
    "label": "Produtiva",
    "color": "#92D050"
  },
  "improdutiva": {
    "value": 0,
    "label": "Produtiva",
    "color": "#FF5050"
  }
}

Onde:

  • value: valor obtido da consulta;
  • label: label exibido no legenda;
  • color: cor da legenda.

Tabela nfs_mapa_generico_relatorio

Possui as definições de cada tipo de relatório gerado (coluna REPORT_TYPES da nfs_mapa_generico), sendo:

1. MAPA_GENERICO_SEQ_DB:

FK da tabela nfs_mapa_generico;

MAPA_GENERICO_SEQ_DB=1

2. TIPO_RELATORIO:

valor do tipo de relatório selecionado;

TIPO_RELATORIO='porcentagem'

3. QUERY_STATEMENTS:

coluna(s) da consulta principal que será usada na construção do tipo de relatório;

{
  "SELECT": [
    "timestampdiff(second, cast(dataAbertura as datetime), cast(dataFinal as datetime)) as tempoApontado"
  ]
}

Ou, para o tipo producao da entidade equipamento da BoBAgro, usa-se os seguintes dados:

{
  "SELECT": ["producao as tempoApontado", "divisao"]
}

Observações Normalmente deve-se especificar aqui o campo que será usado como valor de referência para o relatório. Nos exemplos acima temos o campo tempoApontamento que é o valor em segundos usado para determinar o percentual de apontamento por Jornada de Trabalho da entidade. No segundo exemplo temos a coluna divisao, que é usada para criar um agrupamento adicional (além do agrupamento por dia); nesse exemplo, o agrupamento seria o Código da Fazenda e o Código do Talhão descritos no SUBQUERY_STATEMENTS a seguir.

4. DISPLAY_FIELDS:

definições de exibição e/ou agrupamento dos dados de acordo com o tipo de relatório;

{
  "MAIN": "tempoApontado",
  "TIPO": "absolute",
  "DIVISAO": ""
}

DICA

  • MAIN: refere-se ao valor de referência para o relatório;
  • TIPO: tipo de formatação/exibição do dados. Os valores previstos são "absolute" (valor do campo MAIN convertido em horas, por exemplo, ou producao, no tipo de relatório "producao"*), "percent" (valor exibido em percentual em relação a Jornada de Trabalho da Entidade) ou "html" (este é usando quando existe agrupamento de dados, p.ex., tipo de relatório producao, onde os dados são agrupados por Fazenda/Talhão);
  • DIVISAO: especificado quando existe agrupamento de dados além do dia.

5. SUBQUERY_STATEMENTS:

Statements que serão usando nas subconsultas. Caso as subquery's de um determinado tipo de relatório requera mais colunas/relacionamento/agrupamento, estes são especificados aqui;

{
  "SELECT": [
    "sum(pr.producao) as producao",
    "concat(fz.CODIGO, '-', coalesce(tl.CODIGO, '')) as divisao "
  ],
  "JOIN": [
    "left join app_apontamento_maquina_producao pr on pr.SEQ_DB_DEVICE_MASTER_SEQ_DB = bo.SEQ_DB and pr.ATIVIDADE_SEQ_DB = atv.SEQ_DB and pr.ATIVO = 1 and pr.DELETED = 0 and pr.EMPRESA in (:empresa, 9999) and pr.FILIAL in (:filial, 9999) and pr.LOCAL in (:local, 9999)",
    "left join app_fazenda fz on fz.SEQ_DB = ap.FAZENDA_SEQ_DB and fz.ATIVO = 1 and fz.DELETED = 0 and fz.EMPRESA in (:empresa, 9999) and fz.FILIAL in (:filial, 9999) and fz.LOCAL in (:local, 9999)",
    "left join app_talhao tl on tl.SEQ_DB = pr.TALHAO_SEQ_DB and tl.ATIVO = 1 and tl.DELETED = 0 and tl.EMPRESA in (:empresa, 9999) and tl.FILIAL in (:filial, 9999) and tl.LOCAL in (:local, 9999)"
  ],
  "GROUP_BY": ["divisao"]
}

Referências

Como referência, segue duas consultas geradas de acordo com os dados usados nos exemplos:

  1. Entidade "EQUIPAMENTO" e Tipo de Relatório "porcentagem" no período 23/01/2019 a 27/01/2019
select
  e.SEQ_DB as idEquipamento,
  e.CODIGO as codigoEquipamento,
  m.SEQ_DB as idModelo,
  m.DESCRICAO as descricaoModelo,
  dados.dataAbertura,
  dados.dataFinal,
  codigoOperacao,
  descOperacao,
  produtiva,
  codigoOperador,
  nomeOperador,
  timestampdiff(second,
  cast(dataAbertura as datetime),
  cast(dataFinal as datetime)) as tempoApontado
from (
  select
    eq.SEQ_DB as idEquipamento,
    eq.CODIGO as codigoEquipamento,
    cl.SEQ_DB as idClasse,
    mo.SEQ_DB as idModelo,
    mo.DESCRICAO as descricaoModelo,
    coalesce(atv.CODIGO, '') as codigoOperacao,
    coalesce(atv.DESCRICAO, '') as descOperacao,
    coalesce(atv.FLAG_PRODUTIVA, 0) as produtiva,
    coalesce(op.CRACHA, '') as codigoOperador,
    coalesce(op.NOME, '') as nomeOperador,
    se.SEQ_DB as idRegional,
    ap.INI_DH as dataAbertura,
    ap.FIM_DH as dataFinal
  from app_apontamento_maquina as ap
  inner join app_boletim_maquina bo on bo.SEQ_DB = ap.SEQ_DB_DEVICE_MASTER_SEQ_DB
  inner join app_equipamento eq on eq.SEQ_DB = bo.EQUIPAMENTO_SEQ_DB
  inner join app_modelo mo on mo.SEQ_DB = eq.MODELO_SEQ_DB
  inner join app_classe_operacional cl on cl.SEQ_DB = eq.CLASSE_OPERACIONAL_SEQ_DB
  inner join app_setor se on se.SEQ_DB = ap.SETOR_SEQ_DB
  left join app_atividade atv on atv.SEQ_DB = ap.ATIVIDADE_SEQ_DB
  left join app_operador op on op.SEQ_DB = bo.OPERADOR_SEQ_DB
  where
    ap.ATIVO = 1 and ap.DELETED = 0 and ap.EMPRESA in (1, 9999) and ap.FILIAL in (1, 9999) and ap.LOCAL in (1, 9999)
    and ap.FIM_DH is not null
    and ap.INI_DH between '2019-01-23 00:00:00' and '2019-01-27 23:59:59'
    and date(ap.INI_DH) = date(ap.FIM_DH)
  group by
    idEquipamento,
    codigoEquipamento,
    idClasse,
    idModelo,
    descricaoModelo,
    ap.FIM_DH,
    ap.INI_DH,
    idRegional,
    codigoOperacao,
    descOperacao,
    produtiva,
    codigoOperador,
    nomeOperador
  union all
  select
    eq.SEQ_DB as idEquipamento,
    eq.CODIGO as codigoEquipamento,
    cl.SEQ_DB as idClasse,
    mo.SEQ_DB as idModelo,
    mo.DESCRICAO as descricaoModelo,
    coalesce(atv.CODIGO, '') as codigoOperacao,
    coalesce(atv.DESCRICAO, '') as descOperacao,
    coalesce(atv.FLAG_PRODUTIVA, 0) as produtiva,
    coalesce(op.CRACHA, '') as codigoOperador,
    coalesce(op.NOME, '') as nomeOperador,
    se.SEQ_DB as idRegional,
    ap.INI_DH as dataAbertura,
    concat(date(ap.INI_DH), ' 23:59:59') as dataFinal
  from app_apontamento_maquina as ap
  inner join app_boletim_maquina bo on bo.SEQ_DB = ap.SEQ_DB_DEVICE_MASTER_SEQ_DB
  inner join app_equipamento eq on eq.SEQ_DB = bo.EQUIPAMENTO_SEQ_DB
  inner join app_modelo mo on mo.SEQ_DB = eq.MODELO_SEQ_DB
  inner join app_classe_operacional cl on cl.SEQ_DB = eq.CLASSE_OPERACIONAL_SEQ_DB
  inner join app_setor se on se.SEQ_DB = ap.SETOR_SEQ_DB
  left join app_atividade atv on atv.SEQ_DB = ap.ATIVIDADE_SEQ_DB
  left join app_operador op on op.SEQ_DB = bo.OPERADOR_SEQ_DB
  where
    ap.ATIVO = 1 and ap.DELETED = 0 and ap.EMPRESA in (1, 9999) and ap.FILIAL in (1, 9999) and ap.LOCAL in (1, 9999)
    and ap.FIM_DH is not null
    and (ap.INI_DH between '2019-01-23 00:00:00' and '2019-01-27 23:59:59' or ap.FIM_DH between '2019-01-23 00:00:00' and '2019-01-27 23:59:59')
    and ap.INI_DH >= '2019-01-23 00:00:00'
    and date(ap.INI_DH) < date(ap.FIM_DH)
  group by
    idEquipamento,
    codigoEquipamento,
    idClasse,
    idModelo,
    descricaoModelo,
    ap.FIM_DH,
    ap.INI_DH,
    idRegional,
    codigoOperacao,
    descOperacao,
    produtiva,
    codigoOperador,
    nomeOperador
  union all
  select
    eq.SEQ_DB as idEquipamento,
    eq.CODIGO as codigoEquipamento,
    cl.SEQ_DB as idClasse,
    mo.SEQ_DB as idModelo,
    mo.DESCRICAO as descricaoModelo,
    coalesce(atv.CODIGO, '') as codigoOperacao,
    coalesce(atv.DESCRICAO, '') as descOperacao,
    coalesce(atv.FLAG_PRODUTIVA, 0) as produtiva,
    coalesce(op.CRACHA, '') as codigoOperador,
    coalesce(op.NOME, '') as nomeOperador,
    se.SEQ_DB as idRegional,
    concat(date_add(date(ap.INI_DH), interval 1 day), ' 00:00:00') as dataAbertura,
    ap.FIM_DH as dataFinal
  from app_apontamento_maquina as ap
  inner join app_boletim_maquina bo on bo.SEQ_DB = ap.SEQ_DB_DEVICE_MASTER_SEQ_DB
  inner join app_equipamento eq on eq.SEQ_DB = bo.EQUIPAMENTO_SEQ_DB
  inner join app_modelo mo on mo.SEQ_DB = eq.MODELO_SEQ_DB
  inner join app_classe_operacional cl on cl.SEQ_DB = eq.CLASSE_OPERACIONAL_SEQ_DB
  inner join app_setor se on se.SEQ_DB = ap.SETOR_SEQ_DB
  left join app_atividade atv on atv.SEQ_DB = ap.ATIVIDADE_SEQ_DB
  left join app_operador op on op.SEQ_DB = bo.OPERADOR_SEQ_DB
  where
    ap.ATIVO = 1 and ap.DELETED = 0 and ap.EMPRESA in (1, 9999) and ap.FILIAL in (1, 9999) and ap.LOCAL in (1, 9999)
    and ap.FIM_DH is not null
    and ap.INI_DH >= '2019-01-23 00:00:00'
    and ap.FIM_DH <= '2019-01-27 23:59:59'
    and date(ap.INI_DH) < date(ap.FIM_DH)
  group by
    idEquipamento,
    codigoEquipamento,
    idClasse,
    idModelo,
    descricaoModelo,
    ap.FIM_DH,
    ap.INI_DH,
    idRegional,
    codigoOperacao,
    descOperacao,
    produtiva,
    codigoOperador,
    nomeOperador
) dados
right join app_equipamento e on e.SEQ_DB = dados.idEquipamento
inner join app_classe_operacional c on e.CLASSE_OPERACIONAL_SEQ_DB = c.SEQ_DB
inner join app_modelo m on e.MODELO_SEQ_DB = m.SEQ_DB
where
  e.ATIVO = 1 and e.DELETED = 0 and e.EMPRESA in (1, 9999) and e.FILIAL in (1, 9999) and e.LOCAL in (1, 9999)
  and c.ATIVO = 1 and c.DELETED = 0 and c.EMPRESA in (1, 9999) and c.FILIAL in (1, 9999) and c.LOCAL in (1, 9999)
  and m.ATIVO = 1 and m.DELETED = 0 and m.EMPRESA in (1, 9999) and m.FILIAL in (1, 9999) and m.LOCAL in (1, 9999)
order by
  dados.idModelo,
  dados.codigoEquipamento,
  dados.dataAbertura;
  1. Entidade "EQUIPAMENTO" e Tipo de Relatório "producao":
select
  e.SEQ_DB as idEquipamento,
  e.CODIGO as codigoEquipamento,
  m.SEQ_DB as idModelo,
  m.DESCRICAO as descricaoModelo,
  dados.dataAbertura,
  dados.dataFinal,
  codigoOperacao,
  descOperacao,
  produtiva,
  codigoOperador,
  nomeOperador,
  producao as tempoApontado,
  divisao
from (
  select
    eq.SEQ_DB as idEquipamento,
    eq.CODIGO as codigoEquipamento,
    cl.SEQ_DB as idClasse,
    mo.SEQ_DB as idModelo,
    mo.DESCRICAO as descricaoModelo,
    coalesce(atv.CODIGO, '') as codigoOperacao,
    coalesce(atv.DESCRICAO, '') as descOperacao,
    coalesce(atv.FLAG_PRODUTIVA, 0) as produtiva,
    coalesce(op.CRACHA, '') as codigoOperador,
    coalesce(op.NOME, '') as nomeOperador,
    se.SEQ_DB as idRegional,
    sum(pr.producao) as producao,
    concat(fz.CODIGO, '-', coalesce(tl.CODIGO, '')) as divisao,
    ap.INI_DH as dataAbertura,
    ap.FIM_DH as dataFinal
  from app_apontamento_maquina as ap
  inner join app_boletim_maquina bo on bo.SEQ_DB = ap.SEQ_DB_DEVICE_MASTER_SEQ_DB
  inner join app_equipamento eq on eq.SEQ_DB = bo.EQUIPAMENTO_SEQ_DB
  inner join app_modelo mo on mo.SEQ_DB = eq.MODELO_SEQ_DB
  inner join app_classe_operacional cl on cl.SEQ_DB = eq.CLASSE_OPERACIONAL_SEQ_DB
  inner join app_setor se on se.SEQ_DB = ap.SETOR_SEQ_DB
  left join app_atividade atv on atv.SEQ_DB = ap.ATIVIDADE_SEQ_DB
  left join app_operador op on op.SEQ_DB = bo.OPERADOR_SEQ_DB
  left join app_apontamento_maquina_producao pr on pr.SEQ_DB_DEVICE_MASTER_SEQ_DB = bo.SEQ_DB and pr.ATIVIDADE_SEQ_DB = atv.SEQ_DB
  left join app_fazenda fz on fz.SEQ_DB = ap.FAZENDA_SEQ_DB
  left join app_talhao tl on tl.SEQ_DB = pr.TALHAO_SEQ_DB
  where
    ap.ATIVO = 1 and ap.DELETED = 0 and ap.EMPRESA in (1, 9999) and ap.FILIAL in (1, 9999) and ap.LOCAL in (1, 9999)
    and ap.FIM_DH is not null
    and ap.INI_DH between '2019-01-23 00:00:00' and '2019-01-27 23:59:59'
    and date(ap.INI_DH) = date(ap.FIM_DH)
  group by
    idEquipamento,
    codigoEquipamento,
    idClasse,
    idModelo,
    descricaoModelo,
    ap.FIM_DH,
    ap.INI_DH,
    idRegional,
    codigoOperacao,
    descOperacao,
    produtiva,
    codigoOperador,
    nomeOperador,
    divisao
  union all
  select
    eq.SEQ_DB as idEquipamento,
    eq.CODIGO as codigoEquipamento,
    cl.SEQ_DB as idClasse,
    mo.SEQ_DB as idModelo,
    mo.DESCRICAO as descricaoModelo,
    coalesce(atv.CODIGO, '') as codigoOperacao,
    coalesce(atv.DESCRICAO, '') as descOperacao,
    coalesce(atv.FLAG_PRODUTIVA, 0) as produtiva,
    coalesce(op.CRACHA, '') as codigoOperador,
    coalesce(op.NOME, '') as nomeOperador,
    se.SEQ_DB as idRegional,
    sum(pr.producao) as producao,
    concat(fz.CODIGO, '-', coalesce(tl.CODIGO, '')) as divisao,
    ap.INI_DH as dataAbertura,
    concat(date(ap.INI_DH), ' 23:59:59') as dataFinal
  from app_apontamento_maquina as ap
  inner join app_boletim_maquina bo on bo.SEQ_DB = ap.SEQ_DB_DEVICE_MASTER_SEQ_DB
  inner join app_equipamento eq on eq.SEQ_DB = bo.EQUIPAMENTO_SEQ_DB
  inner join app_modelo mo on mo.SEQ_DB = eq.MODELO_SEQ_DB
  inner join app_classe_operacional cl on cl.SEQ_DB = eq.CLASSE_OPERACIONAL_SEQ_DB
  inner join app_setor se on se.SEQ_DB = ap.SETOR_SEQ_DB
  left join app_atividade atv on atv.SEQ_DB = ap.ATIVIDADE_SEQ_DB
  left join app_operador op on op.SEQ_DB = bo.OPERADOR_SEQ_DB
  left join app_apontamento_maquina_producao pr on pr.SEQ_DB_DEVICE_MASTER_SEQ_DB = bo.SEQ_DB and pr.ATIVIDADE_SEQ_DB = atv.SEQ_DB
  left join app_fazenda fz on fz.SEQ_DB = ap.FAZENDA_SEQ_DB
  left join app_talhao tl on tl.SEQ_DB = pr.TALHAO_SEQ_DB
  where
    ap.ATIVO = 1 and ap.DELETED = 0 and ap.EMPRESA in (1, 9999) and ap.FILIAL in (1, 9999) and ap.LOCAL in (1, 9999)
    and ap.FIM_DH is not null
    and (ap.INI_DH between '2019-01-23 00:00:00' and '2019-01-27 23:59:59' or ap.FIM_DH between '2019-01-23 00:00:00' and '2019-01-27 23:59:59')
    and ap.INI_DH >= '2019-01-23 00:00:00'
    and date(ap.INI_DH) < date(ap.FIM_DH)
  group by
    idEquipamento,
    codigoEquipamento,
    idClasse,
    idModelo,
    descricaoModelo,
    ap.FIM_DH,
    ap.INI_DH,
    idRegional,
    codigoOperacao,
    descOperacao,
    produtiva,
    codigoOperador,
    nomeOperador,
    divisao
  union all
  select
    eq.SEQ_DB as idEquipamento,
    eq.CODIGO as codigoEquipamento,
    cl.SEQ_DB as idClasse,
    mo.SEQ_DB as idModelo,
    mo.DESCRICAO as descricaoModelo,
    coalesce(atv.CODIGO, '') as codigoOperacao,
    coalesce(atv.DESCRICAO, '') as descOperacao,
    coalesce(atv.FLAG_PRODUTIVA, 0) as produtiva,
    coalesce(op.CRACHA, '') as codigoOperador,
    coalesce(op.NOME, '') as nomeOperador,
    se.SEQ_DB as idRegional,
    sum(pr.producao) as producao,
    concat(fz.CODIGO, '-', coalesce(tl.CODIGO, '')) as divisao,
    concat(date_add(date(ap.INI_DH), interval 1 day), ' 00:00:00') as dataAbertura,
    ap.FIM_DH as dataFinal
  from app_apontamento_maquina as ap
  inner join app_boletim_maquina bo on bo.SEQ_DB = ap.SEQ_DB_DEVICE_MASTER_SEQ_DB
  inner join app_equipamento eq on eq.SEQ_DB = bo.EQUIPAMENTO_SEQ_DB
  inner join app_modelo mo on mo.SEQ_DB = eq.MODELO_SEQ_DB
  inner join app_classe_operacional cl on cl.SEQ_DB = eq.CLASSE_OPERACIONAL_SEQ_DB
  inner join app_setor se on se.SEQ_DB = ap.SETOR_SEQ_DB
  left join app_atividade atv on atv.SEQ_DB = ap.ATIVIDADE_SEQ_DB
  left join app_operador op on op.SEQ_DB = bo.OPERADOR_SEQ_DB
  left join app_apontamento_maquina_producao pr on pr.SEQ_DB_DEVICE_MASTER_SEQ_DB = bo.SEQ_DB and pr.ATIVIDADE_SEQ_DB = atv.SEQ_DB
  left join app_fazenda fz on fz.SEQ_DB = ap.FAZENDA_SEQ_DB
  left join app_talhao tl on tl.SEQ_DB = pr.TALHAO_SEQ_DB
  where
    ap.ATIVO = 1 and ap.DELETED = 0 and ap.EMPRESA in (1, 9999) and ap.FILIAL in (1, 9999) and ap.LOCAL in (1, 9999)
    and ap.FIM_DH is not null
    and ap.INI_DH >= '2019-01-23 00:00:00'
    and ap.FIM_DH <= '2019-01-27 23:59:59'
    and date(ap.INI_DH) < date(ap.FIM_DH)
  group by
    idEquipamento,
    codigoEquipamento,
    idClasse,
    idModelo,
    descricaoModelo,
    ap.FIM_DH,
    ap.INI_DH,
    idRegional,
    codigoOperacao,
    descOperacao,
    produtiva,
    codigoOperador,
    nomeOperador,
    divisao
) dados
right join app_equipamento e on e.SEQ_DB = dados.idEquipamento
inner join app_classe_operacional c on e.CLASSE_OPERACIONAL_SEQ_DB = c.SEQ_DB
inner join app_modelo m on e.MODELO_SEQ_DB = m.SEQ_DB
where
  e.ATIVO = 1 and e.DELETED = 0 and e.EMPRESA in (1, 9999) and e.FILIAL in (1, 9999) and e.LOCAL in (1, 9999)
  and c.ATIVO = 1 and c.DELETED = 0 and c.EMPRESA in (1, 9999) and c.FILIAL in (1, 9999) and c.LOCAL in (1, 9999)
  and m.ATIVO = 1 and m.DELETED = 0 and m.EMPRESA in (1, 9999) and m.FILIAL in (1, 9999) and m.LOCAL in (1, 9999)
order by
  dados.idModelo,
  dados.codigoEquipamento,
  dados.dataAbertura;

Exemplo de um Mapa de Apontamento por de Mão de Obra

Caracteriza-se Mão de Obra nesse caso onde tem Encarregado e o mesmo aponta 1 ou mais operadores.

INSERT INTO nfs_mapa_generico_relatorio (SEQ_DB, MAPA_GENERICO_SEQ_DB, TIPO_RELATORIO, QUERY_STATEMENTS, DISPLAY_FIELDS, SUBQUERY_STATEMENTS, ENABLED) VALUES(1, 1, 'porcentagem', '{
    "SELECT": [
        "timestampdiff(second, cast(dataAbertura as datetime), cast(dataFinal as datetime)) as tempoApontado"
    ]
}', '{
    "MAIN": "tempoApontado",
    "TIPO": "percent",
    "DIVISAO": ""
}', NULL, 1);
INSERT INTO nfs_mapa_generico_relatorio (SEQ_DB, MAPA_GENERICO_SEQ_DB, TIPO_RELATORIO, QUERY_STATEMENTS, DISPLAY_FIELDS, SUBQUERY_STATEMENTS, ENABLED) VALUES(2, 1, 'horas', '{
    "SELECT": [
        "timestampdiff(second, cast(dataAbertura as datetime), cast(dataFinal as datetime)) as tempoApontado"
    ]
}', '{
    "MAIN": "tempoApontado",
    "TIPO": "absolute",
    "DIVISAO": ""
}', NULL, 1);


INSERT INTO nfs_mapa_generico (SEQ_DB, EMPRESA, FILIAL, `LOCAL`, TABLENAME, TITLE, MAIN_TABLE, DISPLAY_FIELDS, `OPTIONS`, REPORT_DAYS, REPORT_TYPES, QUERY_STATEMENTS, SUBQUERY_STATEMENTS, JORNADA_QUERY, FILTERS, LEGENDS, INDICATORS, ENABLED) VALUES(1, 1, 9999, 9999, 'EFETIVO_FUNCIONARIO', 'Mapa de Apontamentos de Mão de Obra', 'EFETIVO_FUNCIONARIO', '{
	"KEY": "idFuncionario",
	"TITLE": "crachaFuncionario",
	"FIELD_LINK": "idEncarregado",
	"DATAS": {
		"INI": "dataAbertura",
		"FIM": "dataFinal"
	},
	"COLUNAS": {
		"nomeFuncionario": "Funcionário",
		"cargoFuncionario": "Cargo"
	},
	"INDICADOR": "produtiva",
	"TOOLTIP": {
		"crachaFuncionario": "Crachá Funcionário",
		"nomeFuncionario": "Nome Funcionário",
		"nomeEncarregado": "Encarregado",
		"atividade": "Atividade/Perda"
	}
}', '{
	"JORNADA_PADRAO": 8
}', '{
	"5": {
		"label": "-5 dias",
		"value": 5,
		"selected": true
	},
	"10": {
		"label": "-10 dias",
		"value": 10,
		"selected": false
	},
	"15": {
		"label": "-15 dias",
		"value": 15,
		"selected": false
	}
}', '{
	"tipoPorcentagem": {
		"value": "porcentagem",
		"label": "Porcentagem",
		"selected": true
	},
	"tipoHoras": {
		"value": "horas",
		"label": "Horas",
		"selected": false
	}
}', '{
	"SELECT": [
		"dados.idApontamento",
		"car.SEQ_DB as idCargoFuncionario",
		"car.DESCRICAO as cargoFuncionario",
		"enc.SEQ_DB as idEncarregado",
		"ff.SEQ_DB as idFuncionario",
		"ff.CRACHA as crachaFuncionario",
		"ff.NOME as nomeFuncionario",
		"coalesce(enc.CRACHA, '') as crachaEncarregado",
		"coalesce(enc.NOME, '') as nomeEncarregado",
		"dados.atividade",
		"dados.produtiva",
		"dados.dataAbertura",
		"dados.dataFinal"
	],
	"FROM": [],
	"SUBQUERY": ["dados", "idFuncionario"],
	"JOIN": [
		"inner join app_efetivo_funcionario ff on ff.SEQ_DB = dados.idFuncionario",
		"inner join app_efetivo_funcionario enc on enc.SEQ_DB = dados.idEncarregado",
		"left join app_efetivo_funcao car on car.SEQ_DB = dados.idCargoFuncionario"
	],
	"WHERE": [
		"ff.ATIVO = 1 and ff.DELETED = 0 and ff.EMPRESA in (:empresa, 9999) and ff.FILIAL in (:filial, 9999) and ff.LOCAL in (:local, 9999)",
		"enc.ATIVO = 1 and enc.DELETED = 0 and enc.EMPRESA in (:empresa, 9999) and enc.FILIAL in (:filial, 9999) and enc.LOCAL in (:local, 9999)",
		"car.ATIVO = 1 and car.DELETED = 0 and car.EMPRESA in (:empresa, 9999) and car.FILIAL in (:filial, 9999) and car.LOCAL in (:local, 9999)"
	],
	"ORDER_BY": [
		"dados.dataAbertura",
		"nomeFuncionario"
	]
}', '{
	"SELECT": [
		"ap.SEQ_DB as idApontamento",
		"f.SEQ_DB as idFuncionario",
		"coalesce(f.CRACHA, '') as crachaFuncionario",
		"coalesce(f.NOME, '') as nomeFuncionario",
		"e.SEQ_DB as idEncarregado",
		"coalesce(e.CRACHA, '') as crachaEncarregado",
		"coalesce(e.NOME, '') as nomeEncarregado",
		"ca.SEQ_DB as idCargoFuncionario",
		"coalesce(ca.DESCRICAO, '') as cargoFuncionario",
		"if(ap.TIPO_APONTAMENTO = 1, 1, 0) produtiva",
		"case when (ap.OPER_SEQ_DB IS NULL) then coalesce(i.DESCRICAO, '') when (ap.OPER_SEQ_DB IS NOT NULL AND ap.INTERFERENCIA_SEQ_DB IS NOT NULL) then coalesce(i.DESCRICAO, '') else coalesce(ati.DESCRICAO, '') end as atividade"
	],
    	"FROM": [
		["app_mo_apt", "ap"]
	],
    	"JOIN": [
		"inner join app_mo_boletim bo on bo.SEQ_DB = ap.SEQ_DB_DEVICE_MASTER_SEQ_DB ",
		"inner join app_efetivo_funcionario f ON ap.OPERADOR_SEQ_DB = f.SEQ_DB",
		"inner join app_efetivo_funcionario e ON ap.EFETIVO_FUNCIONARIO_SEQ_DB = e.SEQ_DB",
		"left join app_oper ati ON ap.OPER_SEQ_DB = ati.SEQ_DB",
		"left join app_interferencia i ON ap.INTERFERENCIA_SEQ_DB = i.SEQ_DB",
		"left join app_efetivo_funcao ca ON f.EFETIVO_FUNCAO_SEQ_DB = ca.SEQ_DB"
	],
    	"WHERE": [
		"ap.ATIVO = 1 and ap.DELETED = 0 and ap.EMPRESA in (:empresa, 9999) and ap.FILIAL in (:filial, 9999) and ap.LOCAL in (:local, 9999) and ap.FIM_DH is not null"
	],
    	"GROUP_BY": [
		"idApontamento",
		"idFuncionario",
		"crachaFuncionario",
		"nomeFuncionario",
		"idCargoFuncionario",
		"cargoFuncionario",
		"idEncarregado",
		"crachaEncarregado",
		"nomeEncarregado",
		"atividade",
		"ap.TIPO_APONTAMENTO",
		"ap.FIM_DH",
		"ap.INI_DH"
	],
	"UNION": [{
		"SELECT": [
			"ap.INI_DH as dataAbertura",
			"ap.FIM_DH as dataFinal"
		],
		"WHERE": [
			"ap.INI_DH between concat(:de, ' 00:00:00') and concat(:ate, ' 23:59:59')",
			"date(ap.INI_DH) = date(ap.FIM_DH)"
		]
	},{
		"SELECT": [
			"ap.INI_DH as dataAbertura",
			"concat(date(ap.INI_DH), ' 23:59:59') as dataFinal"
		],
		"WHERE": [
      "(ap.INI_DH between concat(:de, ' 00:00:00') and concat(:ate, ' 23:59:59') or ap.FIM_DH between concat(:de, ' 00:00:00') and concat(:ate, ' 23:59:59'))",
      "ap.INI_DH >= concat(:de, ' 00:00:00')",
			"date(ap.INI_DH) < date(ap.FIM_DH)"
		]
	},{
		"SELECT": [
			"concat(date_add(date(ap.INI_DH), interval 1 day), '' 00:00:00'') as dataAbertura",
			"ap.FIM_DH as dataFinal"
		],
		"WHERE": [
     	"ap.INI_DH >= concat(:de, ' 00:00:00')",
      "ap.FIM_DH <= concat(:ate, ' 23:59:59')",
			"date(ap.INI_DH) < date(ap.FIM_DH)"
		]
	}]
}', '{}', '{
	"efetivo_funcionario": {
		"label": "Funcionário",
		"query_alias": "ff",
		"subquery_alias": "f",
		"id": "SEQ_DB",
		"descricao": "NOME",
		"multiple": true,
		"subfilter": ""
	},
	"efetivo_funcao": {
		"label": "Cargo",
		"query_alias": "car",
		"subquery_alias": "ca",
		"id": "SEQ_DB",
		"descricao": "DESCRICAO",
		"multiple": true,
		"subfilter": "funcionario"
	}
}', '{
	"zero": {
		"label": "0%",
		"color": "#FF5050",
		"eval": "[percent] == 0"
	},
	"insatisfatorio": {
		"label": "> 0%",
		"color": "#E59400",
		"eval": "[percent] > 0 && [percent] < 50"
	},
	"satisfatorio": {
		"label": ">= 50%",
		"color": "#ffff00",
		"eval": "[percent] >= 50 && [percent] < 90"
	},
	"otimo": {
		"label": ">= 90%",
		"color": "#92D050",
		"eval": "[percent] >= 90"
	}
}', '{
    "produtiva": {
        "value": 1,
        "label": "Produtiva",
        "color": "#92D050"
    },
    "improdutiva": {
        "value": 0,
        "label": "Improdutiva",
        "color": "#FF5050"
    }
}', 1);

Resultado em SQL:

Prestar atenção no WHERE que estão filtras pela empresa, filial, local, período e funcionario (808) da civilmaster-mo;

select
	dados.idApontamento,
	car.SEQ_DB as idCargoFuncionario,
	car.DESCRICAO as cargoFuncionario,
	enc.SEQ_DB as idEncarregado,
	ff.SEQ_DB as idFuncionario,
	ff.CRACHA as crachaFuncionario,
	ff.NOME as nomeFuncionario,
	coalesce(enc.CRACHA, '') as crachaEncarregado,
	coalesce(enc.NOME, '') as nomeEncarregado,
	dados.atividade,
	dados.dataAbertura,
	dados.dataFinal,
	timestampdiff(second,
	cast(dataAbertura as datetime),
	cast(dataFinal as datetime)) as tempoApontado
from
	(
	select
		ap.SEQ_DB as idApontamento,
		f.SEQ_DB as idFuncionario,
		coalesce(f.CRACHA, '') as crachaFuncionario,
		coalesce(f.NOME, '') as nomeFuncionario,
		e.SEQ_DB as idEncarregado,
		coalesce(e.CRACHA, '') as crachaEncarregado,
		coalesce(e.NOME, '') as nomeEncarregado,
		ca.SEQ_DB as idCargoFuncionario,
		coalesce(ca.DESCRICAO, '') as cargoFuncionario,
		case
			when (ap.OPER_SEQ_DB is null) then coalesce(i.DESCRICAO, '')
			when (ap.OPER_SEQ_DB is not null
			and ap.INTERFERENCIA_SEQ_DB is not null) then coalesce(i.DESCRICAO, '')
			else coalesce(ati.DESCRICAO, '')
		end as atividade,
		ap.INI_DH as dataAbertura,
		ap.FIM_DH as dataFinal
	from app_mo_apt as ap
	inner join app_mo_boletim bo on bo.SEQ_DB = ap.SEQ_DB_DEVICE_MASTER_SEQ_DB
	inner join app_efetivo_funcionario f on ap.OPERADOR_SEQ_DB = f.SEQ_DB
	inner join app_efetivo_funcionario e on ap.EFETIVO_FUNCIONARIO_SEQ_DB = e.SEQ_DB
	left join app_oper ati on ap.OPER_SEQ_DB = ati.SEQ_DB
	left join app_interferencia i on ap.INTERFERENCIA_SEQ_DB = i.SEQ_DB
	left join app_efetivo_funcao ca on f.EFETIVO_FUNCAO_SEQ_DB = ca.SEQ_DB
	where
		ap.ATIVO = 1
		and ap.DELETED = 0
		and ap.EMPRESA in (1, 9999)
		and ap.FILIAL in (1, 9999)
		and ap.LOCAL in (9, 9999)
		and ap.FIM_DH is not null
		and f.SEQ_DB in (880)
		and ap.INI_DH between '2023-03-10 00:00:00' and '2023-03-14 23:59:59'
		and date(ap.INI_DH) = date(ap.FIM_DH)
	group by
		idApontamento,
		idFuncionario,
		crachaFuncionario,
		nomeFuncionario,
		idCargoFuncionario,
		cargoFuncionario,
		idEncarregado,
		crachaEncarregado,
		nomeEncarregado,
		atividade,
		ap.FIM_DH,
		ap.INI_DH
union all
	select
		ap.SEQ_DB as idApontamento,
		f.SEQ_DB as idFuncionario,
		coalesce(f.CRACHA, '') as crachaFuncionario,
		coalesce(f.NOME, '') as nomeFuncionario,
		e.SEQ_DB as idEncarregado,
		coalesce(e.CRACHA, '') as crachaEncarregado,
		coalesce(e.NOME, '') as nomeEncarregado,
		ca.SEQ_DB as idCargoFuncionario,
		coalesce(ca.DESCRICAO, '') as cargoFuncionario,
		case
			when (ap.OPER_SEQ_DB is null) then coalesce(i.DESCRICAO, '')
			when (ap.OPER_SEQ_DB is not null
				and ap.INTERFERENCIA_SEQ_DB is not null) then coalesce(i.DESCRICAO, '')
			else coalesce(ati.DESCRICAO, '')
		end as atividade,
		ap.INI_DH as dataAbertura,
		concat(date(ap.INI_DH), ' 23:59:59') as dataFinal
	from app_mo_apt as ap
	inner join app_mo_boletim bo on bo.SEQ_DB = ap.SEQ_DB_DEVICE_MASTER_SEQ_DB
	inner join app_efetivo_funcionario f on ap.OPERADOR_SEQ_DB = f.SEQ_DB
	inner join app_efetivo_funcionario e on ap.EFETIVO_FUNCIONARIO_SEQ_DB = e.SEQ_DB
	left join app_oper ati on ap.OPER_SEQ_DB = ati.SEQ_DB
	left join app_interferencia i on ap.INTERFERENCIA_SEQ_DB = i.SEQ_DB
	left join app_efetivo_funcao ca on f.EFETIVO_FUNCAO_SEQ_DB = ca.SEQ_DB
	where
		ap.ATIVO = 1
		and ap.DELETED = 0
		and ap.EMPRESA in (1, 9999)
		and ap.FILIAL in (1, 9999)
		and ap.LOCAL in (9, 9999)
		and ap.FIM_DH is not null
		and f.SEQ_DB in (880)
		and (ap.INI_DH between '2023-03-10 00:00:00' and '2023-03-14 23:59:59'
			or ap.FIM_DH between '2023-03-10 00:00:00' and '2023-03-14 23:59:59')
		and ap.INI_DH >= '2023-03-10 23:59:59'
		and date(ap.INI_DH) < date(ap.FIM_DH)
	group by
		idApontamento,
		idFuncionario,
		crachaFuncionario,
		nomeFuncionario,
		idCargoFuncionario,
		cargoFuncionario,
		idEncarregado,
		crachaEncarregado,
		nomeEncarregado,
		atividade,
		ap.FIM_DH,
		ap.INI_DH
union all
	select
		ap.SEQ_DB as idApontamento,
		f.SEQ_DB as idFuncionario,
		coalesce(f.CRACHA, '') as crachaFuncionario,
		coalesce(f.NOME, '') as nomeFuncionario,
		e.SEQ_DB as idEncarregado,
		coalesce(e.CRACHA, '') as crachaEncarregado,
		coalesce(e.NOME, '') as nomeEncarregado,
		ca.SEQ_DB as idCargoFuncionario,
		coalesce(ca.DESCRICAO, '') as cargoFuncionario,
		case
			when (ap.OPER_SEQ_DB is null) then coalesce(i.DESCRICAO, '')
			when (ap.OPER_SEQ_DB is not null
				and ap.INTERFERENCIA_SEQ_DB is not null) then coalesce(i.DESCRICAO, '')
			else coalesce(ati.DESCRICAO, '')
		end as atividade,
		concat(date_add(date(ap.INI_DH), interval 1 day), ' 00:00:00') as dataAbertura,
		ap.FIM_DH as dataFinal
	from app_mo_apt as ap
	inner join app_mo_boletim bo on bo.SEQ_DB = ap.SEQ_DB_DEVICE_MASTER_SEQ_DB
	inner join app_efetivo_funcionario f on ap.OPERADOR_SEQ_DB = f.SEQ_DB
	inner join app_efetivo_funcionario e on ap.EFETIVO_FUNCIONARIO_SEQ_DB = e.SEQ_DB
	left join app_oper ati on ap.OPER_SEQ_DB = ati.SEQ_DB
	left join app_interferencia i on ap.INTERFERENCIA_SEQ_DB = i.SEQ_DB
	left join app_efetivo_funcao ca on f.EFETIVO_FUNCAO_SEQ_DB = ca.SEQ_DB
	where
		ap.ATIVO = 1
		and ap.DELETED = 0
		and ap.EMPRESA in (1, 9999)
		and ap.FILIAL in (1, 9999)
		and ap.LOCAL in (9, 9999)
		and ap.FIM_DH is not null
		and f.SEQ_DB in (880)
		and ap.INI_DH >= '2023-03-10 00:00:00'
		and ap.FIM_DH <= '2023-03-14 23:59:59'
		and date(ap.INI_DH) < date(ap.FIM_DH)
	group by
		idApontamento,
		idFuncionario,
		crachaFuncionario,
		nomeFuncionario,
		idCargoFuncionario,
		cargoFuncionario,
		idEncarregado,
		crachaEncarregado,
		nomeEncarregado,
		atividade
) dados
inner join app_efetivo_funcionario ff on ff.SEQ_DB = dados.idFuncionario
inner join app_efetivo_funcionario enc on enc.SEQ_DB = dados.idEncarregado
left join app_efetivo_funcao car on car.SEQ_DB = dados.idCargoFuncionario
where
	ff.ATIVO = 1
	and ff.DELETED = 0
	and ff.EMPRESA in (1, 9999)
	and ff.FILIAL in (1, 9999)
	and ff.LOCAL in (9, 9999)
	and enc.ATIVO = 1
	and enc.DELETED = 0
	and enc.EMPRESA in (1, 9999)
	and enc.FILIAL in (1, 9999)
	and enc.LOCAL in (9, 9999)
	and car.ATIVO = 1
	and car.DELETED = 0
	and car.EMPRESA in (1, 9999)
	and car.FILIAL in (1, 9999)
	and car.LOCAL in (9, 9999)
	and ((ff.SEQ_DB in (880)) or (dados.idFuncionario in (880)))
order by
	dados.dataAbertura,
	nomeFuncionario

Definindo cor e texto dos indicadores

Através dos ALIAS indicatorColor e indicatorLegend configurados ao montar o mapa, pode ser especificado qual a cor e texto da legenda:

1. Alterando as SUBQUERY_STATEMENTS

Configuração de cor da legenda subquery statements
  • 1 - Faço o inner join da tabela grupo atividades
  • 2 - Retorno no nome do grupo como indicatorLegend e a cor do grupo como indicatorColor

2. Configuração da QUERY_STATEMENTS

Após a definição dos aliases na SUBQUERY_STATEMENTS, basta trazer o resultado na QUERY para que os efeitos sejam aplicados no mapa de apontamentos

Configuração de cor da legenda query statements

Neste caso criei uma coluna da tabela de grupo de atividade onde defino a cor

Grupo de atividades e definição das cores

Resultado:

Mapa de apontamentos com as novas cores

3. Extras

Criação da coluna de cores no grupo de atividades:

INSERT INTO nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES, CONFIG)
VALUES('GRUPO_ATIVIDADE', 'COR', 4, 0, 1, 1, 1, 'Cor', 'Cor', 'COLOR', NULL, NULL, NULL, NULL, 'IU', '0', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);

Introdução ao Pivot Table

O Pivot Table é uma ferramenta que permite disponibilizar Massa de Dados para Análise. É usado um componente PivotTable.js, responsável pela abstração de toda a composição de gráficos e tabelas usadas, ficando essa ferramenta responsável por:

  1. Extrair a Massa de Dados;
  2. Salvar/Excluir Configurações do componente Pivot Table;
  3. Exportar Tabelas em formato MS Excel para análise externa pelo Cliente.

Pré-requesitos

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

  1. Módulo "Rotas Personalizada" habilitado e rota personalizada para "/custom/pivotTable" criada (veja como);
  2. Tabelas "nfs_pivot_view" e "nfs_pivot_save" criadas na seguinte estrutura:
CREATE TABLE `nfs_pivot_view` (
  `SEQ_DB` bigint(20) NOT NULL AUTO_INCREMENT,
  `EMPRESA` int(11) NOT NULL,
  `FILIAL` int(11) NOT NULL,
  `LOCAL` int(11) NOT NULL,
  `TITLE` varchar(100) DEFAULT NULL,
  `QUERY` text,
  `FILTERS` 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;

CREATE TABLE `nfs_pivot_save` (
  `SEQ_DB` bigint(20) NOT NULL AUTO_INCREMENT,
  `EMPRESA` int(11) NOT NULL,
  `FILIAL` int(11) NOT NULL,
  `LOCAL` int(11) NOT NULL,
  `INS_DH` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `INS_USUARIO_SEQ_DB` bigint(20) unsigned NOT NULL,
  `NOME` varchar(100) DEFAULT NULL,
  `CONFIG` varchar(5000) DEFAULT NULL,
  `PIVOT_QUERY_SEQ_DB` bigint(20) unsigned DEFAULT NULL,
  `ENABLED` int(11) DEFAULT '1',
  `DELETED` int(11) DEFAULT '0',
  `UPD_USUARIO_SEQ_DB` bigint(20) unsigned DEFAULT NULL,
  UNIQUE KEY `SEQ_DB_UNIQUE` (`SEQ_DB`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;

Configurações Iniciais

Tabela nfs_pivot_view

Possui as definições principais do ambiente, sendo:

  1. TITLE: Título do Mapa exibido no topo do módulo;
TITLE='Apontamentos Máquina'
  1. QUERY: QueryBuilder para obter a Massa de Dados;
{
  "select": [
    "apo.INI_DH as 'Data Inicial'",
    "apo.FIM_DH as 'Data Final'",
    "(coalesce(apo.INI_FIM_DIFF_SEC, 0) / 3600) as 'Tempo Apontado (h)'",
    "concat(eq.CODIGO, ' ', eq.DESCRICAO) as 'Equipamento'",
    "coalesce(cla.DESCRICAO, 'não definida') as 'Classe Operacional'",
    "ope.NOME as 'Operador'",
    "case atv.FLAG_PRODUTIVA when 1 then 'Produtiva' else 'Improdutiva' end as 'Tipo Atividade'",
    "concat(atv.CODIGO, ' - ',  atv.DESCRICAO) as 'Atividade'"
  ],
  "from": "boletim_maquina bm",
  "inner_join": [
    ["bm", "equipamento", "eq", "eq.SEQ_DB = bm.EQUIPAMENTO_SEQ_DB"],
    ["bm", "operador", "ope", "ope.SEQ_DB = bm.OPERADOR_SEQ_DB"],
    [
      "bm",
      "apontamento_maquina",
      "apo",
      "apo.SEQ_DB_DEVICE_MASTER_SEQ_DB = bm.SEQ_DB"
    ]
  ],
  "left_join": [
    ["apo", "atividade", "atv", "atv.SEQ_DB = apo.ATIVIDADE_SEQ_DB"],
    [
      "eq",
      "classe_operacional",
      "cla",
      "cla.SEQ_DB = eq.CLASSE_OPERACIONAL_SEQ_DB"
    ]
  ],
  "where": [
    "(date(apo.INI_DH) between :ini and :fim) or (date(apo.FIM_DH) between :ini and :fim)"
  ]
}
  1. FILTERS: coluna criada para futura implementação de filtros para as queries.

Tabela nfs_pivot_save

Tabela usada para salvar as configurações de determinadas visões dos usuários.

Introdução

Quadro de Trabalho Power Service foi desenvolvido objetivando substituir o Quadro de Trabalho adotado pela Prime Action Consulting, usado como padrão na visualização dos dados, permitindo visualizar, de maneira fácil e rápida, a alocação de técnicos e suas demandas de trabalho recente.

Nesse Quadro são exibidas as OS seguindo as regras:

  1. Apenas funcionários/técnicos com flag QIG_DISPLAY = 1;
  2. OS Abertas para data selecionada e dia anterior;
  3. OS em Andamento (independente do dia selecionado);
  4. OS Fechadas para data selecionada e dia anterior;
  5. OS Pausadas (independente do dia selecionado).

Pré-requesitos

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

  1. Módulo "Rotas Personalizada" habilitado e rota personalizada para "/custom/workBoard" criada (veja como);

  2. Configuração de Valores de Referência para devido posicionado dentro do Quadro, seguindo a seguinte estrutura:

    1. Campo LOV1 para tabela TIPO_OS para determinar se a atividade está sendo executada no 1:Campo ou na 2:Oficina;

    IMPORTANTE
    Há limitações na exibição da tabela, é bom ressaltar que as colunas não são dinâmicas, para o Tipo OS são aceitos apenas duas opções obrigatórias.

    Opções para o uso do workboard:

    1:Campo;2:Oficina;
    
    1. Campo LOV1 para tabela MOTIVO_PAUSA para determinar o motivo da pausa de uma atividade, sendo 1:Aguardando Aprovação Cliente, 2:Aguardando Aprovação Interna, 3:Aguardando Peça e 4:Servios de Terceiros;
    2. Parâmetro de relação DE/PARA do SEQ_DB do Status da OS, se necessário, para devida classificação, onde:
      1. OS abertas => 1;
      2. OS em Serviço/Execução => 2;
      3. OS Fechadas => 3;
      4. OS Pausadas => 4.
-- referência para execução de atividades internas / externas
INSERT INTO nfs_core_ds_tabela_campo (
  `TABELA_NOME`, `NOME`, `SEQ`, `SYS`, `SEND_XMOVA`, `GRID`, `GRID_MOBILE`, `DESCRICAO`, `DESCRICAO_RESUMIDA`,
  `TIPO`, `OBRIGATORIO`, `TAMANHO`, `INSERT_UPDATE`, `DISABLED`, `OPCOES`
) VALUES(
  'TIPO_OS', 'ATENDIMENTO', 3, 0, 1, 1, 1, 'Atendimento', 'Atendimento',
  'LOV1', 1, 1, 'IU', '0', '1:Campo;2:Oficina'
);
-- referência para motivos de pausa
INSERT INTO nfs_core_ds_tabela_campo (
  `TABELA_NOME`, `NOME`, `SEQ`, `SYS`, `SEND_XMOVA`, `GRID`, `GRID_MOBILE`, `DESCRICAO`, `DESCRICAO_RESUMIDA`,
  `TIPO`, `OBRIGATORIO`, `TAMANHO`, `INSERT_UPDATE`, `DISABLED`, `OPCOES`
) VALUES(
  'MOTIVO_PAUSA', 'KANBAN', 4, 0, 1, 1, 1, 'Kanban', 'Posição Kanban',
  'LOV1', 1, 1, 'IU', '0', '1:Aguardando Aprovação Cliente;2:Aguardando Aprovação Interna;3:Aguardando Peça;4:Servios de Terceiros'
);
-- referência de status da OS
INSERT INTO nfs_core_par_parametros (
  `EMPRESA`, `FILIAL`, `LOCAL`, `NOME`, `CONTEUDO`, `TIPO`
) VALUES(
  4, 9999, 9999, 'WORKBOARD_GRID', '1:1;2:2;3:3;4:4', 1
);

IMPORTANTE As DDL acima devem ser usadas como referência. Ao implementar em novos ambientes, favor atentar para os dados da Empresa (EMPRESA, FILIAL e LOCAL) e ID's sequênciais (SEQ_DB's) {.is-danger}

Erros

Em caso que não exiba nenhum card mesmo existindo detalhes da OS cadastrado e associado a técnico, a seguinte consulta é a que busca o detalhes por funcionário:

SELECT 
  OS_TECNICO.SEQ_DB AS OS_TECNICO_SEQ_DB, 
  OS_TECNICO.FUNCIONARIO_SEQ_DB AS SEQ_DB, 
  OS_TECNICO.OS_SEQ_DB AS OS_SEQ_DB, 
  OS_TECNICO.STATUS_OS_SEQ_DB AS OS_TECNICO_STATUS, 
  count(OST_DETALHES.SEQ_DB) TOTAL_DETALHES_OS, 
  OST_DETALHES.DATA_INICIAL AS DATA_INICIAL, 
  OST_DETALHES.DATA_FINAL AS DATA_FINAL, 
  OST_DETALHES.DIA_INTEIRO AS DIA_INTEIRO, 
  concat(
    '/g/OS/dados/', OS_TECNICO.OS_SEQ_DB, 
    '?popup=1'
  ) AS LINK, 
  OS.CODIGO AS OS_CODIGO, 
  OS.OBSERVACAO AS OS_OBSERVACAO, 
  OS.STATUS_OS_SEQ_DB AS OS_STATUS, 
  OS.LOCAL_ATENDIMENTO AS LOCAL_ATENDIMENTO, 
  TIPO_OS.ATENDIMENTO AS TIPO_OS_ATENDIMENTO, 
  concat(
    CLIENTE.CODIGO, ' ', CLIENTE.DESCRICAO
  ) AS CLIENTE, 
  0 EXIBIR_DETALHES 
FROM 
  APP_OS_TECNICO OS_TECNICO 
  inner JOIN APP_OS OS ON OS.SEQ_DB = OS_TECNICO.OS_SEQ_DB 
  left JOIN APP_OS_TECNICO_DETALHE OST_DETALHES ON OST_DETALHES.OS_TECNICO_SEQ_DB = OS_TECNICO.SEQ_DB 
  AND OST_DETALHES.ATIVO = 1 
  AND OST_DETALHES.DELETED = 0 
  inner JOIN APP_CLIENTE CLIENTE ON CLIENTE.SEQ_DB = OS.CLIENTE_SEQ_DB 
  left JOIN APP_TIPO_OS TIPO_OS ON TIPO_OS.SEQ_DB = OS.TIPO_OS_SEQ_DB 
WHERE 
  (1 = 1) 
  AND (
    OS_TECNICO.EMPRESA IN (4, 9999)
  ) 
  AND (
    OS_TECNICO.FILIAL IN (1, 9999)
  ) 
  AND (
    OS_TECNICO.LOCAL IN (1, 9999)
  ) 
  AND (OS_TECNICO.ATIVO = 1) 
  AND (OS_TECNICO.DELETED = 0) 
  AND (
    OS_TECNICO.FUNCIONARIO_SEQ_DB = 15
  ) 
  AND (
    OS_TECNICO.STATUS_OS_SEQ_DB in (2, 4) 
    or (
      OS_TECNICO.STATUS_OS_SEQ_DB in (1) 
      and (
        OST_DETALHES.DATA_INICIAL between '2023-09-01 00:00:00' 
        AND '2023-10-07 23:59:59' 
        and OST_DETALHES.DATA_FINAL between '2023-09-01 00:00:00' 
        AND '2023-10-07 23:59:59'
      )
    ) 
    or (
      OS_TECNICO.STATUS_OS_SEQ_DB in (3) 
      and OS_TECNICO.UPD_DH between '2023-10-02 00:00:00' 
      AND '2023-10-07 23:59:59'
    )
  ) 
GROUP BY 
  OS.CODIGO, 
  OS_TECNICO.SEQ_DB;
 

Implementação de Filtros no Quadro de Trabalho

Este guia explica detalhadamente como implementar filtros no Quadro de Trabalho por meio de entry points. Os filtros permitem restringir os dados exibidos com base em condições específicas. A seguir, são descritas as etapas para criar filtros, as opções disponíveis e as regras para configuração.


Inserção de Entry Point no Banco de Dados

Para implementar um filtro, insira um registro na tabela nfs_entry_point com a seguinte estrutura:

INSERT INTO nfs_homol_cortezengenharia_construmobil_smartos.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 
('WORK_BOARD', 'filter', NULL, NULL, 9999, 9999, 9999, '', NULL, '<JSON_FILTRO>', 1, 12, 1, 'JSON', NULL, NULL, '2024-11-11 12:50:14', NULL, 'nfs.joao.beleno@177.192.16.97');

Campos Importantes

  • FILE_OR_DOMAIN: Deve ser "WORK_BOARD".
  • ACTION: Deve ser "filter".
  • EMPRESA, FILIAL, LOCAL: Utilize 9999 para filtros globais ou os códigos específicos para restringir por empresa, filial ou local.
  • ATIVO: Deve ser sempre 1 (ativo).
  • CODETYPE: Deve ser "JSON".
  • CODE: Contém o JSON detalhado dos filtros que serão aplicados (veja abaixo).

Estrutura do JSON de Filtros

O campo CODE no registro contém o JSON que define os filtros. A chave do JSON deve corresponder ao nome do parâmetro utilizado na condição SQL (ex.: :NUMERO_OS).

Exemplo de JSON básico:

{
  "NUMERO_OS": {
    "type": "Text",
    "options": {
      "label": "Número OS",
      "multiple": true,
      "required": false,
      "language": "pt-BR",
      "sql-condition": "OS.NUMERO_OS LIKE :NUMERO_OS",
      "sql-table": "os"
    }
  }
}

Tipos de Filtro Disponíveis

  1. Text: Campo de texto simples.

    • Exemplo:
      {
        "NUMERO_OS": {
          "type": "Text",
          "options": {
            "label": "Número OS",
            "sql-condition": "OS.NUMERO_OS LIKE :NUMERO_OS",
            "sql-table": "os"
          }
        }
      }
      
  2. Table: Campo com autocomplete, permitindo selecionar valores de outra tabela a chave do json deve ser o nome da tabela exemplo OBRA

    • Exemplo:
      {
        "obra": {
          "type": "Table",
          "options": {
            "label": "Obra",
            "sql-condition": "EXISTS (SELECT 1 FROM APP_OBRA_RENTAL WHERE OBRA_SEQ_DB = :obra AND SEQ_DB = OS.OBRA_RENTAL_SEQ_DB)",
            "sql-table": "os"
          }
        }
      }
      
  3. Choice: Campo de seleção com opções predefinidas.

    • Exemplo:
      {
        "OS_ATIVO": {
          "type": "Choice",
          "options": {
            "label": "Status OS",
            "choices": {
              "1": "ATIVO",
              "0": "INATIVO"
            },
            "sql-condition": "OS.ATIVO = :OS_ATIVO",
            "sql-table": "os"
          }
        }
      }
      
  4. Date: Campo para seleção de datas.

    • Exemplo:
      {
        "DATA_INICIAL": {
          "type": "Date",
          "options": {
            "label": "Data Inicial",
            "sql-condition": "OS_TECNICO.DATA_INICIAL >= :DATA_INICIAL",
            "sql-table": "os"
          }
        }
      }
      

Regras para Configuração do JSON

  • Chave do JSON: Deve corresponder ao nome do parâmetro na condição SQL (ex.: :obra, :NUMERO_OS).
  • type: Define o tipo do filtro (Text, Table, Choice, Date).
  • sql-table: Define a tabela na qual o filtro será aplicado.
    • Valores disponíveis:
      • os: Filtros aplicados diretamente às OS.
      • funcionario: Filtros aplicados aos funcionários.
  • sql-condition: Define a condição SQL para filtrar os dados. Pode ser uma condição básica ou avançada utilizando EXISTS.

Tabelas Disponíveis para Filtros

Filtros Aplicados a OS

  • Tabelas disponíveis:
    • OS_TECNICO
    • OST_DETALHES
    • OS
    • TIPO_OS
  • Exemplo de sql-condition básico:
    "sql-condition": "OS_TECNICO.DATA_INICIAL >= :DATA_INICIAL"
    
  • Exemplo de sql-condition avançado caso nao existam as tabelas nescessarias para a consulta pode ser usado o exists incluindo jois a consulta, deve ser usado no nome real da tabela:
    "sql-condition": "EXISTS (SELECT 1 FROM APP_OBRA_RENTAL WHERE OBRA_SEQ_DB = :obra AND SEQ_DB = OS.OBRA_RENTAL_SEQ_DB)"
    

Filtros Aplicados a Funcionários

  • Tabela disponível:
    • fun (abreviação para funcionário)
  • Exemplo de sql-condition básico:
    "sql-condition": "fun.NOME = :NOME"
    

Exemplo Completo de JSON com Múltiplos Filtros

{
  "obra": {
    "type": "Table",
    "options": {
      "label": "Obra",
      "multiple": true,
      "required": false,
      "language": "pt-BR",
      "sql-condition": "EXISTS (SELECT 1 FROM APP_OBRA_RENTAL WHERE OBRA_SEQ_DB = :obra AND SEQ_DB = OS.OBRA_RENTAL_SEQ_DB)",
      "sql-table": "os"
    }
  },
  "NUMERO_OS": {
    "type": "Text",
    "options": {
      "label": "Número OS",
      "multiple": true,
      "required": false,
      "language": "pt-BR",
      "sql-condition": "OS.NUMERO_OS LIKE :NUMERO_OS",
      "sql-table": "os"
    }
  },
  "OS_ATIVO": {
    "type": "Choice",
    "options": {
      "label": "Status OS",
      "multiple": true,
      "required": false,
      "language": "pt-BR",
      "sql-condition": "OS.ATIVO = :OS_ATIVO",
      "choices": {
        "1": "ATIVO",
        "0": "INATIVO"
      },
      "sql-table": "os"
    }
  }
}

Com essas informações detalhadas, você pode configurar filtros específicos, personalizáveis e funcionais no Quadro de Trabalho.

GIS

Sobre

A camada de objetos GIS é formada por áreas, pontos e linhas exibidas no mapa, sendo estes elementos criados através de informações contidas em um GeoJSON.

Tem como função, através de algumas figuras, delimitar áreas e marcar pontos de interesse nos mapas GIS e Marte, possibilitando a verificação de algumas caracteriscas de veiculos,equipamentos, entre outros objetos que estão dentro dessas áreas, como:

  • Se algum veículo entrou ou saiu de determinada área;
  • Se algum veículo esta excedendo a velocidade máxima permitida na área;

Cada elemento que compõe esta camada é considerado um Objeto GIS e este objeto deverá pertencer a um grupo de objetos, o qual é denominado Grupo GIS.

Os mapas são ferramentas que permitem visualizar informações geográficas usando dados de tabelas específicas, possibilitando exibir a localização de objetos e apresentar informações relevantes. Eles podem ser configurados para exibir dados de forma interativa, como rotas, áreas geográficas ou uma representação visual das últimas localizações de objetos específicos.

Inicialmente, os mapas podem ser gerados com base em informações predefinidas, de acordo com a entidade para a qual foram configurados. No entanto, também é possível criar mapas de forma mais personalizada por meio dos Entry Points.

Enviando dados para o mobile

Trabalhando com geofence

OBS.: o campo coordinates, contém os dados de longitude e latitude, nessa ordem. No mobile, existe a função de detecção de área (geofence), esta funcionalidade espera receber primeiro a latitude e depois a longitude. Portanto, no entryPoint de envio de dados é necessário gravar a localização de forma invertida, como no exemplo abaixo:

$result = NFSQueryBuilder::select([
	"t.SEQ_DB id",
	"t.SEQ_DB area",
	"t.DESCRICAO name",
	"t.DESCRICAO description",
	"'ENTER' notify",
	"g.GEOJSON",
	"CONCAT('Entrou na área: ',t.DESCRICAO) enterText",
	"CONCAT('Saiu da área: ',t.DESCRICAO) exitText",
])
->from('CLIENTE','t')
->innerJoin('t', 'GIS_OBJECT', 'g', 'g.SEQ_DB = t.GIS_OBJECT_SEQ_DB')
->orderBy('t.SEQ_DB', 'ASC')
->get();
$_recordsToMobileXMOVA = $result;

foreach($result as $key => $row) {
	$coord = '';
	$obj = json_decode($row['GEOJSON']);
	$objArray = $obj->{'geometry'}->{'coordinates'}[0];
	foreach($objArray as $keyA => $rowA) {		
		$coord = $coord.$rowA[1].',';
		$coord = $coord.$rowA[0].';';
		
	}	
	$_recordsToMobileXMOVA[$key]['coordinates'] = $coord;
	unset($_recordsToMobileXMOVA[$key]['GEOJSON']);
}

Trabalhando com GIS (após a versão 3.3.2)

OBS.: o campo geometry, contém os dados de latitude e longitude, e deve ser enviado como no exemplo:

[geometry] => POLYGON((-17.058126 -46.188592,-17.064034 -46.189278,-17.064814 -46.182672,-17.067767 -46.182887,-17.067932 -46.180227,-17.067849 -46.17598,-17.059726 -46.175079,-17.058126 -46.188592))

Portanto, no entryPoint de envio de dados é necessário utilizar as funções:

  • ST_GeomFromGeoJson: para transformar o conteúdo das coordenadas em objeto
  • ST_AsText: para transformar o objeto em texto

Veja mais em: postgis.net {.is-info}

$_recordsToMobileXMOVA = NFSQueryBuilder::select([
	"t.SEQ_DB id",
	"t.DESCRICAO name",
	"ST_AsText(ST_GeomFromGeoJson(g.GEOJSON)) geometry",
])
->from('CLIENTE','t')
->innerJoin('t', 'GIS_OBJECT', 'g', 'g.SEQ_DB = t.GIS_OBJECT_SEQ_DB')
->orderBy('t.SEQ_DB', 'ASC')
->get();

MOBILE

Trabalhando com geofence

Após a versão 3.3.2, usar exemplo do fluxo 100123104 {.is-warning}

Ver exemplo no fluxo de homologação 100123102

APP

Configurar o atributo geofence

App appCode=Teste
	...

	location
		//Habilita a funcionalidade de geofencing
		geofence

MODEL

Adicionar entidades: _GEOFENCE_LOCATION e _GEOFENCE_PLACE

//------------------------GEOFENCE INICIO--------------------------
_GEOFENCE_LOCATION sync=out
	id inc
	place _GEOFENCE_PLACE inlineData
	location Location inlineData
	entered boolean
	events
		OnGeolocationTransitionEnter
			_area = :place.id
			actionbarsubtitle place.description
		OnGeolocationTransitionExit
			_area = null
			actionbarsubtitle @

_GEOFENCE_PLACE sync=in emptyVerify=false
	id int
	name String
	description String inUpper
	coordinates String
	notify String
	enterText String
	exitText String
	area long
	server name=EQP_Geofence_Place
//------------------------GEOFENCE FIM--------------------------

Trabalhando com GIS

Nova funcionalidade GIS + geometry

Ver exemplo no fluxo de homologação 100123104

APP

Configurar o atributo geofence

App appCode=Teste
	...
	location

MODEL

Adicionar entidade com um campo do tipo Geometry

//------------------------GEOFENCE INICIO--------------------------
Place sync=in emptyVerify=false
	id int
	name String
	geometry Geometry
	server name=EQP_Geometry
//------------------------GEOFENCE FIM--------------------------

Veja mais em: documentação do xMova

Configuração

Para habilitarmos esta funcionalidade será necessario:

  1. Adicionar o parâmetro GIS_ENABLED na tabela nfs_core_par_parametros, exemplo:
INSERT INTO nfs_core_par_parametros(EMPRESA, FILIAL, `LOCAL`, NOME, CONTEUDO, TIPO)
VALUES(1, 9999, 9999, 'GIS_ENABLED', '1', 1);

Caso o ambiente possua o parâmetro MARTE_ENABLED = 1, a camada de objetos GIS, ficará habilitada por padrão {.is-tip}

  1. Logar no ambiente com algum usuario que possua acesso ao admin_console({host}/admin);
  2. Executar o DS/DDL create; com isso, as seguintes tabelas serão criadas:
  • gis_group: Grupos de objetos GIS;
  • gis_object: Objetos GIS.

Os SEQ_DB 10020 e 10099, são reservados para os menus Cadastro("Mapas e Localizações") e Grupos GIS(Mapa) respectivamente. {.is-warning}

  1. Executar o DS Reload e deslogar o usuario;
  2. Ao logar novamente no sistema, no menu de "Mapas e Localizações" deverá aparecer o submenu de cadastros, o qual contêm os CRUDS para os Grupos e Objetos GIS, o mapa "Grupos GIS"* também deverá aparecer no menu; exemplo:

mapas_menu.png

Grupos GIS

Representa os agrupamentos de Objetos GIS, possuindo as seguintes caracteristicas:

  • ID: Identificação única do grupo;
  • Descrição: Nome atribuído ao grupo;
  • Cor das Áreas: Este atributo é opcional, caso seja selecionada uma cor, todos os objetos GIS pertencentes a este grupo, serão exibidos nos mapas na cor escolhida(exceto marker(pontos));
  • Tag: Funciona como palavra-chave, sendo utilizado por exemplo no envio de Alerts;
  • Detectar Entrada: Detecta a entrada de elementos nas áreas;
  • Detectar Entrada: Detecta a saída de elementos nas áreas;
  • Velocidade mínima: Atribui uma velocidade minima para as áreas;
  • Velocidade máxima: Atribui uma velocidade máxima para as áreas;

caminho: {host}/t/gis_group\

grupos_gis.png

Objetos GIS

Representação dos elementos GIS, possui as seguintes caracteristicas:

  • ID: Identificação unica do objeto;
  • Descrição: Nome atribuido ao objeto;
  • Grupo: Grupo GIS ao qual o objeto pertence;
  • Tag: Funciona como palavra-chave, sendo utilizado por exemplo no envio de Alerts;
  • Tipo: Define o tipo(forma) da area, podendo ser: Ponto, Área/Poligono ou Linha;
  • Detectar Entrada: Detecta a entrada de elementos na área especifica;
  • Detectar Entrada: Detecta a saida de elementos na área especifica;
  • Velocidade mínima: Atribui uma velocidade minima para a área especifica;
  • Velocidade máxima: Atribui uma velocidade máxima para a área especifica;
  • GeoJSON: Ao clicar no botão Demarcar/Editar área e criar alguma área, aqui ficará o GEOJson da área; GEOJsons de terceiros tambem poderão ser inseridos nesta área geojson.io.
  • Exibir automaticamente nos mapas: Ao marcar esta opção, o objeto GIS será renderizado automaticamente ao abrir algum mapa GIS ou Marte.

*caminho: {host}/t/gis_object*

object_gis.png

Demarcar/Editar Objetos GIS

Ao acessar o CRUD de criação ou edição de um objeto GIS, podemos clicar no botão "Demarcar/Editar área", com isso um modal com um mapa contendo os objetos cadastrados e ferramentas para desenho ou edição do objeto atual, irá aparecer:

gis_modal.png

Caso você precise apenas visualizar o mapa, sem criar ou editar algum objeto GIS, poderá acessa-lo pelo mapa "Grupos GIS" em "Mapas e Localizações. {.is-tip}

Criando uma novo objeto GIS

  1. Na tela de criação/edição de um objeto GIS, devemos selecionar o seu tipo:

tipos.png

  1. Clicar no botão "Demarcar/Editar área";
  2. Selecionar a forma na toolBar que fica no canto superior esquerdo;
  3. Desenhar a forma;
  4. Ao concluir o desenho, irá aparecer a mensagem pedindo para fechar o modal e salvar as alterações no CRUD do objeto.

add_area.gif

Editando um objeto GIS existente

  1. Clicar no botão "Demarcar/Editar área";
  2. Selecionaro botão de edição ou a forma desejada(caso queira que a nova forma substitua a anterior) na toolBar que fica no canto superior esquerdo;
  3. Desenhe ou edite a forma;
  4. Ao concluir, irá aparecer a mensagem pedindo para fechar o modal e salvar as alterações no CRUD do objeto.

ezgif.com-gif-maker.gif

Integração com mapas GIS

Caso o ambiente possua algum objeto GIS cadastrado, o grupo correspondente a este objeto, irá aparecer como uma camada disponivel em mapas GIS.

Ao clicar em algum grupo, os objetos GIS pertencentes a este grupo, serão exibidos no mapa.

gis_integration.png

Objetos GIS que possuam a opção "Exibir automaticamente nos mapas" habilitada, vão ser exibidos assim que o mapa carregar, sendo o grupo correspondente a este objeto, adicionado como uma camada no mapa. Esta opção pode ser habilitada, ao criar ou editar um objeto GIS, nas telas de CRUD mostradas em: Objetos GIS. {.is-tip}

Integração com mapas MARTE

Caso o ambiente possua algum objeto GIS cadastrado, a aba Grupos GIS estará disponivel no menu do mapa e o grupo correspondente a esta objeto, irá aparecer como uma camada disponivel na aba de Grupos GIS.

Ao clicar em algum grupo, os objetos GIS pertencentes a este grupo, serão exibidos no mapa.

marte_integration.png

Objetos GIS que possuam a opção "Exibir automaticamente nos mapas" habilitada, vão ser exibidos assim que o mapa Marte carregar, sendo o grupo correspondente a este objeto, adicionado como uma camada no mapa. Esta opção pode ser habilitada, ao criar ou editar um objeto GIS, nas telas de CRUD mostradas em: Objetos GIS. {.is-tipp}

Configuração Alerts

TODO: Adicionar docs de configuração GEOFENCE, ENTRY_POINTS E AGENDAMENTO

Atualmente é possivel configurar Alerts para entradas e saídas em áreas da camada GIS, para isso devemos criar um topico no alerts, com um dos seguintes padrões de NOME:

  • GEOFENCEGROUP_ENTRY{TAG DO GRUPO}: Envia alerta de entrada nas áreas do grupo ;
  • GEOFENCEGROUP_ENTRY{TAG DO GRUPO}: Envia alerta de saída nas áreas do grupo;
  • GEOFENCEOBJECT_ENTRY{TAG DO OBJECT}: Envia alerta de entrada na área específica;
  • GEOFENCEOBJECT_ENTRY{TAG DO OBJECT}: Envia alerta de saída na área específica.

Após a criação do tópico, será necessario atribui-lo a um grupo ou usuario do Alerts.

Devido ao grande número de objetos GIS que podem ser cadastrados, a utilização desta opção em objetos GIS, deve ser feita apenas em casos de extrema necessidade, caso contrario, utilizar a detecção de acordo com o grupo deste objeto. {.is-danger}

Para que o Alerta sejá efetivamente enviado, verificar se a opção de Detectar entrada e/ou saída foi habilitada para o grupo ou objeto em questão. {.is-warning}

Camada de estados e municipios

É possível registrarmos em qual estado/município um apontamento foi realizado, para isso temos que fazer algumas configurações.

Definindo os grupos

Adicionar os grupos padrões para as regiões na tabela app_gis_group:

-- Inserir grupos padrões para regiões
INSERT INTO app_gis_group(SEQ_DB, EMPRESA, FILIAL, `LOCAL`,ATIVO, DELETED, DESCRICAO, COR, TAG, DETECT_ENTRY, DETECT_EXIT, GIS_GROUP_MASTER_SEQ_DB, INS_USUARIO_SEQ_DB, ID)
VALUES(100, 9999, 9999, 9999, 1, 0, 'País', NULL, '#PAIS', 0, 0, null, 1, 100);
INSERT INTO app_gis_group(SEQ_DB,EMPRESA, FILIAL, `LOCAL`,ATIVO, DELETED, DESCRICAO, COR, TAG, DETECT_ENTRY, DETECT_EXIT, GIS_GROUP_MASTER_SEQ_DB, INS_USUARIO_SEQ_DB, ID)
VALUES(101, 9999, 9999, 9999, 1, 0, 'Estado', NULL, '#ESTADO', 0, 0, 100, 1, 101);
INSERT INTO app_gis_group(SEQ_DB, EMPRESA, FILIAL, `LOCAL`, ATIVO, DELETED, DESCRICAO, COR, TAG, DETECT_ENTRY, DETECT_EXIT, GIS_GROUP_MASTER_SEQ_DB, INS_USUARIO_SEQ_DB, ID)
VALUES(102, 9999, 9999, 9999, 1, 0, 'Município', NULL, '#MUNICIPIO', 0, 0, 101, 1, 102);

Feito isso será necessário gerar os arquivos sql que contem os estados e municípios para serem inseridos no banco pelo core: cd tests e depois php importacao_geojson_regioes.php ou usar os arquivos sql enconstrados em https://drive.google.com/drive/folders/1VAVRLwQNSnEcrZDn32482ggmNZxm1mCb?usp=sharing(valido apenas para estados e municipios brasileiros). {.is-warning}

Relacionando os estados aos municípios

Após inserir os objetos na tabela de objetos GIS(app_gis_object), devemos atribuir os estados de cada município:

-- Adiciona o estado de cada municipio
create temporary table states select * from app_gis_object ngo where GROUP_SEQ_DB = 101;
update app_gis_object obj set
obj.GIS_OBJECT_MASTER_SEQ_DB = (
	select stt.SEQ_DB from states stt
	where ST_Intersects(ST_GeomFromText(ST_AsText(ST_Centroid(ST_GeomFromText(ST_AsText(obj.GEOM_DATA)))),4326), stt.GEOM_DATA)
) where obj.GROUP_SEQ_DB = 102;

Preparando a tabela de apontamento

Alguns campos deverão ser adicionados para que seja registrado os o estado e município no apontamento:

-- Atualizações tabela de apontamento
INSERT INTO nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('APONTAMENTO_MAQUINA', 'GIS_OBJECT_CITY_SEQ_DB', 0, 1, 1, 1, 'Cidade', 'Cidade', 'BIGINT', NULL, NULL, NULL, NULL, 'IU', '0', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('APONTAMENTO_MAQUINA', 'GIS_OBJECT_STATE_SEQ_DB', 0, 1, 1, 1, 'Estado', 'Estado', 'BIGINT', NULL, NULL, NULL, NULL, 'IU', '0', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('APONTAMENTO_MAQUINA', 'GIS_ST', 1, 1, 1, 1, 'Status processamento GIS', 'processamento GIS', 'TINT', NULL, NULL, NULL, NULL, 'IU', '0', NULL, NULL, NULL, NULL, '0', NULL, NULL, NULL, NULL);

-- Depois de executar o create DS/DDL
alter table app_apontamento_maquina
add constraint app_apt_city_fk foreign key (GIS_OBJECT_CITY_SEQ_DB) references app_gis_object(SEQ_DB),
add constraint app_apt_state_fk foreign key (GIS_OBJECT_STATE_SEQ_DB) references app_gis_object(SEQ_DB);

Criando o agendamento(scheduler)

Adicionaremos uma agendamento que irá executar a cada 10(dez) minutos,o qual ira utilizar o método services\GisService::setAptGisUfRegion($nomeTabelaApontamento), o qual irá processar os apontamentos:

INSERT INTO nfs_core_scheduler
(NAME, DESCRIPTION, CODE_ENTRY_POINT, EMPRESA, FILIAL, `LOCAL`, ATIVO, CRON_EXPRESSION, LAST_EXECUTED, EXECUTED)
VALUES( 'update_aptgis_state_n_city', 'Worker para update dos estados e municipios na tabela de apontamentos', 'services\\GisService::setAptGisUfRegion('apontamento_maquina');
', 1, 1, 1, 1, '*/10 * * * *', '2022-03-23 11:58:06', 1);

Processamento GIS(GIS_ST):

  • Pega os registro com GIS_ST=0, POSICAO_PLAT e POSICAO_PLON não nuls e gera o POSICAO_GEOM, altera o GIS_ST para 1;
  • Define os municípios dos registros com GIS_ST = 1, altera o GIS_ST para 2;
  • Define os estados de registro com o município já definido, altera o GIS_ST para 3.

Camadas WMS, KMZ, KML

Podemos adicionar cadas externas nos nossos mapas Marte e mapas GIS através de entry points "MAPS-LAYERS-EXTERNAL"

Configuração

Criar um entryPoint com o FILE_OR_DOMAIN = MAPS-LAYERS-EXTERNAL

O Conteúdo dele deve ser, como exemplo:


$externalLayers[] = ["type" => "kml",
						"Name" => "Teste KML"
						,"Url" => "/assets/etc/Teste_KML.kml"];

$externalLayers[] = ["type" => "kmz",
						"Name" => "Simova"
						,"Url" => "/assets/etc/Simova.kmz"];

return $externalLayers;

Exemplo de chamada ao portal WMS com api esri


$externalLayers[] = ["Name" => "UPs-Silv-MS-2"
				,"Url" => "https://portalgis.suzano.com.br/server/rest/services/Desenvolvimento/PLANTIO_MS_T1/MapServer/"
				,"Layers" => ['UP']];
return $externalLayers;

Exemplo com conteúdo KML dentro do entryPoint (ou pode vir de um cadastro, api, etc):


$content = "&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;kml xmlns=&quot;http://www.opengis.net/kml/2.2&quot; xmlns:gx=&quot;http://www.google.com/kml/ext/2.2&quot; xmlns:kml=&quot;http://www.opengis.net/kml/2.2&quot; xmlns:atom=&quot;http://www.w3.org/2005/Atom&quot;&gt;
&lt;Document&gt;
	&lt;name&gt;Teste KML.kml&lt;/name&gt;
	&lt;StyleMap id=&quot;m_ylw-pushpin&quot;&gt;
		&lt;Pair&gt;
			&lt;key&gt;normal&lt;/key&gt;
			&lt;styleUrl&gt;#s_ylw-pushpin&lt;/styleUrl&gt;
		&lt;/Pair&gt;
		&lt;Pair&gt;
			&lt;key&gt;highlight&lt;/key&gt;
			&lt;styleUrl&gt;#s_ylw-pushpin_hl&lt;/styleUrl&gt;
		&lt;/Pair&gt;
	&lt;/StyleMap&gt;
	&lt;Style id=&quot;s_ylw-pushpin_hl&quot;&gt;
		&lt;IconStyle&gt;
			&lt;scale&gt;1.3&lt;/scale&gt;
			&lt;Icon&gt;
				&lt;href&gt;http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png&lt;/href&gt;
			&lt;/Icon&gt;
			&lt;hotSpot x=&quot;20&quot; y=&quot;2&quot; xunits=&quot;pixels&quot; yunits=&quot;pixels&quot;/&gt;
		&lt;/IconStyle&gt;
		&lt;LineStyle&gt;
			&lt;color&gt;ff2c5dfd&lt;/color&gt;
			&lt;width&gt;2.9&lt;/width&gt;
		&lt;/LineStyle&gt;
		&lt;PolyStyle&gt;
			&lt;color&gt;807899f7&lt;/color&gt;
			&lt;colorMode&gt;random&lt;/colorMode&gt;
		&lt;/PolyStyle&gt;
	&lt;/Style&gt;
	&lt;Style id=&quot;s_ylw-pushpin&quot;&gt;
		&lt;IconStyle&gt;
			&lt;scale&gt;1.1&lt;/scale&gt;
			&lt;Icon&gt;
				&lt;href&gt;http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png&lt;/href&gt;
			&lt;/Icon&gt;
			&lt;hotSpot x=&quot;20&quot; y=&quot;2&quot; xunits=&quot;pixels&quot; yunits=&quot;pixels&quot;/&gt;
		&lt;/IconStyle&gt;
		&lt;LineStyle&gt;
			&lt;color&gt;ff2c5dfd&lt;/color&gt;
			&lt;width&gt;2.9&lt;/width&gt;
		&lt;/LineStyle&gt;
		&lt;PolyStyle&gt;
			&lt;color&gt;807899f7&lt;/color&gt;
			&lt;colorMode&gt;random&lt;/colorMode&gt;
		&lt;/PolyStyle&gt;
	&lt;/Style&gt;
	&lt;Placemark&gt;
		&lt;name&gt;Teste KML&lt;/name&gt;
		&lt;styleUrl&gt;#m_ylw-pushpin&lt;/styleUrl&gt;
		&lt;Polygon&gt;
			&lt;tessellate&gt;1&lt;/tessellate&gt;
			&lt;outerBoundaryIs&gt;
				&lt;LinearRing&gt;
					&lt;coordinates&gt;
						-49.98030955391177,-20.11809369236751,0 -50.24076996027695,-20.18153182836599,0 -50.54669615017947,-20.5268783615084,0 -50.71789044379601,-21.52543068234906,0 -50.39520524821304,-21.71558335465463,0 -49.39739572821397,-22.27000319158382,0 -48.51880265648861,-21.88970888360517,0 -48.24752126546238,-21.56225003094103,0 -48.92921058980514,-20.99788546319585,0 -48.24565122044976,-20.87519670753311,0 -48.18302886940717,-20.65207767618273,0 -48.21348486326866,-20.44782877579947,0 -48.41990936243072,-20.41806673823675,0 -48.64766134424571,-20.55323262129171,0 -49.09207205295417,-20.53042843073673,0 -49.39189442510747,-20.47850528508822,0 -49.58803631627013,-20.37846921394921,0 -49.98030955391177,-20.11809369236751,0
					&lt;/coordinates&gt;
				&lt;/LinearRing&gt;
			&lt;/outerBoundaryIs&gt;
		&lt;/Polygon&gt;
	&lt;/Placemark&gt;
&lt;/Document&gt;
&lt;/kml&gt;";


$externalLayers[] = ["type" => "kmlcontent",
						"Name" => "Simova content"
						,"content" => $content];

return $externalLayers;

Foto

Foi criado um metodo chamado showImage() utilizando a classe nfsUi que prove um componente html capaz de exibir uma imagem baseada na Tabela, Coluna e SEQ_DB da mesma.

Sintaxe básica de implementação


\html\nfsUi::showImage('tabela','coluna','seq_db')

Exemplo de uso em um entrypoint


	array_push($_aLists['apontamentos_semana'],
		array('date_pos'=>$dataApt,
			'date_pos_str'=>$dataApt,
			'plat'=>$latApt,
			'plon'=>$lonApt,
			'popup'=> $objTorre['DESCRICAO'],
			'label'=> $objTorre['DESCRICAO'].(empty($seqApt) ? $seqApt : ' *****'),
			'popup_json'=>json_encode(array(
				'COORDENADAS'=>$objTorre['POSICAO_PLAT'].' '.$objTorre['POSICAO_PLON'],
				'DESCRICAO'=>$objTorre['DESCRICAO'],
				'ATIVIDADE REALIZADA'=>$descAtividade,
				'ENCARREGADO'=>$descEncarregado,
				'DATA'=>$dataApt,
				)
			),
			'popup_end'=>\html\nfsUi::showImage('APONTAMENTO_MO_PRODUCAO','FOTO_THUMBNAIL',$seqDBMoProd)."<div class=text-center'>".\html\nfsUi::buttonCrudSeq('ENERGY_TOWER',$objTorre['SEQ_DB'],$objTorre['DESCRICAO']).' '.\html\nfsUi::buttonCrudFK('APONTAMENTO_MO','SEQ_DB',$seqDB,'','Abrir Apontamentos')."</div>",'icon'=>$towerType[$tipo]['ICON']
		)
	);

Exemplo de entrypoint completo


$_jsonReturn = ['status' => [], 'config' => [], 'layers' => [], 'lists' => []];

 $_config['mapName'] = 'Apontamentos da Semana';

 $user = \core\xDS::getUser();
 $empresa = $user->EMPRESA_ATUAL;
 $local = $user->LOCAL_ATUAL;
 $filial = $user->FILIAL_ATUAL;
 $_aLists['apontamentos_semana'] = [];

 $towerType = \core\TabelaBO::getAllDataToArrayKey("ENERGY_TOWER_TYPE","SEQ_DB");
 $towers = \core\TabelaBO::getAllDataToArrayKey("ENERGY_TOWER","SEQ_DB");

 $sql = "SELECT mo.SEQ_DB, mo.SEQ_DB_DEVICE, mo.INI_DH, concat(f.CRACHA,' - ',f.NOME) as FUNCIONARIO, mo.TORRE_SEQ_DB, a.DESCRICAO, mo.POSICAO_PLAT, mo.POSICAO_PLON, mop.SEQ_DB as SEQ_DB_MO_PROD
 	FROM app_apontamento_mo mo
 	inner join app_atividade a ON mo.ATIVIDADE_SEQ_DB = a.SEQ_DB
 	inner join app_funcionario f on mo.FUNCIONARIO_SEQ_DB = f.SEQ_DB
    inner join app_apontamento_mo_producao mop on mo.SEQ_DB_DEVICE_MASTER_SEQ_DB = mop.SEQ_DB_DEVICE_MASTER_SEQ_DB and mo.seq_db_device_relacional = mop.seq_db_device_relacional
 	where mo.empresa = {$empresa} and mo.filial = {$filial} and mo.local = {$local}
 	and mo.INI_DH between DATE_SUB(CURDATE(),INTERVAL 30 day) and CURDATE()
 	order by INI_DH DESC";

 $aptos = \core\ConexaoDB::query($sql, $params = []);

 foreach($aptos as $apt){

 	$seqDB = $apt['SEQ_DB'];
    $seqDBMoProd = $apt['SEQ_DB_MO_PROD'];
 	$torreSeqDb = $apt['TORRE_SEQ_DB'];
 	$seqApt = $apt['SEQ_DB_DEVICE'];
 	$dataApt = $apt['INI_DH'];
 	$descAtividade = $apt['DESCRICAO'];
 	$descEncarregado = $apt['FUNCIONARIO'];
 	$latApt = $apt['POSICAO_PLAT'];
 	$lonApt = $apt['POSICAO_PLON'];

 	if(!empty($latApt) and !empty($lonApt) and !empty($torreSeqDb)){

 		$objTorre = $towers[$torreSeqDb] ?? null;

 		if (empty($objTorre)) {
 			continue;
 		}

 // 			$sqlTorre = "select et.SEQ_DB as SEQ_DB, et.NOME, et.CODIGO, et.DESCRICAO, et.POSICAO_PLAT, et.POSICAO_PLON, et.POSICAO_DH, et.UPD_DH, et.ENERGY_TOWER_TYPE_SEQ_DB
 // 			from app_energy_tower et where et.ATIVO = 1 and et.DELETED = 0 and (et.POSICAO_PLAT is not null and et.POSICAO_PLON is not null)
 // 			and et.empresa = {$empresa} and et.filial = {$filial} and et.local = {$local} and et.SEQ_DB = {$torre}";
 // 			$torresMO = \core\ConexaoDB::query($sqlTorre, $params = []);


 		$tipo = $objTorre['ENERGY_TOWER_TYPE_SEQ_DB'];

 		array_push($_aLists['apontamentos_semana'],
 			array('date_pos'=>$dataApt,
 				'date_pos_str'=>$dataApt,
 				'plat'=>$latApt,
 				'plon'=>$lonApt,
 				'popup'=> $objTorre['DESCRICAO'],
 				'label'=> $objTorre['DESCRICAO'].(empty($seqApt) ? $seqApt : ' *****'),
 				'popup_json'=>json_encode(array(
 					'COORDENADAS'=>$objTorre['POSICAO_PLAT'].' '.$objTorre['POSICAO_PLON'],
 					'DESCRICAO'=>$objTorre['DESCRICAO'],
 					'ATIVIDADE REALIZADA'=>$descAtividade,
 					'ENCARREGADO'=>$descEncarregado,
 					'DATA'=>$dataApt,
 					)
 				),
 				'popup_end'=>\html\nfsUi::showImage('APONTAMENTO_MO_PRODUCAO','FOTO_THUMBNAIL',$seqDBMoProd)."<div class=text-center'>".\html\nfsUi::buttonCrudSeq('ENERGY_TOWER',$objTorre['SEQ_DB'],$objTorre['DESCRICAO']).' '.\html\nfsUi::buttonCrudFK('APONTAMENTO_MO','SEQ_DB',$seqDB,'','Abrir Apontamentos')."</div>",'icon'=>$towerType[$tipo]['ICON']
 			)
 		);
 	}
 }

 $_config['apontamentos_semana']['showDescricao'] = true;
 array_push($_jsonReturn['layers'], array('name' => 'Apontamentos-da-Semana','list' => 'apontamentos_semana','mtype' => 'dot'));

Imagem do resultado esperado

foto.png

Sobre

Mapas marte descrevem a localização de veiculos/equipamentos que possuem o hardware "Marte" instalado.

Este mapa demonstra os veículos e equipamentos, bem como as suas últimas localizações.

Para acessar, certifique-se que o ambiente atual é um ambiente com Marte ativo, caso esteja tudo configurado corretamente, poderá acessá-lo através do menu Marte > Mapa de Equipamentos com Marte.

Mapa

caminho: {host}/marte/map/live

map_marte.png

Este mapa possui recarregamento automático que atualiza as localizações e demais informações, este será executado a cada 30s. {.is-warning}

  1. Visualização do Mapa: Contém a exibição dos veículos e equipamentos, camadas de objetos GIS ou o trajeto dos veiculos/equipamentos.
  2. Menu Lateral: Este menu ficará oculto normalmente, podendo ser expandido através do botao ocultar lista(vide item legenda 4).
  3. Tabs menu lateral: Exibida ao expandir o menu lateral, possui funcionalidades que trabalham em conjunto com o mapa; é composta pelos seguintes items:
    • Localizações: Lista contendo os veículos e equipamentos, sendo cada registro dessa lista dividido em: ações(Zoom, rastreio e telemetria), código, descrição e a ultima posição registrada.
    • Parâmetros: Configurção de datas para efetuar o rastreio do trajetos do veiculo/equipamento, utilizando as ação rastrear, descrita no item acima.
    • Grupos GIS: Esta tab será exibida caso existam camadas de objetos GIS ativas(exista algum objeto GIS cadastrado). Contém os grupos GIS que correspondem a objetos GIS cadastrados.
    • Camadas São geradas ao clicar em algum dos grupos descritos no item acima, podendo ser: Ponto interesse, Área de dominio da obra, entre outros.
  1. Menu Ferramentas: Contêm algumas opções como: exibir/ocultar Lista, ajustar mapa, exibir icones em angulo, ver em modo de pontos, ver em modo de cluster, minha localização, fullscreen.

Marcadores de Veículos/Equipamentos

eqp_marker.png

  1. Marcador do veículo/equipamento: descreve a ultima posição registrada, ao clicar neste marcador, um pop-up irá surgir, contendo os outros items listados abaixo;
  2. Rastreio do trajeto: similar a ação disponivel na tab Localizações;
  3. Ultima posição registrada: De acordo com a fonte do registro, poderá conter por exemplo posições GSM,APP ou CRUD;
  4. Caracteristicas: descreve alguns atributos do veículo/equipamento em questão.

Mapas XMOVA Location

Esse tipo de mapa necessita apenas que algumas configurações basicas sejam executadas para que os items sejam efetivamente exibidos, em geral, este tipo de mapa utiliza o registro de location para exibição dos marcadores. exemplo:

  1. Criar um EntryPoint com FILE_OR_DOMAIN = "XMOVALOCATION";
  2. Definir no campo TABELA a tabela "EQP";
  3. Definir no campo FIELD o campo da tabela "nfs_sync_req_xmova_auth" que sera o vinculo com o SEQ_DB do equipamento(normalmente seqEquipamento);
INSERT INTO nfs_entry_point(SEQ_DB, 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(33, 'XMOVALOCATION', NULL, 9999, '9999', 9999, 9999, 9999, 'EQUIPAMENTO', NULL, NULL, 0, 0, 1, 'SQL', 'seqEquipamento', 'MAPAS', '2018-09-28 11:32:48', '2018-09-28 11:32:48', NULL, NULL);

Para esse tipo de mapa será necessario passarmos a tabela referente ao objeto que será exibido na tela, alem do nome do campo que contem o SEQ_DB e que vai fazer a relação do objeto com a sua location(no exemplo usamos o campo seqEquipamento). { .is-warning }

map.png

  • Exemplo de mapa para a tabela FUNCIONARIO usando location.

Tracking

Por padrão o tracking esta disponivel para este tipo de mapa, para isso basta executar os seguintes passos:

  1. No mapa com as ultimas localizações, selecione o objeto que deseja ver o trajeto;
  2. Defina na aba "Parâmetros" o periodo do trajeto;
  3. Clique no marcador do objeto e no botão azul "Tracking";
  4. Feito isso a aba com o trajeto será aberta. Video demonstração

Dados via Entry point

Este tipo de mapa permite que os dados venham diretamente de consultas feitas via entry point, para isso devemos realizar as consultas e retornala no $this->outputValues em um formato que é esperado pela biblioteca de renderização de mapas utilizada atualmente no sistema(Leflet).

parametros:

  • _config: Configuração do mapa como nome da camada, etc...;
  • _jsonReturn: Json que será utilizado para renderização das areas no mapa;
  • _aLists: Lista de registros.

Exemplos

Mapas de clientes:

EntryPoint clientes(MAPS): https://smartos.c.simova.com.br/gis/tracking/FUNCIONARIO/9/0?popup=1

$_jsonReturn = ['status' => [], 'config' => [], 'layers' => [], 'lists' => []];
$_config['mapName'] = 'Cliente';
$_aLists['layersClientes'] = [];

$clientesList = Dao::table('CLIENTE')
			->select(['SEQ_DB','POSICAO_PLAT','POSICAO_PLON','POSICAO_DH','DESCRICAO','CODIGO','UPD_DH'])
            ->whereIsNotNull('POSICAO_PLAT')
			->whereIsNotNull('POSICAO_PLON')
            ->limit(9999999)
            ->get();

$geoFenceDistance = 100;
foreach ($clientesList as $_seq => $_obj) {
	if (!empty($_obj['POSICAO_PLAT']) && !empty($_obj['POSICAO_PLON'])) {
		$geofence = [];
		if ($geoFenceDistance > 0) {
			$geofence = Utils::getArrayFromBoundingBox(floatval($_obj['POSICAO_PLAT']), floatval($_obj['POSICAO_PLON']), $geoFenceDistance);
		}
		$datePOS = isset($_obj['POSICAO_DH']) ? $_obj['POSICAO_DH'] : $_obj['UPD_DH'];
		array_push($_aLists['layersClientes'],
			array('date_pos' => $datePOS,
				'date_pos_str' => $datePOS,
				'plat' => $_obj['POSICAO_PLAT'],
				'plon' => $_obj['POSICAO_PLON'],
				'popup' => $_obj['DESCRICAO'],
				'label' => $_obj['DESCRICAO'],
				'geofence'=> json_encode($geofence),
				'popup_json' => json_encode(array(
						'CLIENTE' => $_obj['CODIGO'],
						'COORDENADAS' => $_obj['POSICAO_PLAT'].' '.$_obj['POSICAO_PLON'],
						'DESCRIÇÃO' => $_obj['DESCRICAO']
						)
					),
				'popup_end' => "<div class='text-center'>".\html\nfsUi::buttonCrudSeq('CLIENTE',$_obj['SEQ_DB'],$_obj['CODIGO'])."</div>",
				'icon' => ''
			)
		);
	}
}
$_config['layersClientes']['showDescricao'] = true;
array_push($_jsonReturn['layers'], array('name' => 'Cliente','list' => 'layersClientes','mtype' => 'url'));

/** envia os valores para saida do entry point */
$this->outputValues = compact('_config', '_jsonReturn', '_aLists');

Mapa de torres:

tower.png

$_jsonReturn = ['status' => [], 'config' => [], 'layers' => [], 'lists' => []];
$_config['mapName'] = 'Torres com cerca eletrônica';
$_aLists['torres'] = [];

$statusTorre = \core\TabelaBO::getAllDataToArrayKey("STATUS_TORRE","SEQ_DB");
$towers = \core\TabelaBO::getAllDataToArrayKey("ENERGY_TOWER","SEQ_DB");
$towerType = \core\TabelaBO::getAllDataToArrayKey("ENERGY_TOWER_TYPE","SEQ_DB");
$parametros = \core\TabelaBO::getAllDataToArrayKey("PARAMETRO_GERAL","SEQ_DB");
$geoFenceDistance = 100;
foreach ($towers as $_seq => $_obj) {
	if (!empty($_obj['POSICAO_PLAT']) && !empty($_obj['POSICAO_PLON'])) {
		foreach ($parametros as $param) {
			if ($_obj['LOCAL'] == $param['LOCAL']){
				if(!empty($param['CERCA_TORRE']) and $param['CERCA_TORRE'] > 0){
					$geoFenceDistance = intval($param['CERCA_TORRE']);
				}
				else{
					if(!empty($param['CERCA_PADRAO']) and $param['CERCA_PADRAO'] > 0){
						$geoFenceDistance = intval($param['CERCA_PADRAO']);
					}
				}
			}
		}
		
		$geofence = [];
		if ($geoFenceDistance > 0) {
			$geofence = Utils::getArrayFromBoundingBox(floatval($_obj['POSICAO_PLAT']), floatval($_obj['POSICAO_PLON']), $geoFenceDistance);
		}
		$datePOS = isset($_obj['POSICAO_DH']) ? $_obj['POSICAO_DH'] : $_obj['UPD_DH'];
		array_push($_aLists['torres'],
			array('date_pos' => $datePOS,
				'date_pos_str' => $datePOS,
				'plat' => $_obj['POSICAO_PLAT'],
				'plon' => $_obj['POSICAO_PLON'],
				'popup' => $_obj['DESCRICAO'],
				'label' => $_obj['DESCRICAO'],
				'geofence'=> json_encode($geofence),
				'popup_json' => json_encode(array(
						'TORRE' => $_obj['CODIGO'],
						'COORDENADAS' => $_obj['POSICAO_PLAT'].' '.$_obj['POSICAO_PLON'],
						'DESCRIÇÃO' => $_obj['DESCRICAO']
						)
					),
				'popup_end' => "<div class='text-center'>".\html\nfsUi::buttonCrudSeq('ENERGY_TOWER',$_obj['SEQ_DB'],$_obj['CODIGO'])."</div>",
				'icon' => $statusTorre[$_obj['STATUS_TORRE_SEQ_DB']]['ICON']
			)
		);
	}
}
$_config['torres']['showDescricao'] = true;
array_push($_jsonReturn['layers'], array('name' => 'Torres','list' => 'torres','mtype' => 'url'));
$this->outputValues = compact('_config', '_jsonReturn', '_aLists');

Filtros

Para adicionarmos filtros nos mapas gerados por entry point primeiramente devemos nos certificar que tabela nfs_gis_maps existe no ambiente, caso não exista, execute o comando CREATE DS/DDL FULL no admin console, que ela será criada. exemplo de filtro:

INSERT INTO nfs_gis_maps
(SEQ_DB, NAME, DISPLAY_NAME, FILTERS, ENABLED, INS_DH, UPD_DH, NFS_USER, DB_USER)
VALUES(1, 'action_do_entrypoint_de_mapa', 'Clientes Max', '{
	"inicio": {
		"type": "Date",
		"options": {
			"label": "De",
			"required": false
		}
	},
    "fim": {
        "type": "Date",
        "options": {
            "label": "Até",
            "required": false,
			"language": ''pt-BR''
        }
    },
    "cliente": {
		"type": "Table",
		"options": {
			"label": "Cliente",
			"required": false,
			"multiple": true
		}
	},
	"numero": {
		"type": "Decimal",
		"options": {
			"label": "Numero",
			"required": false
		}
	},
 	"codigo": {
        "type": "Text",
        "options": {
            "label": "Código",
            "required": false
        }
    },
	"hasEqp": {
    	"type": "Choice",
   		"options": {
    		"label": "Tem Equipamento?",
			"required": false,
    		"choices": {
        		"Sim": "1",
				"Não": "0"
      		}
    	}
  	},
	"limit": {
		"type": "Integer",
		"options": {
			"label": "Limite",
			"required": true
		}
	},
}
', 1, '2023-06-19 09:51:26', '2023-06-28 14:18:58', NULL, NULL);

feito isso os valores que os usuario preencher no filtro, será enviado e poderá ser acessado no entry point através da variavel $this->inputValues, desta forma:

$_jsonReturn = ['status' => [], 'config' => [], 'layers' => [], 'lists' => []];
$_config['mapName'] = 'Cliente';
$_aLists['layersClientes'] = [];

$filters = $this->inputValues ?? [];

if (!empty($filters)) {
	/** Criação do objeto Dao */
	$dao = Dao::table('CLIENTE')
			->select(['SEQ_DB','POSICAO_PLAT','POSICAO_PLON','POSICAO_DH','DESCRICAO','CODIGO','UPD_DH', 'FLAG_TEM_EQUIPAMENTO'])
            ->whereIsNotNull('POSICAO_PLAT')
			->whereIsNotNull('POSICAO_PLON');

	/** Adição dos Filtros */
	extract($filters);
	if($inicio && $fim) {
		$dao->whereDateBetween('POSICAO_DH', $inicio, $fim);
	}
	if($cliente) {
		$dao->whereIn('SEQ_DB', $cliente);
	}
	if ($codigo) {
		$dao->where(['CODIGO' => $codigo]);
	}
	if ($hasEqp) {
		$dao->where(['FLAG_TEM_EQUIPAMENTO' => $hasEqp]);
	}
	if($limit) {
		$dao->limit($limit);
	}
	/** Consulta do banco */
	$clientesList = $dao->get();
}

$geoFenceDistance = 0;
foreach ($clientesList as $_seq => $_obj) {
	if (!empty($_obj['POSICAO_PLAT']) && !empty($_obj['POSICAO_PLON'])) {
		$geofence = [];
		if ($geoFenceDistance > 0) {
			$geofence = Utils::getArrayFromBoundingBox(floatval($_obj['POSICAO_PLAT']), floatval($_obj['POSICAO_PLON']), $geoFenceDistance);
		}
		$datePOS = isset($_obj['POSICAO_DH']) ? $_obj['POSICAO_DH'] : $_obj['UPD_DH'];
		array_push($_aLists['layersClientes'],
			array('date_pos' => $datePOS,
				'date_pos_str' => $datePOS,
				'plat' => $_obj['POSICAO_PLAT'],
				'plon' => $_obj['POSICAO_PLON'],
				'popup' => $_obj['DESCRICAO'],
				'label' => $_obj['DESCRICAO'],
				'geofence'=> json_encode($geofence),
				'popup_json' => json_encode(array(
						'CLIENTE' => $_obj['CODIGO'],
						'COORDENADAS' => $_obj['POSICAO_PLAT'].' '.$_obj['POSICAO_PLON'],
						'DESCRIÇÃO' => $_obj['DESCRICAO']
						)
					),
				'popup_end' => "<div class='text-center'>".\html\nfsUi::buttonCrudSeq('CLIENTE',$_obj['SEQ_DB'],$_obj['CODIGO'])."</div>",
				'icon' => ''
			)
		);
	}
}
$_config['layersClientes']['showDescricao'] = true;
array_push($_jsonReturn['layers'], array('name' => 'Cliente','list' => 'layersClientes','mtype' => 'url'));

$this->outputValues = compact('_config', '_jsonReturn', '_aLists');

Importação

Data Import - Padrão Simova

Introdução

Esse módulo visa a Importação de Dados através de Planilhas preenchidas de acordo com os Templates fornecidos pelo próprio módulo.

Fique atento!

  1. Todo processo se baseia nas definições da tabela "nfs_core_ds_tabela_campo";
  2. Planilhas Compatíveis / Testadas: MS Excel / LibreOffice Calc;
  3. Rotinas Executadas: Inserção / Atualização.

IMPORTANTE: Por se tratar de processo de importação, o processo depende do volume de dados importados e das configurações definidas para os processos PHP/NGINX. Nos scripts PHP o tempo de execução máximo é definido dinamicamente. No NGINX, requer a definição do parâmentro a seguir:{.is-danger}

# /etc/nginx/<dominio>
location ~ \.php(/.*)?$ {
   ...
   fastcgi_read_timeout 1500
   ...

Pré-requesitos

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

  • Permissão na tabela de rotas para /dataImport;
  • Tabelas "nfs_core_ds_import" e "nfs_core_ds_import_file" criadas na seguinte estrutura:
CREATE TABLE `nfs_core_ds_import` (
  `SEQ_DB` bigint(20) NOT NULL AUTO_INCREMENT,
  `TITLE` varchar(100) DEFAULT NULL,
  `CONFIG` varchar(4000) 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;

CREATE TABLE `nfs_core_ds_import_file` (
  `SEQ_DB` bigint(20) NOT NULL AUTO_INCREMENT,
  `EMPRESA` int(11) NOT NULL,
  `FILIAL` int(11) NOT NULL,
  `LOCAL` int(11) NOT NULL,
  `INS_DH` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `INS_USUARIO_SEQ_DB` bigint(20) unsigned NOT NULL,
  `TABLENAME` varchar(60) DEFAULT NULL,
  `UPLOAD_SEQ_DB` bigint(20) unsigned DEFAULT NULL,
  `SIT_PROCESS` tinyint(1) NOT NULL DEFAULT '0',
  `JSON_STATUS` varchar(500) DEFAULT NULL,
  `REPORT_UPLOAD_SEQ_DB` bigint unsigned DEFAULT NULL,
  UNIQUE KEY `SEQ_DB_UNIQUE` (`SEQ_DB`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;

Configurações

Tabela nfs_core_ds_import

Possui as definições principais do ambiente, sendo:

  • TITLE: Título do ambiente;
TITLE='Importação de Dados'
  • CONFIG: Configuração do ambiente de importação;
{
  "TABELAS": ["TALHAO", "OPERADOR"],
  "COLUNAS": {
    "TALHAO": ["CODIGO", "DESCRICAO", "FAZENDA", "ATIVO"]
  },
  "KEYS": {
    "TALHAO": "CODIGO",
    "CLIENTE": "CODIGO",
    "FUNCAO": "CODIGO",
    "FAZENDA": "CODIGO",
    "SETOR": "CODIGO",
    "OPERADOR": "CRACHA"
  },
  "NOME_PLANILHA": {
    "OPERADOR": "OPER"
  },
  "CUSTOMIZATION": {
    "STANDARD_CELL_COLOR": "FF00FF00",
    "STANDARD_FONT_COLOR": "FF0000FF",
    "REQUIRED_CELL_COLOR": "FFFF0000",
    "REQUIRED_FONT_COLOR": "FFFFFF00"
  }
}

Onde:

  • TABELAS: lista de tabelas principais passíveis de importação. Também serão importados dados das tabelas relacionadas (os relacionamentos serão criados usando as colunas TIPO e LINK da tabela "nfs_core_ds_tabela_campo", sendo a coluna TIPO FK e a tabela relacionada a especificada na coluna LINK);

  • COLUNAS: lista as colunas que farão parte da importação (que estarão disponíveis no template quando este for baixado no módulo). Se este grupo não estiver presente será usada a regra disposta em ORIENTAÇÕES GERAIS desse documento;

  • KEYS: chaves de todas as tabelas envolvidas no processo de importação. A coluna especificada para cada tabela funcionará como chave primária, PK, durante o processo de importação, assim permitindo a devida criação das chaves estrangeiras, FK, nas rotinas de inserção/atualização;

  • NOME_PLANILHA: essa propriedade é usada como um apelido que para os nomes das planilhas dentro do arquivo;

    O nome da planilha não pode ser maior que 31 caracteres. Para tabelas que possuem um valor maior que 31 caracteres essa configuração é obrigatória.

  • CUSTOMIZATION: possibilita a personalização do cabeçalho da planilha de importação de dados. A configuração deste item é opcional, quando utilizado permite que seja alterada a cor da fonte e a cor da célula dos campos presentes no cabeçalho, podendo por exemplo diferenciar visualmente campos obrigatórios de campos não obrigatórios.

    • Parâmetros de customização: Todos os parâmetros são opcionais, podendo ser configurado apenas um ou todos eles, conforme necessidade. São eles:
      1. "STANDARD_CELL_COLOR": Cor da CÉLULA de campo NÃO OBRIGATÓRIO.
      2. "STANDARD_FONT_COLOR": Cor da FONTE de campo NÃO OBRIGATÓRIO.
      3. "REQUIRED_CELL_COLOR": Cor da CÉLULA de campo OBRIGATÓRIO.
      4. "REQUIRED_FONT_COLOR": Cor da FONTE de campo OBRIGATÓRIO.

Em caso de não adoção de um dos parâmetros, a configuração default é aplicada, onde a CÉLULA assume cor AZUL e a FONTE do texto assume cor BRANCA.

As CORES a serem utilizadas devem seguir o padrão HEXADECIMAL de 8 caracteres (incluindo os 2 primeiros dígitos de transparência, recomenda-se “FF” para 0% de transparência aplicada), e passadas em formato string sem o uso do caracter “#”, como no exemplo acima.

Chaves Composta e Relacionamentos LOVN!

1. Chaves Composta

Em alguns clientes/estrutura de dados, faz-se necessária a criação de chaves compostas por N Colunas. Para esses caso, as colunas devem ser separadas por ":" (dois pontos) na configuração da KEY usada para aquela tabela. Ao preencher a planilha com valores compostos, o caracter usado como separador será o "|" (pipe). Mais detalhes no exemplo a seguir.

Exemplo:

{
  "TABELAS": [
    "ESTADO",
    "OS",
    "OS_TECNICO",
    "STATUS_OS",
    "CLIENTE",
    "FUNCIONARIO"
  ],
  "KEYS": {
    "OS": "CODIGO",
    "OS_TECNICO": "OS:FUNCIONARIO",
    "STATUS_OS": "DESCRICAO",
    "CLIENTE": "CODIGO:LOJA",
    "FUNCIONARIO": "CRACHA",
    "ESTADO": "SIGLA",
    "FAMILIA_EQUIPAMENTO_GRUPO_PRODUTO": "FAMILIA_EQUIPAMENTO:GRUPO_PRODUTO"
  },
  "NOME_PLANILHA": {
    "FAMILIA_EQUIPAMENTO_GRUPO_PRODUTO": "FAMLIA_EQP_GRP_PRODUTO"
  }
}

Usando a configuração acima, o processo de importação usará os valores das colunas OS e FUNCIONARIO da planilha/aba OS_TECNICO para criar uma chave na tabela OS_TECNICO, que no caso seria o CODIGO da OS e o CRACHA do funcionário (sendo essa composição única na importação).

O preenchimento de valores compostos deve usar o pipe (|) como separador. Tomando como base o exemplo anterior, seria informado o valor da coluna CODIGO da aba OS e o valor da coluna CRACHA da aba FUNCIONARIO: "OO1|CRACHÁ 001".

2. Relacionamento LOVN

Para contemplar os relacionamentos LOVN, ao preencher a coluna desse relacionamento, informar os valores separados por ";" (ponto e vírgula).

Exemplo:

{
  "TABELAS": ["CHECK_GRUPO_QUESTAO", "CHECK_TIPO_RESPOSTA", "CHECK_QUESTAO"],
  "KEYS": {
    "CHECK_GRUPO_QUESTAO": "DESCRICAO",
    "CHECK_TIPO_RESPOSTA": "DESCRICAO",
    "CHECK_QUESTAO": "DESCRICAO",
    "EQP_CLASSE": "CODIGO"
  }
}

planilhas.png

IMPORTANTE! Os relacionamento LOVN só serão deletados (ATIVO = 0, DELETED = 1) através desse módulo se a coluna que representa o(s) relacionamento(s) estiver preenchida com a tag LIMPAR_REGISTROS!

Tabela nfs_core_ds_import_file

Estrutura de dados criada para manter histórico de todos os processo executados, sendo:

  • TABLENAME: Nome da tabela principal;
MAIN_TABLE='TALHAO'
  • UPLOAD_SEQ_DB: SEQ_DB da tabela NFS_UPLOAD, responsável pelo armazenamento da planilha enviada;
UPLOAD_SEQ_DB=1
  • SIT_PROCESS: status do processo, sendo [0 = Criado, 3 = Iniciado, 5 = Concluído];
SIT_PROCESS = 0
  • JSON_STATUS: Status final do processo de importação [NULL || JSON];
{
  "ID": "1",
  "tabelas": {
    "CLIENTE": {
      "descricao": "Cliente",
      "importados": 2,
      "atualizados": 0,
      "erro": 0
    },
    "SETOR": {
      "descricao": "Setor / Regi\u00e3o",
      "importados": 5,
      "atualizados": 0,
      "erro": 0
    },
    "FAZENDA": {
      "descricao": "Fazenda",
      "importados": 4,
      "atualizados": 0,
      "erro": 1
    },
    "TALHAO": {
      "descricao": "Talh\u00e3o",
      "importados": 22,
      "atualizados": 0,
      "erro": 1
    }
  },
  "EXECUTION_TIME": 6
}
  • REPORT_UPLOAD_SEQ_DB: SEQ_DB da tabela NFS_UPLOAD, responsável pelo armazenamento da planilha com o resultado do processo de importação;
REPORT_UPLOAD_SEQ_DB=1

Criar Menu de Acesso

É necessário executar as consultas a seguir (seguindo a ordem e, ao executar as inserções 2, 3 e 4, substituir os SEQ_DB'S nas consultas seguintes).

-- 1. raiz do menu
INSERT INTO nfs_core_menu (EMPRESA, FILIAL, `LOCAL`, DESCRICAO, `TYPE`, ATIVO, FATHER, MENUORDER, URL, ICON) VALUES(1, 9999, 9999, 'Gerenciamento de Dados', 'MENU', 1, NULL, 998, '', 'fa fa-database');
-- 2. permissão para raiz
INSERT INTO nfs_acl_grupo_permissao (EMPRESA, FILIAL, `LOCAL`, MENU_SEQ_DB, TABELA_NOME, SIUD, GRUPO_SEQ_DB, CAMPOS_PERMITIDOS) VALUES(1, 9999, 9999, **SEQ_DB_RAIZ**, NULL, '1111', 1, '');
-- 3. item do menu
INSERT INTO nfs_core_menu (EMPRESA, FILIAL, `LOCAL`, DESCRICAO, `TYPE`, ATIVO, FATHER, MENUORDER, URL, ICON) VALUES(1, 9999, 9999, 'Importação de Dados   ', 'MENU', 1, **SEQ_DB_RAIZ**, 1, 'dataImport/', 'fa fa-database');
-- 4. permissão para item de menu
INSERT INTO nfs_acl_grupo_permissao (EMPRESA, FILIAL, `LOCAL`, MENU_SEQ_DB, TABELA_NOME, SIUD, GRUPO_SEQ_DB, CAMPOS_PERMITIDOS) VALUES(1, 9999, 9999, **SEQ_DB_ITEM_MENU**, NULL, '1111', 1, '');

Orientações Gerais

tabelas.png

  1. As Tabelas Principais são as especificadas na configuração;
  2. Em Relacionamentos temos as Dependências, que são as tabelas com dados que serão usados no processo de importação. Esses dados são dispospos em Planilhas/Abas no template, e cada uma delas representa uma Chave Estrangeira (FK) para a Tabela Principal ou para outras Tabelas Relacionadas;
  3. Em Relacionamentos temos os Complementos, que são tabelas com dados opcionais ao processo de importação. Esses dados são dispostos em Planilhas/Abas no template, e cada uma delas representa uma Chave Estrangeira (FK) para uma Tabela LOVN ou para outras Tabelas Relacionadas;
  4. As Colunas disponíveis no template são aquelas que possuem as propriedades GRID = 1 e DISABLED = 0 ou que são especificadas no grupo COLUNAS previstas na Configuração;
  5. Ao selecionar o arquivo com os dados que serão importados é possível gerar um relatório de todo o processo de importação, selecionando a opção "Gerar Relatório". No final do processo esse relatório estará disponível para download através do botão "Baixar Relatório";
  6. Todas as colunas geradas possuem em seu Cabeçalho informações referentes ao Tipo de Dado e preenchimento (se o preenchimento é Obrigatória e/ou Valor Padrão);
  7. Os tipos de dados devem ser respeitados! O processo de importação faz algumas conversões, mas não é possível determinar precisamente todos os dados.

Importação de campo GPS_POINT

  1. Necessário que o campo esteja com GRID = 1; gps_point.png
  2. Ao baixar o TEMPLATE em Gerenciamento de Dados -> Importação de Dados, verificar se a coluna POSICAO está com o valor 1. Este valor definirá que o campo está ativo para ser enviado pela importação; positicao_ativo.png
  3. Atentar para a forma de formatação dos campos de POSICAO_PLAT e POSICAO_PLON, que devem estar separados por .(Ponto) e não ,(Vírgula); lat_long.png

Indicadores - Cards (new)

Cards de indicadores podem ser adicionados em algumas partes do sistema, existem também tipos diferentes de cards, nessa doc explicaremos como e onde esses cards podem ser adicionados e todos os seus tipos e personalizações.

indicadores.png

Locais dos Indicadores

Home Painel CRUD

Tipos de card

Os cards do tipo: mean, sum, panel e também o padrão (sem type) compartilham de uma mesma estrutura de configuração. Cada um deles vai possuir 4 campos que identificamos por esses termos.

card.png

O que apresenta uma diferença visual é o tipo progress. Esse além dos 4 campos semelhantes aos outros tipos teremos mais 3 campos que são:

progress.png

Todos são configurados por JSON e quando há necessidade de uma consulta no banco de dados é usado o NFS Query Builder.

Propriedades do card

O JSON que armazena as configurações espera as seguintes propriedades:

PropriedadeCard PadrãoCard ProgressoAceita estilo?Detalhes
labelsimsimsimRepresenta a descrição do indicador, possui as propriedades text e style.
iconsimsimsimÍcone que é apresentado no card, possui as propriedades class e style. Ver icones
colorsimsimnãoCor do indicador que pode ser encontrada em Color Library
cardsimsimsimPermite receber um estilo personalizado para o card.
numbersimsimsimPermite receber um estilo personalizado para o número principal.
progressBarnãosimsimPermite receber um estilo personalizado a barra de progresso.
statusProgressBarnãosimsimPermite receber um estilo personalizado para o status da barra de progresso (percentual).
statusTextnãosimsimPermite receber um estilo personalizado para o texto que fica abaixo da barra de progresso.
typesimsimnãoOs tipos suportados são: Painel (panel), Média (mean), Soma (sum) e Progresso (progress).
displaysimsimnãoComo será exibido o resultado, caso o resulta do query seja um valor a ser exibido em porcentagem ficaria [result] %.
decimalPlacesimsimnãoQuantidade de casas após a vírgula.
queryBuildersimsimnãoUma query que vai retornar o valor que deseja ser exibido.
reloadsimsimnãoPode ser true ou false, se não for passado a chave reload é considerado por padrão false.
reportEnabledsimsimnãoHabilita o Relatório do Indicador o seu padrão é false.
porcentagesimsimnão"porcentage": {"title": "OS em Andamento"},    
title diciona um título para a barra de progresso.

displayName

Define o nome que aparecerá acima dos quadros fazendo o detalhamento deles.

{
    "displayName": "Indicadores (Momento atual)"
}

Card padrão

O modelo de card padrão pode não ter um type definido ou ser dos tipos "sum" ou "mean".

Exemplo:

{
  "totalOS": {
    "label": {
			"text": "OS criadas em 2018"
		},
    "color": "red",
    "icon": {
			"class": "fa-signal"
		},
    "display": "[result]",
    "type": "",
    "reload": true,
    "queryBuilder": {
      "totalOS": {
        "select": [
          "count(os.seq_db) result"
        ],
        "from": "os os",
        "where": [
          "os.INS_DH > '2018-01-01 00:00:00'"
        ]
      }
    }
  }
}
  • totalOS: Será o nome do indicador e não pode haver dois iguais.
  • label
    • text: Representa uma descrição que será exibida no indicador.
  • color : Cor do indicador. Se houver configurado a propriedade card esse campo não é necessário.
  • icon
    • class : Nome do ícone que será exibido. Por padrão o ícone é exibido como uma marca d'agua no canto inferior esquerdo do car.
  • type: Os tipos suportados são: mean (média) e sum (soma). Caso deseje somente exibir um determinado valor diretamente não adicione a propriedade type ao objeto e retorne um único valor.
    • mean: A query deve retornar mais de um valor e será feita a sua soma e divido pela quantidade de itens.
    • sum: É feita a soma de todos os valores que são retornados na queryBuilder.
  • display: Como será exibido o resultado, caso o resulta do query seja um valor a ser exibido em porcentagem ficaria [result] %.
  • decimalPlace: Quantidade de casas após a vírgula.
  • queryBuilder: (Exclusivo do Indicador do cadastro): Uma query que vai retornar o valor que deseja ser exibido.
  • reload: Pode ser true ou false, se não for passado a chave reload é considerado por padrão false.

card-default.png

Card type panel

O tipo panel é um indicador com comportamento estático que pode ou não receber uma url. Sem a chave "indicatorUrl" o indicador fica sem interação.

"label": ...
"indicatorUrl": "https://google.com",
"icon": ...

Sem nenhum estilo sua aparencia padrão será igual ao padrão.

card-panel-default.png

Card type progress

O tipo progress habilita novos recursos ao card, mas precisa receber novos itens para que seja exibido corretamente.

Na query precisaremos receber a porcentagem e o valor correspondente para montar uma barra de progresso preenchida com a porcentagem indicada.

No JSON, além da propriedade "type": "progress" vamos adicionar a propriedade "porcentage" que irá receber um objeto contendo um "title": "OS em Andamento" que conterá a descrição do subtítulo.

Exemplo:

{
  "osAnd": {
    "label": {
			"text": "OS Abertas nos últimos 90 dias"
	},
    "color": "yellow-gold",
    "icon": {
    	"class": "fa-file"
	},
    "type": "progress",
    "porcentage": {
      "title": "OS em Andamento"
    },
    "display": "[result]",
    "queryBuilder": {
      "osAnd": {
        "select": [
          "count(os.seq_db) porcentage",
          "180 value"
        ],
        "from": "os os",
        "inner_join": [
          [
            "os",
            "status_os",
            "st",
            "st.SEQ_DB = os.STATUS_OS_SEQ_DB"
          ]
        ],
        "where": [
          "os.INS_DH > date_sub(now(), interval 90 DAY)",
          "st.DESCRICAO  = 'Andamento'"
        ]
      }
    }
  }

Sem nenhum estilo sua aparencia padrão será assim:

card-progress.jpg

Personalização & estilos

Algumas propriedades aceitam receber estilos CSS em seus componentes, podemos passar esses estilos da seguinte forma no nosso objeto JSON. Essas propriedades passam a receber um objeto com essas instruções.

Os estilos serão aplicados a partir do estilo padrão.

card: Recebe a propriedade style e controla os estilos do container. Com a inclusão dessa propriedade a propriedade color do objeto JSON passa a não ter efeito:

"card": {
	"style": {
		"cursor": "pointer",
		"background-color": "#000",
		"border": "2px solid limegreen"
	}
},

label: Recebe a propriedade style e pode receber estilos conforme exemplo:

"label": {
	"text": "OS's Encerradas e Integradas",
	"style": {
		"text-decoration": "none",
		"color": "limegreen",
		"font-weight": "bold",
		"font-family": "\"Open Sans\", sans-serif"          
	}      
},

number: Recebe a propriedade style e pode receber estilos no número principal do card:

"number": {
	"style": {
		"font-size": "2.5em",
		"text-align": "right",
		"color": "#e43a45",
		"font-family": "\"Open Sans\", sans-serif",
		"font-weight": "bold"
	}
},

icon: Recebe a propriedade style e pode receber estilos no icone, e também propriedades de posicionamento.

"icon":{
	"style": {
		"color": "limegreen",
		"margin-top": "-28px",
		"margin-left": "70px"
	},
	"class": "fa-folder-open-o"
},

Resultado da configuração:

card-style.png

Estilos card progress

Como já vimos o card do tipo progress possui elementos adicionais em sua composição, listaremos aqui como podemos aplicar também a esses elementos estilos CSS.

progressBar: Divisão que é usada para ser o fundo da barra de progresso.

"progressBar": {
	"style": {
		"background-color": "black"
	}
},

statusProgressBar: Barra que irá sendo preenchida conforme porcentagem.

"statusProgressBar": {
	"style": {
		"background-color": "green"
	}
},

statusText: Texto localizado abaixo da barra de progresso.

"statusText": {
	"style": {
		"color": "#000"
	}
},

Resultado da configuração:

card-progress-style_2.png

Intervalo de Atualização do Card

O card atualiza a cada minuto por padrão, caso deseja alterar esse valor basta adicionar a chave interval no json, seu valor é em milisegundos, lembrando que cada segundo corresponde a mil milisegundos.

1s = 1000ms

Casos de uso

No caso abaixo a atualização está para ser feita a cada 2 minutos.

{
  "osIntegracao": {
    "label": "OS's com Faturamento liberado",
    "color": "",
    "icon": "fa fa-signal",
    "display": "[result]",
    "interval" : "120000",
    "decimalPlace": "2",
    "queryBuilder": {
      "osIntegracao": {
        "select": [
          "count(os.seq_db) result"
        ],
        "from": "os os",
        "inner_join": [
          [
            "os",
            "status_os",
            "st",
            "st.SEQ_DB = os.STATUS_OS_SEQ_DB"
          ],
          [
            "os",
            "cliente",
            "c",
            "c.SEQ_DB = os.CLIENTE_SEQ_DB"
          ]
        ],
        "where": [
          "st.DESCRICAO in ('Liberada Faturamento')"
        ]
      }
    }
  }
}

Definir Colunas na Tabela de Indicadores

Criando a Tabela

Para criar a tabela de indicadores, é necessário incluir o bloco queryBuilderData. Abaixo está um exemplo completo de definição:

"OSProspeccao": {
    "label": "OT's Programadas (Prospección)",
    "color": "green-jungle",
    "icon": "fa fa-calendar",
    "display": "[result]",
    "queryBuilder": {
        "OSProspeccao": {
            "select": [
                "count(distinct os.CODIGO) result"
            ],
            "from": "os os",
            "inner_join": [
                ["os", "agendamento_servico", "ag", "ag.OS_SEQ_DB = os.SEQ_DB"],
                ["ag", "agendamento_servico_detalhe", "ags", "ags.AGENDAMENTO_SERVICO_SEQ_DB = ag.SEQ_DB"],
                ["ags", "funcionario", "fun", "fun.SEQ_DB = ags.FUNCIONARIO_SEQ_DB"],
                ["ag", "cliente", "cli", "cli.SEQ_DB = ag.CLIENTE_SEQ_DB"]
            ],
            "left_join": [
                ["ag", "apontamento_prospeccao_nc", "apn", "apn.SEQ_DB = ag.APONTAMENTO_PROSPECCAO_NC_SEQ_DB"]
            ],
            "where": [
                "ag.FLAG_PROSPECCAO = 1"
            ]
        }
    },
    "queryBuilderData": {
        "OSProspeccao": {
            "select": [
                "os.SEQ_DB seq_db_os",
                "os.CODIGO 'Código OT'",
                "DATE_FORMAT(os.DATA_ABERTURA, '%d/%m/%Y') 'Fecha Apertura'",
                "CONCAT(fun.CRACHA,'::',fun.NOME) 'Empleado'",
                "apn.SEQ_DB_DEVICE 'Número Checklist'",
                "CONCAT(eqp.CHASSI,'::',eqp.DESCRICAO) 'Equipo'",
                "cli.DESCRICAO 'Cliente'",
                "fun.SEQ_DB func_seq_db"
            ],
            "from": "os os",
            "inner_join": [
                ["os", "agendamento_servico", "ag", "ag.OS_SEQ_DB = os.SEQ_DB"],
                ["ag", "agendamento_servico_detalhe", "ags", "ags.AGENDAMENTO_SERVICO_SEQ_DB = ag.SEQ_DB"],
                ["ags", "funcionario", "fun", "fun.SEQ_DB = ags.FUNCIONARIO_SEQ_DB"],
                ["ag", "cliente", "cli", "cli.SEQ_DB = ag.CLIENTE_SEQ_DB"],
                ["os", "equipamento", "eqp", "eqp.SEQ_DB = os.EQUIPAMENTO_SEQ_DB"]
            ],
            "left_join": [
                ["ag", "apontamento_prospeccao_nc", "apn", "apn.SEQ_DB = ag.APONTAMENTO_PROSPECCAO_NC_SEQ_DB"]
            ],
            "where": [
                "ag.FLAG_PROSPECCAO = 1"
            ],
            "group_by": [
                ["os.SEQ_DB"]
            ],
            "order_by": [
                ["os.SEQ_DB"]
            ]
        }
    },
    "report": {
        "displayColumns": [
            "Código OT",
            "Fecha Apertura",
            "Empleado",
            "Número Checklist",
            "Equipo",
            "Cliente"
        ],
        "links": {
            "Código OT": "/t/os/edit/%seq_db_os%"
        }
    }
}

Definindo as Colunas

Para exibir as colunas na tabela de indicadores:

  1. Inclua os campos desejados na cláusula select do queryBuilderData.
  2. Utilize alias para nomear as colunas conforme devem aparecer na visualização. Por exemplo:
   DATE_FORMAT(os.DATA_ABERTURA, '%d/%m/%Y') 'Fecha Apertura'

Adicione o nome da coluna no array displayColumns.

Observação: Para nomes com espaços, utilize aspas simples no alias.

Indicadores no Cadastro

Na tela de cadastros também há opção de criar indicadores como na imagem abaixo:

An image

Sua razão é mostrar informações dos dados de um cadastro, como por exemplo, foi feito no Cadastro de OS, onde foram criados indicadores mostrando o Total de OS criadas em 2018, Cliente e a quantidade de OS em cada estado (Aberta, Andamento, Pausada, Cancela, Liberada para Faturamento, Finalizada) nos últimos 90 dias.

Sua configuração fica na nfs_core_ds_tabela, onde foi criada uma nova coluna chamada INDICATORS, pode ter 1 ou mais indicadores como é possível ver no exemplo.

Exemplo

O exemplo abaixo foi feito para a tela de OS da baggio:

{
  "totalOS": {
    "label": {
			"text": "OS criadas em 2018"
		},
    "color": "",
    "icon": {
			"class": "fa-signal"
		},
    "display": "[result]",
    "queryBuilder": {
      "totalOS": {
        "select": [
          "count(os.seq_db) result"
        ],
        "from": "os os",
        "where": [
          "os.INS_DH > '2018-01-01 00:00:00'"
        ]
      }
    }
  },
  "clientesAt": {
    "label": {
			"text": "Clientes atendidos nos últimos 90 dias"
		},
    "color": "green-jungle",
    "icon": {
			"class": "fa-check"
		},
    "display": "[result]",
    "queryBuilder": {
      "clientesAt": {
        "select": [
          "count(c.seq_db) result"
        ],
        "from": "os os",
        "inner_join": [
          [
            "os",
            "status_os",
            "st",
            "st.SEQ_DB = os.STATUS_OS_SEQ_DB"
          ],
          [
            "os",
            "cliente",
            "c",
            "c.SEQ_DB = os.CLIENTE_SEQ_DB"
          ]
        ],
        "where": [
          "os.INS_DH > date_sub(now(), interval 90 DAY)",
          "st.DESCRICAO not in ('Aberto', 'Cancelado')"
        ]
      }
    }
  },
  "osAberta": {
    "label": {
			"text": "OS abertas nos últimos 90 dias"
		},
    "color": "yellow-crusta",
    "icon": {
			"class": "fa-file"
		},
    "display": "[result]",
    "queryBuilder": {
      "osAberta": {
        "select": [
          "count(os.seq_db) r"
        ],
        "from": "os os",
        "inner_join": [
          [
            "os",
            "status_os",
            "st",
            "st.SEQ_DB = os.STATUS_OS_SEQ_DB"
          ]
        ],
        "where": [
          "os.INS_DH > date_sub(now(), interval 90 DAY)",
          "st.DESCRICAO  = 'Aberto'"
        ]
      }
    }
  },
  "osAnd": {
    "label": {
			"text": "OS em Andamento nos últimos 90 dias"
		},
    "color": "yellow-gold",
    "icon": {
			"class": "fa-file"
		},
    "display": "[result]",
    "queryBuilder": {
      "osAberta": {
        "select": [
          "count(os.seq_db) r"
        ],
        "from": "os os",
        "inner_join": [
          [
            "os",
            "status_os",
            "st",
            "st.SEQ_DB = os.STATUS_OS_SEQ_DB"
          ]
        ],
        "where": [
          "os.INS_DH > date_sub(now(), interval 90 DAY)",
          "st.DESCRICAO  = 'Andamento'"
        ]
      }
    }
  },
  "osPausado": {
    "label": {
			"text": "OS Pausada últimos 90 dias"	
		},
    "color": "red-mint",
    "icon": {
			"class": "fa-file"
		},
    "display": "[result]",
    "queryBuilder": {
      "osAberta": {
        "select": [
          "count(os.seq_db) r"
        ],
        "from": "os os",
        "inner_join": [
          [
            "os",
            "status_os",
            "st",
            "st.SEQ_DB = os.STATUS_OS_SEQ_DB"
          ]
        ],
        "where": [
          "os.INS_DH > date_sub(now(), interval 90 DAY)",
          "st.DESCRICAO  = 'Pausado'"
        ]
      }
    }
  },
  "osCancelado": {
    "label": {
			"text": "OS Cancelada últimos 90 dias"
		},
    "color": "grey-gallery",
    "icon": {
			"class": "fa-file"
		},
    "display": "[result]",
    "queryBuilder": {
      "osAberta": {
        "select": [
          "count(os.seq_db) r"
        ],
        "from": "os os",
        "inner_join": [
          [
            "os",
            "status_os",
            "st",
            "st.SEQ_DB = os.STATUS_OS_SEQ_DB"
          ]
        ],
        "where": [
          "os.INS_DH > date_sub(now(), interval 90 DAY)",
          "st.DESCRICAO  = 'Cancelado'"
        ]
      }
    }
  },
  "osLibFaturamento": {
    "label": {
			"text": "OS Liberada para Faturamento últimos 90 dias"
		},
    "color": "blue",
    "icon": {
			"class": "fa-file"
		},
    "display": "[result]",
    "queryBuilder": {
      "osAberta": {
        "select": [
          "count(os.seq_db) r"
        ],
        "from": "os os",
        "inner_join": [
          [
            "os",
            "status_os",
            "st",
            "st.SEQ_DB = os.STATUS_OS_SEQ_DB"
          ]
        ],
        "where": [
          "os.INS_DH > date_sub(now(), interval 90 DAY)",
          "st.DESCRICAO  = 'Liberado Faturamento'"
        ]
      }
    }
  },
  "osFechada": {
    "label": {
			"text": "OS finazalidas dos últimos 90 dias"
		},
    "color": "grey-cascade",
    "icon": {
			"class": "fa-file"
		},
    "display": "[result]",
    "queryBuilder": {
      "osFechada": {
        "select": [
          "count(os.seq_db) r"
        ],
        "from": "os os",
        "inner_join": [
          [
            "os",
            "status_os",
            "st",
            "st.SEQ_DB = os.STATUS_OS_SEQ_DB"
          ]
        ],
        "where": [
          "os.INS_DH > date_sub(now(), interval 90 DAY)",
          "st.DESCRICAO  = 'Fechado'"
        ]
      }
    }
  }
}

Exemplo de Indicador com Field Action

Veja aqui field action.

{
  "osIntegracao": {
    "label": {
      "text": "OS's com Faturamento liberado"
    },
    "color": "",
    "icon": {
      "class": "fa fa-signal"
    },
    "display": "[result]",
    "interval": "120000",
    "decimalPlace": "2",
    "queryBuilder": {
      "osIntegracao": {
        "select": [
          "count(os.seq_db) result"
        ],
        "from": "os os",
        "inner_join": [
          [
            "os",
            "status_os",
            "st",
            "st.SEQ_DB = os.STATUS_OS_SEQ_DB"
          ],
          [
            "os",
            "cliente",
            "c",
            "c.SEQ_DB = os.CLIENTE_SEQ_DB"
          ]
        ],
        "where": [
          "st.DESCRICAO in ('Liberada Faturamento')"
        ]
      }
    },
    "queryBuilderData": {
      "osIntegracao": {
        "select": [
          "os.seq_db Integrar",
          "os.codigo Codigo",
          "c.DESCRICAO Cliente",
          "st.DESCRICAO Status"
        ],
        "from": "os os",
        "inner_join": [
          [
            "os",
            "status_os",
            "st",
            "st.SEQ_DB = os.STATUS_OS_SEQ_DB"
          ],
          [
            "os",
            "cliente",
            "c",
            "c.SEQ_DB = os.CLIENTE_SEQ_DB"
          ]
        ],
        "where": [
          "st.DESCRICAO in ('Liberada Faturamento')"
        ]
      }
    },
    "report": {
      "displayColumns": [
        "Codigo",
        "Cliente",
        "Status",
        "Integrar"
      ],
      "links": {
        "Codigo": "/t/os/%Integrar%"
      },
      "fieldAction":{
        "Integrar" : "INDICATOR_FAT"
      }
    }
  }
}

Estilos e personalização

É possivel aplicar estilos CSS para alterar a aparencia dos cards, clique aqui para saber mais.

Indicadores na Home

Os cards indicadores podem ser exbidos da tela inicial do NFS. O JSON de configuração dos indicadores na home é feita na tabela nfs_cloud > nfs_rpo no campo CODE.

No RPO ou EntryPoint busque ou crie um entry point com número 502 e com name de home_indicator.

indicadores-home.png

Exemplo completo:

INSERT INTO nfs_cloud.nfs_rpo
(SEQ_DB, NAME, FILE_OR_DOMAIN, `ACTION`, XMOVA_INSTALLCODE, XMOVA_INSTALLCODE_VERSION, TABELA, ENTRY_NUM, CODE, VERSION, BUILD, ATIVO, CODETYPE, FIELD, DESCRIPTION, INS_DH, UPD_DH)
VALUES(80, 'home_indicator', 'SYSTEM', 'IndicatorHome', NULL, NULL, NULL, 502, '{
  "osIntegracao": {
	"card": {
		"style": {
			"cursor": "pointer",
			"background-color": "burlywood",
			"border": "2px solid #000",
			"max-height": "125px"
		}
	},
	"number": {
		"style": {
			"font-size": "1.5em",
			"color": "#000",
			"font-family": "\\"Open Sans\\", sans-serif",
			"font-weight": "bold"
		}
	},
	"icon":{
		"style": {
			"color": "#E87E04",
			"opacity": "0.3",
			"margin-top": "11px",
    		"margin-left": "-42px",
    		"transform": "scale(2.5)"
		},
		"class": "fa-file"
	},
	"statusText": {
		"style": {
			"color": "#000"
		}
	},
	"progressBar": {
		"style": {
			"background-color": "black"
		}
	},
	"label": {
		"text": "OS Abertas nos últimos 90 dias",
		"style": {
			"font-size": "1em",
			"text-decoration": "none",
			"color": "#000",
			"font-weight": "bold",
			"font-family": "\\"Open Sans\\", sans-serif"			
		}
	},
    "color": "yellow-gold",
    "type": "progress",
    "porcentage": {
      "title": "OS em Andamento"
    },
    "display": "[result]",
    "queryBuilder": {
      "osIntegracao": {
        "select": [
          	"count(c.seq_db) porcentage",
			"180 value"
        ],
        "from": "os os",
        "inner_join": [
          [
            "os",
            "status_os",
            "st",
            "st.SEQ_DB = os.STATUS_OS_SEQ_DB"
          ],
          [
            "os",
            "cliente",
            "c",
            "c.SEQ_DB = os.CLIENTE_SEQ_DB"
          ]
        ],
        "where": [
          "(os.PROC_ST = 0 or os.PROC_ST is null)",
          "os.STATUS_OS_SEQ_DB = 3"
        ]
      }
    },
    "queryBuilderData": {
      "osIntegracao": {
        "select": [
          "os.seq_db seq_db",
          "os.codigo Codigo",
          "c.DESCRICAO Cliente",
          "st.DESCRICAO Status"
        ],
        "from": "os os",
        "inner_join": [
          [
            "os",
            "status_os",
            "st",
            "st.SEQ_DB = os.STATUS_OS_SEQ_DB"
          ],
          [
            "os",
            "cliente",
            "c",
            "c.SEQ_DB = os.CLIENTE_SEQ_DB"
          ]
        ],
        "where": [
          "(os.PROC_ST = 0 or os.PROC_ST is null)",
          "os.STATUS_OS_SEQ_DB = 3"
        ]
      }
    },
    "report":{
      "displayColumns": [
        "Codigo",
        "Cliente",
        "Status"
      ],
      "links" : {
        "Codigo" : "/t/os/edit/%seq_db%"
      }
    }
  },
  "mais12Horas": {
    "label": {
		"text": "Boletins com mais de 12 Horas",
		"style": {
			"font-size": "1.3em",
			"text-decoration": "none",
			"color": "#E87E04",
			"font-weight": "bold",
			"font-family": "\\"Open Sans\\", sans-serif"			
		}
	},
    "color": "yellow-gold",
	"card": {
		"style": {
			"cursor": "pointer",
			"background-color": "#FFF",
			"border": "3px solid #E87E04"
		}
	},
	"number": {
		"style": {
			"font-size": "2.5em",
			"text-align": "right",
			"color": "#E87E04",
			"font-family": "\\"Open Sans\\", sans-serif",
			"font-weight": "bold"
		}
	},
	"icon":{
		"style": {
			"color": "#E87E04",
			"opacity": "0.5",
			"margin-top": "-70px",
    		"margin-left": "-40px",
    		"transform": "scale(0.3)"
		},
		"class": "fa-clock-o"
	},
    "display": "[result]",
    "queryBuilder": {
      "mais12Horas": {
        "select": [
          "count(b.seq_db) result"
        ],
        "from": "boletim b",
        "where": [
          "b.INI_DH > date_sub(now(), interval 180 DAY)",
          "b.INI_FIM_DIFF_SEC >= ((3600) * 12)"
        ]
      }
    }, "queryBuilderData": {
      "mais12Horas": {
        "select": [
          "b.SEQ_DB seq_db",
          "b.FUNCIONARIO_SEQ_DB funcionario_seq_db",
          "concat(f.CRACHA, '' :: '',f.NOME) Funcionário",
          "b.INI_DH Início",
          "b.FIM_DH Término",
          "replace(round((b.INI_FIM_DIFF_SEC)/3600,2),''.'','','')  Total"
        ],
        "from": "boletim b",
        "inner_join": [
          [
            "b",
            "funcionario",
            "f",
            "f.SEQ_DB = b.FUNCIONARIO_SEQ_DB"
          ]
        ],
        "where": [
          "b.INI_DH > date_sub(now(), interval 180 DAY)",
          "b.INI_FIM_DIFF_SEC >= ((3600) * 12)"
        ]
      }
    },
    "report":{
      "displayColumns": [
        "Funcionário",
        "Início",
        "Término",
        "Total"
      ],
      "links" : {
        "Funcionário" : "/details/1/%funcionario_seq_db%/1"
      }
    }
  },
  "OSAberta": {
    "label": "OS''s Abertas",
    "color": "",
    "icon": "fa fa-folder-open-o",
    "display": "[result]",
    "queryBuilder": {
      "OSAberta": {
        "select": [
          "count(os.seq_db) result"
        ],
        "from": "os os",
        "where": [
          "(os.STATUS_OS_SEQ_DB = 4 or os.STATUS_OS_SEQ_DB = 1)"
        ]
      }
    },
    "queryBuilderData": {
      "OSAberta": {
        "select": [
          "os.SEQ_DB seq_db_os",
          "os.CODIGO CODIGO",
          "date_format(os.DATA_ABERTURA, ''%d/%m/%Y'') ABERTURA",
          "sto.DESCRICAO STATUS",
          "case when os.STATUS_OS_SEQ_DB = 4 then mot.DESCRICAO else '''' end as MOTIVO_PAUSA",
          "datediff((date(sysdate())),(date(os.DATA_ABERTURA)))  DIAS_ABERTOS"
        ],
        "from": "os os",
        "left_join": [
          [
            "os",
            "status_os",
            "sto",
            "sto.SEQ_DB = os.STATUS_OS_SEQ_DB"
          ],
          [
            "os",
            "apontamento_status_os",
            "apt",
            "apt.OS_SEQ_DB = os.SEQ_DB"
          ],
          [
            "os",
            "motivo_pausa",
            "mot",
            "mot.SEQ_DB = apt.MOTIVO_PAUSA_SEQ_DB"
          ]
        ],
        "where": [
          "(os.STATUS_OS_SEQ_DB = 4 or os.STATUS_OS_SEQ_DB = 1)"
        ],
        "group_by": [
          ["os.SEQ_DB"]
        ],        
        "order_by": [
          ["os.SEQ_DB"],
          ["max(apt.SEQ_DB)"]
        ]

      }
    },
    "report": {
      "displayColumns": [
        "CODIGO",
        "ABERTURA",
        "DIAS_ABERTOS",
        "STATUS",
        "MOTIVO_PAUSA"
      ],
      "links": {
        "CODIGO": "/t/os/edit/%seq_db_os%"
      }
    }
  },
  "OSEncerradaIntegrada": {
	"card": {
		"style": {
			"cursor": "pointer",
			"background-color": "#000",
			"border": "2px solid limegreen"
		}
	},
	"number": {
		"style": {
			"color": "limegreen",
			"font-family": "\\"Open Sans\\", sans-serif",
			"font-weight": "bold"
		}
	},
	"icon":{
		"style": {
			"color": "limegreen",
			"margin-top": "-28px",
    		"margin-left": "70px",
			"opacity": "0.2"
		},
		"class": "fa-folder-open-o"
	},
	"indicatorUrl": "https://google.com",
    "label": {
		"text": "OS''s Encerradas e Integradas",
		"style": {
			"text-decoration": "none",
			"color": "limegreen",
			"font-weight": "bold",
			"font-family": "\\"Open Sans\\", sans-serif"			
		}
	},
	"type": "panel",
    "display": "[result]",
    "queryBuilder": {
      "OSEncerradaIntegrada": {
        "select": [
          "count(os.seq_db) result"
        ],
        "from": "os os",
        "where": [
          "os.PROC_ST = 4"
        ]
      }
    },
    "queryBuilderData": {
      "OSAberta": {
        "select": [
          "os.SEQ_DB seq_db_os",
          "os.CODIGO CODIGO",
          "date_format(os.DATA_ABERTURA, ''%d/%m/%Y'') ABERTURA",
          "os.PROC_DESC INTEGRADOR"
        ],
        "from": "os os",
        "where": [
          "os.PROC_ST = 4"
        ],
        "order_by": [
          ["os.SEQ_DB"]
        ]

      }
    },
    "report": {
      "displayColumns": [
        "CODIGO",
        "ABERTURA",
        "INTEGRADOR"
      ],
      "links": {
        "CODIGO": "/t/os/edit/%seq_db_os%"
      }
    }
  }  
}', 1, 77, 1, 'JSON', NULL, NULL, '2019-07-02 17:11:04', '2023-06-02 13:55:05');

Estilos e personalização

É possivel aplicar estilos CSS para alterar a aparencia dos cards, clique aqui para saber mais.

Indicadores no Painel

Para adicionar os indicadores no painel basta incluir o objeto de configuração no campo INDICATOR_OPTIONS da tabela nfs_qig_panels.

indicadores-panel.png

Exemplo:

{
  "funcionarioApontando": {
    "label": {
			"text": "Funcionários Apontando"
		},
    "type": "panel",
    "color": "grey",
    "icon": {
			"class": "fa-users"
		},
    "display": "[result]"
  },
  "horas": {
    "label": {
			"text": "Produtividade"
		},
    "type": "panel",
    "color": "green",
    "icon": {
			"class" : "fa-signal"
		},
    "display": "[result] %"
  },
 "os_aberta": {
    "label": {
			"text": "OS's Aberta nos Últimos 30 dias"
		},
    "type": "panel",
    "color": "red",
    "icon": {
			"class": "fa-check"
		},
    "display": "[result]"
  },
 "os_fechada": {
    "label": {
			"text": "OS's Fechadas nos Últimos 30 dias"
		},
    "type": "panel",
    "color": "blue",
    "icon": {
			"class": "fa-close"
		},
    "display": "[result]"
  }
}

É possível adicionar um link usando a seguinte propriedade:

  "funcionarioApontando": {
		{...}
    "indicatorUrl": "/t/equipe"
  },

Estilos e personalização

É possivel aplicar estilos CSS para alterar a aparencia dos cards, clique aqui para saber mais.

Indicadores QII

An image

Foi criado os indicadores para cada QII do painel.

Pode ser configurado por enquanto até 4 indicadores e cada indicador tem 4 campos que são:

  • FIELD: Nome do Apelido que foi criado no Query Builder, esse valor também será exibido ao lado ícone.
  • ICON: Icone de acordo com icon awesome Galeria de Ícones.
  • TOOLTIP: Uma legenda.
  • COLOR: Cor do ícone e do texto, as opções podem ser encontradas em Color Library.

Sua configuração é parecida com a dos indicadores do painel, basta no query builder colocar o valor que queria que seja exibido, esse valor deve ter um apelido igual ao F8 até F11 do display fields.

Ex.:

DISPLAY_FIELDS

...
  "F8": [
    {
      "FIELD": "NOT_WORKING",
      "ICON": "fa-circle",
      "TOOLTIP": "Em Perda",
      "COLOR": "red",
      "VALUE_DEFAULT": "00:00"
    }
  ],
  "F9": [
    {
      "FIELD": "PAUSE",
      "ICON": "fa-pause",
      "TOOLTIP": "Em Almoço",
      "COLOR": "blue"
    }
  ],
  "F10": [
    {
      "FIELD": "WORKING",
      "ICON": "fa-check-square-o",
      "TOOLTIP": "Em Serviço",
      "COLOR": "#1dff5d"
    }
  ],
  "F11": [
    {
      "FIELD": "DO_NOTHING",
      "ICON": "fa-circle-o",
      "TOOLTIP": "Sem Apontamento",
      "COLOR": "yellow-crusta"
    }
...
  • VALUE_DEFAULT : Caso o valor seja nulo irá exibir o valor padrão se estiver configurado, caso o mesmo não esteja configurado irá exibir zero. O valor default é útil quando o FIELD é valor em horas, então pode ser configurado o valor padrão como 00:00 ao invés de 0.

QUERY_BUILDER

{
  "QUERY_BUILDER": {
    "main_table": {
      "select": [
        "FUNCIONARIO.seq_db SEQ_DB",
        "FUNCIONARIO.cracha CRACHA"
      ],
      "from": "FUNCIONARIO FUNCIONARIO",
      "where": [
        "FUNCIONARIO.QIG_DISPLAY = 1"
      ],
      "group_result_by": "SEQ_DB",
      "order_by": [
        [
          "FUNCIONARIO.CRACHA",
          "ASC"
        ]
      ]
    },
    "secondary_table": {
      "select": [
        "IF(BOLETIM_RH.FIM_DH IS NULL, 1, 0) DAY_OFF",
        "BOLETIM_RH.FIM_DH FIM_DH",
        "BOLETIM_RH.FUNCIONARIO_SEQ_DB",
        "FUNCIONARIO.NOME NOME",
        "FUNCIONARIO.FOTO_SEQ_DB FOTO_SEQ_DB",
        "BOLETIM_RH.SEQ_DB BOLETIM_RH_SEQ_DB"
      ],
      "from": "BOLETIM_RH",
      "inner_join": [
        [
          "BOLETIM_RH",
          "FUNCIONARIO",
          "FUNCIONARIO",
          "BOLETIM_RH.FUNCIONARIO_SEQ_DB = FUNCIONARIO.SEQ_DB"
        ]
      ],
      "left_join": [
        [
          "FUNCIONARIO",
          "RESPONSAVEL",
          "RESPONSAVEL",
          "FUNCIONARIO.RESPONSAVEL_SEQ_DB = RESPONSAVEL.SEQ_DB"
        ]
      ],
      "where": [
        "BOLETIM_RH.FIM_DH is NULL",
        "FUNCIONARIO.RESPONSAVEL_SEQ_DB IN (:responsavel)",
        "RESPONSAVEL.GERENCIA_SEQ_DB IN (:gerencia)"
      ],
      "group_result_by": "FUNCIONARIO_SEQ_DB"
    },
    "additional_tables": {
      "apontamento_rh": {
        "select": [
          "(select (sum(time_to_sec(  timeDIFF(IF(rh.fim_dh IS NULL or  rh.fim_dh = '', NOW() , rh.fim_dh), rh.ini_dh)))) from app_apontamento_rh rh  inner join app_operacao o on o.SEQ_DB = rh.OPERACAO_SEQ_DB  where rh.SEQ_DB_DEVICE_MASTER_SEQ_DB = APONTAMENTO_RH.SEQ_DB_DEVICE_MASTER_SEQ_DB and o.FLAG_PRODUTIVA =1 ) as horasProdutivas",
          "(select (sum(time_to_sec(  timeDIFF(IF(rh.fim_dh IS NULL or  rh.fim_dh = '', NOW() , rh.fim_dh), rh.ini_dh)))) from app_apontamento_rh rh  inner join app_operacao o on o.SEQ_DB = rh.OPERACAO_SEQ_DB  where rh.SEQ_DB_DEVICE_MASTER_SEQ_DB = APONTAMENTO_RH.SEQ_DB_DEVICE_MASTER_SEQ_DB and o.FLAG_PRODUTIVA =0 ) as horasImprodutivas",
          " (((select (sum(time_to_sec(  timeDIFF(IF(rh.fim_dh IS NULL or  rh.fim_dh = '', NOW() , rh.fim_dh), rh.ini_dh)))) from app_apontamento_rh rh  inner join app_operacao o on o.SEQ_DB = rh.OPERACAO_SEQ_DB  where rh.SEQ_DB_DEVICE_MASTER_SEQ_DB = APONTAMENTO_RH.SEQ_DB_DEVICE_MASTER_SEQ_DB and o.FLAG_PRODUTIVA =1 ) - (select (sum(time_to_sec(  timeDIFF(IF(rh.fim_dh IS NULL or  rh.fim_dh = '', NOW() , rh.fim_dh), rh.ini_dh)))) from app_apontamento_rh rh  inner join app_operacao o on o.SEQ_DB = rh.OPERACAO_SEQ_DB  where rh.SEQ_DB_DEVICE_MASTER_SEQ_DB = APONTAMENTO_RH.SEQ_DB_DEVICE_MASTER_SEQ_DB and o.FLAG_PRODUTIVA =0 )) / (select (sum(time_to_sec(  timeDIFF(IF(rh.fim_dh IS NULL or  rh.fim_dh = '', NOW() , rh.fim_dh), rh.ini_dh)))) from app_apontamento_rh rh  inner join app_operacao o on o.SEQ_DB = rh.OPERACAO_SEQ_DB  where rh.SEQ_DB_DEVICE_MASTER_SEQ_DB = APONTAMENTO_RH.SEQ_DB_DEVICE_MASTER_SEQ_DB ) * 100 )eficiencia ",
          "APONTAMENTO_RH.SEQ_DB SEQ_DB",
          "APONTAMENTO_RH.INI_DH INI_DH",
          "APONTAMENTO_RH.FIM_DH APT_FIM_DH",
          "concat(OPERACAO.CODIGO, ' - ', OPERACAO.DESCRICAO) OPERACAO",
          "OPERACAO.QII_COLOR QII_COLOR",
          "APONTAMENTO_RH.SEQ_DB_DEVICE_MASTER_SEQ_DB BOLETIM_RH_SEQ_DB",
          "APONTAMENTO_RH.TIPO_APONTAMENTO TIPO_APONTAMENTO",
          "(select COUNT(rh.SEQ_DB) from app_apontamento_rh rh  inner join app_operacao o on o.SEQ_DB = rh.OPERACAO_SEQ_DB  where rh.SEQ_DB_DEVICE_MASTER_SEQ_DB = APONTAMENTO_RH.SEQ_DB_DEVICE_MASTER_SEQ_DB and o.FLAG_PRODUTIVA =1 ) as WORKING",
          "(select concat (COUNT(rh.SEQ_DB), ' %')  from app_apontamento_rh rh  inner join app_operacao o on o.SEQ_DB = rh.OPERACAO_SEQ_DB  where rh.SEQ_DB_DEVICE_MASTER_SEQ_DB = APONTAMENTO_RH.SEQ_DB_DEVICE_MASTER_SEQ_DB and o.FLAG_PRODUTIVA =0 ) as NOT_WORKING",
          "0 as PAUSE",
          "0 as DO_NOTHING"
        ],
        "from": "APONTAMENTO_RH APONTAMENTO_RH",
        "where": [
          "APONTAMENTO_RH.OPERACAO_SEQ_DB IN (:operacao)"
        ],
        "left_join": [
          [
            "APONTAMENTO_RH",
            "OPERACAO",
            "OPERACAO",
            "OPERACAO.SEQ_DB = APONTAMENTO_RH.OPERACAO_SEQ_DB"
          ],
          [
            "APONTAMENTO_RH",
            "CONTRATO",
            "CONTRATO",
            "CONTRATO.SEQ_DB = APONTAMENTO_RH.CONTRATO_SEQ_DB"
          ]
        ],
        "group_result_by": "BOLETIM_RH_SEQ_DB"
      }
    }
  }
}

Intervalo de Atualização

O card atualiza a cada minuto por padrão, caso deseja alterar esse valor basta adicionar a chave interval no json, seu valor é em milisegundos, lembrando que cada segundo corresponde a mil milisegundos

1s = 1000ms

Exemplo de uso

No caso abaixo a atualização está para ser feita a cada 2 minutos.

{
  "osIntegracao": {
    "label": "OS's com Faturamento liberado",
    "color": "",
    "icon": "fa fa-signal",
    "display": "[result]",
    "interval" : "120000",
    "decimalPlace": "2",
    "queryBuilder": {
      "osIntegracao": {
        "select": [
          "count(os.seq_db) result"
        ],
        "from": "os os",
        "inner_join": [
          [
            "os",
            "status_os",
            "st",
            "st.SEQ_DB = os.STATUS_OS_SEQ_DB"
          ],
          [
            "os",
            "cliente",
            "c",
            "c.SEQ_DB = os.CLIENTE_SEQ_DB"
          ]
        ],
        "where": [
          "st.DESCRICAO in ('Liberada Faturamento')"
        ]
      }
    }
  }
}

Indicador Relatório

O indicador pode servir de link para seu relatório, se tiver um indicador que exibe a quantidade de Ordens de Serviço que estão no estado de Aguardando Faturamento, ao configurar o relatório e clicar no indicador vai levar para um tela onde é possível mostrar as colunas, Código da OS, Nome do Cliente, Estado da OS, Técnico, entre outras informações.

Funcionalidade do Relatório:

  • Imprimir
  • Exportação PDF/Excel
  • Colunas podem se tornar link para outros lugares do sistema como por exemplo edição do crud.

Essa configuração é feita em duas partes primeiro o NFS Query Builder, no caso a chave deve chamar queryBuilderData, é o query onde tratá todas as colunas necessárias, exemplo:

"queryBuilderData": {
  "osIntegracao": {
    "select": [
      "os.seq_db seq_db",
      "os.codigo Codigo",
      "c.DESCRICAO Cliente",
      "st.DESCRICAO Status"
    ],
    "from": "os os",
    "inner_join": [
      [
        "os",
        "status_os",
        "st",
        "st.SEQ_DB = os.STATUS_OS_SEQ_DB"
      ],
      [
        "os",
        "cliente",
        "c",
        "c.SEQ_DB = os.CLIENTE_SEQ_DB"
      ]
    ],
    "where": [
      "st.DESCRICAO in ('Liberada Faturamento')"
    ]
  }
}

Dentro do queryBuilderData o nome da query deve ser o mesmo do indicador por questões de relacionamento.

O segundo passo é configurar o relatório, para isso foi criada a chave report, dentro dela teremos displayColumns que é um array das colunas que devem ser mostradas no relatório, se não for configurada vai mostrar todas. A outra chave é links, onde é necessário passar o nome da coluna que deseja o link junto com o link propriamente configurado.

No link se quiser adicionar uma variável no endereço basta colocar entre porcentagem assim,%seq_db%, supode que o nome da coluna seja seq_db, se fosse Código, seria %Código% e etc.

É possível passar um field action (Botão de Ação) dentro da configuração do relatório, a chave fieldAction dentro da chave report, nela é passado o nome da coluna que vai exibir e o NAME do field action.

Exemplo de acordo com a consulta anterior, só desejamos exibir as colunas Codigo, Cliente e Status e na coluna Código terá um link direcionando a edição da OS:

"report":{
  "displayColumns": [
    "Codigo",
    "Cliente",
    "Status"
  ],
  "links" : {
    "Codigo" : "/t/os/edit/%seq_db%"
  },
  "fieldAction":{
    "Integrar" : "INDICATOR_FAT"
  }
}

Tabela nfs_qig_details:

{.align-center}

  • Crie ou duplique uma linha;
  • A coluna PANEL_QIG_SEQ_DB faz conexão com a tabela nfs_qig_panels (as chaves tem que ser a mesma);
  • Na coluna DISPLAY_NAME (que será o nome que aparece no menu lateral), adicione o nome adequado;
  • A coluna MAIN_TABLE_OPTIONS exibe as informações no cabeçalho da Tela de Detalhes;
  • A coluna SECONDARY_TABLE_OPTIONS exibe as informações no cabeçalho do bloco diário da Tela de Detalhes;
  • A coluna ADDITIONAL_TABLES exibe informações do apontamento do item dentro do bloco diário;
  • A coluna FILTERS permite criar um filtro dentro da Tela de Detalhes.

MAIN_TABLE_OPTIONS

{.align-center}

Exemplo de preenchimento:

{
	"TABLE": "EQP",
	"FILTER": "",
	"ORDER": "",
	"DISPLAY_FIELDS": [
		"CODIGO",
		"DESCRICAO",
		"EQP_CLASSE_FK",
		"EQP_MARCA_FK",
        "EQP_MODELO_FK"
	]
}

SECONDARY_TABLE_OPTIONS

{.align-center}

Exemplo de preenchimento:

{
	"TABLE": "EQP_BOLETIM",
	"FILTER": "FIM_DH IS NULL",
	"ORDER": "INI_DH DESC",
	"DISPLAY_FIELDS": [
		"INI_DH",
		"FIM_DH",
		"FUNCIONARIO_FK"
	]
}

ADDITIONAL_TABLES

{.align-center}

Exemplo de preenchimento:

{
	"TABLES": [{
		"NAME": "EQP_APT",
		"FILTER": "",
		"ORDER": "INI_DH ASC",
		"DISPLAY_FIELDS": [
			"FIM_DH",
			"INI_FIM_DIFF_STR",
			"HR_MARTE",
			"HR_MARTE_FIM",
			"MEDICAO_GE",
			"MEDICAO_GE_FINAL",
			"PERDA_FK",
			"EQP_OS_FK",
			"ENCARREGADO_FK",
			"FASE_CICLO_FK",
			"EQP_MATERIAL_FK"
		]
	},
	{
		"NAME": "EQP_APT_PRODUCAO",
		"FILTER": "",
		"ORDER": "INI_DH ASC",
		"DISPLAY_FIELDS": [
			"EQP_OS_FK",
			"PRODUCAO",
			"EQP_MATERIAL_FK"
		]
	}]
}

FILTERS

{.align-center}

Exemplo de preenchimento:

{
	"inicio": {
		"type": "Date",
		"options": {
			"label": "De",
			"required": false,
			"attr": {
				"data-filter": "EQP_BOLETIM.INI_DH"
			}
		}
	},
	"fim": {
		"type": "Date",
		"options": {
			"label": "Até",
			"required": false,
			"attr": {
				"data-filter": "EQP_BOLETIM.INI_DH"
			}
		}
	}
}

Tabela nfs_entry_point:

  • Crie ou duplique uma linha;
  • Na coluna FILE_OR_DOMAIN informe a qual domínio ele pertence (PANEL);
  • Na coluna ACTION adicione o nome que foi inserido na tabela nfs_qig_panels no campo NAME;
  • Na coluna CODE é inserido o código de execução dos valores apresentados nos quadros;
  • Na coluna CODE_TYPE informa a linguagem do código utilizado na coluna CODE.

CODE 

Exemplo de preenchimento:

$dadosPainel = $this->inputValues;
$countYM = 0;
$countPD = 0;
$countPDF = 0;

$mainTable = $this->defaultQigRawData['main_table'];

foreach ($dadosPainel['QII'] as $index => $qii) {

	$equipamento = $mainTable[$index][0];
 

	if($equipamento['STATUS'] == 'Y_M')
	{	
                $countYM++;
	}

	if($qii['SUBTITLE']['color'] == '#FFFF00')
	{
        		$countPD++;
	}
        else 
                $countPDF++;
             
	$qii['F4']['FIELD'] = 'Status: '.$equipamento['STATUS'];

	$dadosPainel['QII'][$index] = $qii;
}

$this->outputValues = $dadosPainel;
$this->indicatorsValues['Y_M'] = $countYM;
$this->indicatorsValues['COM_PD'] = $countPD;
$this->indicatorsValues['BOLETIM_FECHADO'] = $countPDF;

Indicadores por Entrypoint

Passa a ser possível criar indicadores usando entrypoints php, abaixo vamos ver como usar o recurso e configurá-lo.

CODE

Aqui temos um exemplo de entrypoint de indicadores, a estrutura é a mesma do JSON que é usado atualmente com Query Builder, mas agora em PHP.

/** Obtém os dados com o DAO */

/** Consulta 'main' que são os dados que serão exibidos no card */
$osIntegracaoMain = Dao::table('os', 'os')
    ->select([
        'COUNT(c.seq_db) AS porcentage',
        '180 AS value'
    ])
    ->innerJoin('os', 'status_os', 'st', 'st.SEQ_DB = os.STATUS_OS_SEQ_DB')
    ->innerJoin('os', 'cliente', 'c', 'c.SEQ_DB = os.CLIENTE_SEQ_DB')
    ->whereRaw('(os.PROC_ST = 0 OR os.PROC_ST IS NULL)')
    ->where('os.STATUS_OS_SEQ_DB', '=', 3)
    ->get();

/** Consulta 'data' para os dados do relatório */
$osIntegracaoData = Dao::table('os', 'os')
    ->select([
        'os.seq_db AS seq_db',
        'os.codigo AS Codigo',
        'c.DESCRICAO AS Cliente',
        'st.DESCRICAO AS Status'
    ])
    ->innerJoin('os', 'status_os', 'st', 'st.SEQ_DB = os.STATUS_OS_SEQ_DB')
    ->innerJoin('os', 'cliente', 'c', 'c.SEQ_DB = os.CLIENTE_SEQ_DB')
    ->whereRaw('(os.PROC_ST = 0 OR os.PROC_ST IS NULL)')
    ->where('os.STATUS_OS_SEQ_DB', '=', 3)
    ->get();

/**
 * Config do card
 */
$osIntegracao = [
    "card" => [
        "style" => [
            "cursor" => "pointer",
            "background-color" => "burlywood",
            "border" => "2px solid #000",
            "max-height" => "125px"
        ]
    ],
    "number" => [
        "style" => [
            "font-size" => "1.5em",
            "color" => "#000",
            "font-family" => "\"Open Sans\", sans-serif",
            "font-weight" => "bold"
        ]
    ],
    "icon" => [
        "style" => [
            "color" => "#E87E04",
            "opacity" => "0.3",
            "margin-top" => "11px",
            "margin-left" => "-42px",
            "transform" => "scale(2.5)"
        ],
        "class" => "fa-file"
    ],
    "statusText" => [
        "style" => [
            "color" => "#000"
        ]
    ],
    "progressBar" => [
        "style" => [
            "background-color" => "black"
        ]
    ],
    "label" => [
        "text" => "OS Abertas nos últimos 90 dias",
        "style" => [
            "font-size" => "1em",
            "text-decoration" => "none",
            "color" => "#000",
            "font-weight" => "bold",
            "font-family" => "\"Open Sans\", sans-serif"
        ]
    ],
    "color" => "yellow-gold",
    "type" => "progress",
    "porcentage" => [
        "title" => "OS em Andamento"
    ],
    /** Passa os dados obtidos na query */
    "display" => $osIntegracaoMain,
    "query" => [
        "data" => $osIntegracaoData
    ],
    "report" => [
        "displayColumns" => [
            "Codigo",
            "Cliente",
            "Status"
        ],
        "links" => [
            "Codigo" => "/t/os/edit/%seq_db%"
        ]
    ]
];

/** Obtém os dados com o DAO */

/** Consulta 'main' que são os dados que serão exibidos no card */
$mais12HorasMain = Dao::table('boletim', 'b')
    ->select([
        'COUNT(b.seq_db) AS result'
    ])
    ->whereRaw('b.INI_DH > DATE_SUB(NOW(), INTERVAL 180 DAY)')
    ->whereRaw('b.INI_FIM_DIFF_SEC >= (3600 * 12)')
    ->get();

/** Consulta 'data' para os dados do relatório */
$mais12HorasData = Dao::table('boletim', 'b')
    ->select([
        'b.SEQ_DB AS seq_db',
        'b.FUNCIONARIO_SEQ_DB AS funcionario_seq_db',
        "CONCAT(f.CRACHA, ' :: ', f.NOME) AS Funcionário",
        'b.INI_DH AS Início',
        'b.FIM_DH AS Término',
        "REPLACE(ROUND((b.INI_FIM_DIFF_SEC)/3600, 2), '.', ',') AS Total"
    ])
    ->innerJoin('b', 'funcionario', 'f', 'f.SEQ_DB = b.FUNCIONARIO_SEQ_DB')
    ->whereRaw('b.INI_DH > DATE_SUB(NOW(), INTERVAL 180 DAY)')
    ->whereRaw('b.INI_FIM_DIFF_SEC >= (3600 * 12)')
    ->get();

/**
 * Config do card
 */
$mais12Horas = [
    /** Config dos filtros */
    "filters" => [
        "entity" => [
            "field" => "funcionario_seq_db",
            "column_name" => "Funcionário",
            "column_pos" => 2
        ],
        "ini_date" => [
            "column_name" => "Início",
            "column_pos" => 3
        ],
        "fim_date" => [
            "column_name" => "Término",
            "column_pos" => 4
        ]
    ],
    "label" => [
        "text" => "Boletins com mais de 12 Horas",
        "style" => [
            "font-size" => "1.3em",
            "text-decoration" => "none",
            "color" => "#E87E04",
            "font-weight" => "bold",
            "font-family" => "\"Open Sans\", sans-serif"
        ]
    ],
    "color" => "yellow-gold",
    "card" => [
        "style" => [
            "cursor" => "pointer",
            "background-color" => "#FFF",
            "border" => "3px solid #E87E04"
        ]
    ],
    "number" => [
        "style" => [
            "font-size" => "2.5em",
            "text-align" => "right",
            "color" => "#E87E04",
            "font-family" => "\"Open Sans\", sans-serif",
            "font-weight" => "bold"
        ]
    ],
    "icon" => [
        "style" => [
            "color" => "#E87E04",
            "opacity" => "0.5",
            "margin-top" => "-70px",
            "margin-left" => "-40px",
            "transform" => "scale(0.3)"
        ],
        "class" => "fa-clock-o"
    ],
    /** Passa os dados obtidos na query */
    "display" => $mais12HorasMain,
    "query" => [
        "data" => $mais12HorasData
    ],
    "report" => [
        "displayColumns" => [
            "Funcionário",
            "Início",
            "Término",
            "Total"
        ],
        "links" => [
            "Funcionário" => "/details/1/%funcionario_seq_db%/1"
        ]
    ]
];

/** Array final com todos indicadores */
$finalArray = [
    "osIntegracao" => $osIntegracao,
    "mais12Horas" => $mais12Horas,
];

$this->queryData['finalArray'] = $finalArray;

Home

Para utilizar na home, um entrypoint deverá ser criado na tabela nfs_entrypoint com a seguinte configuração inicial.

FILE_OR_DOMAIN: 'SYSTEM' | ACTION: 'IndicatorHome' | ENTRY_NUM: 502 | CODETYPE: 'PHP'

Quando esse entrypoint estiver ativo, passa a ser ignorado o entrypoint da tabela nfs_rpo.

CRUD

Para o CRUD, teremos que usar o campo INDICATORS da tabela nfs_core_ds_tabela para fazer uma espécie de apontamento para qual entrypoint deverá ser chamado, esse apontamento é um JSON que indica qual EP será utilizado para carregar os indicadores.

{
	"entrypoint": true,
	"action": "indicadores_equipamento"
}

PANEL

Para o painel o comportamento é bem semelhante que o CRUD, na tabela nfs_qig_panels usaremos o campo INDICATORS_OPTIONS para fazer o apontamento, a estrutura é a mesma, e é possivel usar o mesmo entrypoint que o CRUD por exemplo.

{
	"entrypoint": true,
	"action": "indicadores_equipamento"
}

Filtros no relatório de indicador (entidade, e datas de início e fim)

É possível adicionar filtros nesse relatório, o filtro utiliza recursos da biblioteca datatables. Os filtros estão disponiveis nos indicadores tradicionais (Query Builder) ou na nova estrutura de indicadores por entrypoint.

Disponíveis filtros por entidade, e datas de início e fim.

Filtros

Sobre

Dentro da estrutura do indicador podemos adicionar uma nova propriedade chamada filters e nela vamos configurar os filtros. O valor no card exibido no relatório é atualizado conforme a quantidade de registros retornados.

Declarando os filtros

Usa-se o indice "entity", cujo os atributos são field que é o idenetificador da entidade, mais, column_name e column_pos. Para os filtros de data se faz necessário passar apenas os campos column_name e column_pos.

field que é campo que identifica a entidade.
column_name é o nome da coluna.
column_pos é localização da coluna na tabela.

  • Query Builder
  "mais12Horas": {
	"filters": {
		"entity": {
			"field": "funcionario_seq_db",
			"column_name": "Funcionário",
			"column_pos": 2
		},
		"ini_date": {
			"column_name": "Início",
			"column_pos": 3
		},
		"fim_date": {
			"column_name": "Termino",
			"column_pos": 4
		}
	},
    "label": {...}
  }
  • Entrypoint
$mais12Horas = [
    "filters" => [
        "entity" => [
            "field" => "funcionario_seq_db",
            "column_name" => "Funcionário",
            "column_pos" => 2
        ],
        "ini_date" => [
            "column_name" => "Início",
            "column_pos" => 3
        ],
        "fim_date" => [
            "column_name" => "Término",
            "column_pos" => 4
        ]
    ],
    [...]
]

O atributo field faz parte o filtro por entidade, ele que determina o campo que usaremos para identificar as entidades.

O atributo column_name é o nome dado a coluna.

E o atributo column_pos determina em qual coluna o campo está.

Configuração de Filtros na Tela de Indicadores (Filtros customizados)

NÃO possui compatibilidade com o filtro simples 'filters' acima.

Esta funcionalidade permite a configuração de filtros para refinar os dados exibidos na tela de Indicadores. Os filtros são definidos no objeto filters_custom, onde cada filtro possui uma column, um type e options específicas. Diferentemente do filters, o filters_custom permite criar qualquer tipo de filtro, definindo o operador lógico e o tipo de dado que será filtrado.

Tipos de Filtros

1. Filtro por Data (type: Date)

Permite filtrar registros com base em uma data específica. O input enviará para o backend um valor no formato brasileiro 'd/m/Y'. É importante realizar a conversão usando STR_TO_DATE e estar atento ao tipo de dado da coluna:

  • Caso a coluna seja do tipo DATETIME (contém hora), utilize DATE_ADD(..., INTERVAL 1 DAY) para garantir a inclusão de registros do mesmo dia.
  • Caso a coluna seja do tipo DATE (sem hora), a comparação direta com STR_TO_DATE(..., '%d/%m/%Y') é suficiente.

Exemplo de uso para intervalo de datas:

"ABERTURA_1": {
    "type": "Date",
    "options": {
        "label": "De",
        "sql-condition": "os.DATA_ABERTURA >= STR_TO_DATE(:ABERTURA_1, '%d/%m/%Y')"
    }
},
"ABERTURA_2": {
    "type": "Date",
    "options": {
        "label": "Até",
        "sql-condition": "os.DATA_ABERTURA < DATE_ADD(STR_TO_DATE(:ABERTURA_2, '%d/%m/%Y'), INTERVAL 1 DAY)"
    }
}
  • ABERTURA_1 (De): Retorna registros cuja data de abertura seja maior ou igual à data informada.
  • ABERTURA_2 (Até): Retorna registros cuja data de abertura seja menor ou igual ao final do dia informado (23:59:59), funcionando corretamente mesmo quando a coluna contém hora.

2. Filtro por Tabela (type: Table)

Utilizado para selecionar valores de uma tabela externa.

Exemplo:

"CLIENTE": {
    "type": "Table",
    "options": {
        "label": "Cliente",
        "sql-condition": "cli.SEQ_DB = :CLIENTE",
        "multiple": true
    }
},
  • Lógica: Permite selecionar múltiplos registros da tabela os para filtrar os dados.

3. Filtro por Escolha (type: Choice)

Permite selecionar um ou mais valores predefinidos.

Exemplo:

"OT": {
    "type": "Choice",
    "options": {
        "label": "Selecione um OT",
        "sql-condition": "cli.OT = :OT",
        "multiple": true,
        "choices": {
            "700000019622": "700000019622",
            "teste 2": "teste 2",
            "teste 3": "teste 3",
            "teste 4": "teste 4"
        }
    }
}
  • Lógica: O usuário pode selecionar um ou mais valores da lista predefinida. O operador = garante que apenas registros com os valores selecionados sejam retornados.

4. Filtro por Texto (type: Text)

Permite pesquisar registros por um campo textual específico.

Exemplo:

"NUMERO_CHECKLIST": {
    "type": "Text",
    "options": {
        "label": "Número do Checklist",
        "sql-condition": "apn.SEQ_DB_DEVICE = :NUMERO_CHECKLIST"
    }
}
  • Lógica: Retorna registros onde o valor da coluna CLIENTE seja exatamente igual ao informado pelo usuário.

Cada filtro pode ser configurado de acordo com a necessidade do usuário, garantindo maior flexibilidade na consulta de dados na tela de Indicadores.

Exemplo de relatorio completo: Coluna CODE da tabela nfs_entry_point

{
  "OSProspeccao": {
    "label": "OS's Programadas (Prospecção)",
    "color": "green-jungle",
    "icon": "fa fa-calendar",
    "display": "[result]",
    "queryBuilder": { ...
    },
    "report": {
      "displayColumns": [
        "OT",
        "ABERTURA",
        "TECNICO",
        "NUMERO_CHECKLIST",
        "EQUIPAMENTO",
        "CLIENTE"
      ],
      "links": {
        ...
      }
    },
    "filters_custom": {
      "ABERTURA_1": {
          "type": "Date",
          "options": {
              "label": "De",
              "sql-condition": "os.DATA_ABERTURA >= STR_TO_DATE(:ABERTURA_1, '%d/%m/%Y')"
          }
      },
      "ABERTURA_2": {
          "type": "Date",
          "options": {
              "label": "Até",
              "sql-condition": "os.DATA_ABERTURA < DATE_ADD(STR_TO_DATE(:ABERTURA_2, '%d/%m/%Y'), INTERVAL 1 DAY)"
          }
      },
      "OS": {
          "type": "Table",
          "options": {
              "label": "OT",
              "sql-condition": "os.SEQ_DB = :OS",
              "multiple": true
          }
      },
      "FUNCIONARIO": {
          "type": "Table",
          "options": {
              "label": "Técnico",
              "sql-condition": "fun.SEQ_DB = :FUNCIONARIO",
              "multiple": true
          }
      },
      "EQUIPAMENTO": {
          "type": "Table",
          "options": {
              "label": "Equipamento",
              "multiple": true,
              "sql-condition": "eqp.SEQ_DB = :EQUIPAMENTO"
          }
      },
      "CLIENTE": {
          "type": "Table",
          "options": {
              "label": "Cliente",
              "sql-condition": "cli.SEQ_DB = :CLIENTE",
              "multiple": true
          }
      },
      "NUMERO_CHECKLIST": {
          "type": "Text",
          "options": {
              "label": "Número do Checklist",
              "sql-condition": "apn.SEQ_DB_DEVICE = :NUMERO_CHECKLIST"
          }
      }
    }
  }
}

Integrações e Sincronismo

Power BI

Integração

Power BI

Power BI

Para integramos nosso token ao Power BI, devemos seguir os passos descritos em Configurando o ambiente e seguiremos os passos de Acesso Web com algumas alterações:

1. Configuração:

1.1 EntryPoint

Para este caso precisamos fazer 2 configurações:

  • Criar o ENTRY_POINT com FILE_OR_DOMAIN igual a POWERBI e com o CODETYPE igual a PHP, a action desse entryPoint será passada na route da config(core_par_parametros):
INSERT INTO nfs_entry_point (SEQ_DB, 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(287, 'POWERBI', 'aptPendenciaUltimas100', 9999, '9999', 9999, 9999, 9999, NULL, NULL, '$dao = Dao::table(''apontamento_pendencia'',''apt'')->limit(100)->orderBy(''apt.INI_DH'',''DESC'');
$result = $dao->get();
$this->outputValues = ArrayUtils::convertToCsv($result) ?? '';', 1, 26, 1, 'PHP', NULL, NULL, '', '', NULL, '');

Quando entryPoint a url poderá aceitar querys na url que poderão ser acessadas através do $this->inputValues no seu entryPoint.

Os dados serão retornados pela API através do $this->outputValues ;

Uma coisa muito importante na hora de montar o Entry Point é que o seu output será um Array e cada linha desse array será uma linha no resultado, exemplo:

Supondo que o seu resultado seja

 [ 
            [
                'ID' => 999,
                'FIELD_A' => 'LOREM',
                'FIELD_B' => 'IPSUM',
                'FIELD_C' => 'FOO;BAR',
                'FIELD_D' => 'FO,OB,AR',
                'FIELD_E' => 'ALGO|ALGO',
            ],
            [
                'ID' => 9999,
                'FIELD_A' => 'dolor "sit"',
                'FIELD_B' => 'AMET',
                'FIELD_C' => null,
                'FIELD_D' => '',
                'FIELD_E' => 0,
            ],
        ];

No caso o resultado do CSV vai ser:

            'ID|FIELD_A|FIELD_B|FIELD_C|FIELD_D|FIELD_E'.PHP_EOL.
            '999|"LOREM"|"IPSUM"|"FOO;BAR"|"FO,OB,AR"|"ALGO/ALGO"'.PHP_EOL.
            '9999|"dolor '."'sit'".'"|"AMET"||""|0'.PHP_EOL

Sendo a primeira linha as chaves do array, e as linhas seguintes cada item do do array.

  • Adicionar o entryPoint que criamos ao CONTEUDO do parâmetro ACESSO_POR_TOKEN(core_para_parametros):
{
    "EntryPoint: Ultimas 100 pendencias":{
        "route":"/nfs/api/powerbi/v1/aptPendenciaUltimas100"
    }
}

1.1.1 Parâmetros

Caso necessário podemos configurar parâmetros que poderão ser acessados através do $this->inputValues no entryPoint que criamos para consulta dos dados, isso deverá ser feito utilizando a propriedade "params" na configuração do nosso entryPoint exemplo:

{
  "Entry Fornecedor":{
      "route":"/nfs/api/powerbi/v1/powerBiFornecedor",
      "params":{
         "data_ini":{
            "type":"date",
			"format":"DD/MM/YYYY",
			"label":"Data inicial"
         },
         "data_fim":{
            "type":"date"
         },
         "seqDb":{
            type: "number"
         }
      }
   }
}

Por hora o ‘type’ dos parametro é baseados nos types do html e só o label pode ser configurado, alem do "format" para o type = "date" {.is-warning}

1.2 Tabela:

Podemos configurar a API para trazer os dados de uma tabela especifica sem que tenhamos que criar um Entry Point, para este caso basta adicionarmos a rota da powerBi com o caminho adicional /t/{tabela}, neste caso a consulta é limitada a 2000 registros caso a tabela seja MOBILE ou 5000 para as demais tabelas:

{
    "Tabela: Ultimos 2000 apontamentos pendencia":{
          "route":"/nfs/api/powerbi/v1/t/apontamento_pendencia"
    },
}

Utilizando a API

Para utilizarmos, basta geramos o token, validar o token através do e-mail, e copiarmos o link que estará disponível no perfil do usuário, segue um exemplo da utilização da API com o Insomnia: insominia.png

Testando no Power BI

insominia.png insominia.png insominia.png Troque pelo o dominio para o qual você configurou o token insominia.png

Neste ultimo passo, o usuario deve configurar as estruturas da tabela no powerBI

Integração

A integrão é uma tela específica criada a partir do CRUD para que seja possível realizar integração, onde o dado é alterado para um status final onde um sistema de terceiro obtenha esses dados, por exemplo, faturamento de horas, atividades trabalhadas e etc.

Pré Requisitos

Configuração na DS Tabela Campo

Ter feito a configuração na nfs_core_ds_tabela, nela definimos em qual tela (CRUD) que será realizada a integração, geralmente são nos dados que voltam para o cliente como: OS, Boletim, Apontamento etc.

O JSON a seguir deve ser configurado na coluna OPTIONS da tabela nfs_core_ds_tabela, no exemplo, vamos supor que a integração é feita na tabela de OS (APP_OS):

{
  "favorite_name": "OS", // Não faz parte da Config. de Integração
  "favorite": true, // Não faz parte da Config. de Integração
  "integration": [
    {
      "text": "Liberar OS para realizar nova integração com o sistema ERP",
      "icon_class": "fa fa-eject",
      "field_status": "PROC_ST",
      "field_description": "PROC_DESC",
      "field_date": "PROC_DH",
      "filter": { // Chave filter responsável por filtrar os dados que serão exibidos
        "field": "PROC_ST",
        "value": 4,
        "text": "Selecione a(s) OS(s) que já foram integradas e serão liberadas para novo processo de integração"
      },
      "action": { // Ações que vão ser executadas
        "updates": [
          {
            "field": "PROC_ST", // Coluna para ser alterada
            "value": 0 // Valor final da coluna
          },
          {
            "field": "RO",
            "value": 0
          },
          {
            "field": "PROC_REINTEGRACAO",
            "value": "1"
          },
          {
            "field": "PROC_DESC",
            "value": "OS foi liberada para novo processo de integração"
          },
          {
            "field": "PROC_DH",
            "value": "CURRENT_TIMESTAMP" // Constante dessa tela de integração para obter a data que foi feita integração
          }
        ],
        "text": "Mudar o status dos registros selecionados para 0",
        "text_button": "Executar processsamento",
        "error_message": "Erro ao atualizar",
        "success_message": "Valores atualizados com sucesso",
        "before_integration": { // Importante, é um status temporário usado no tipo batch e batchWithEntryPoint para filtrar somente os dados nesse status;
          "PROC_ST": 2,
          "PROC_DESC": "OS em processo de integração",
          "PROC_DH": "CURRENT_TIMESTAMP"
        }
      }
    }
  ],
  "icon": "fa fa-upload"  // Não faz parte da Config. de Integração
}

Field Action, botão que leva para integração

Para mostrar na tela o botão que vai enviar para integração de OS é necessário se utilizar de outra funcionalidade chamada Field Action, onde torna possível a adição de botões na listagem no CRUD e comumente usada na integraão

Configuração Field Action

INSERT INTO nfs_core_ds_field_action (`TABELA`, `DESCRICAO`, `CONFIG`,  `INS_DH`, `ATIVO`, `DELETED`, `NAME`) VALUES('OS', 'Ação de Integração de OS', '{
    "url": "/t/os?p1=proc_st&v1=4&c1=eq&v1r=Selecione a(s) OS(s) que já foram integradas e serão liberadas para novo processo de integração&integration=0&desc=''Teste''",
    "text": "Integração",
    "icon_class": "fa fa-eject",
    "button_class": "btn-lg btn-default ajaxify tooltips",
    "tooltip":"Liberar os para integração"
}', '2019-07-30 18:42:56.000', 1, 0, NULL);

O Importante é na URL no parâmetro integration, que vai pegar a primeira configuração na chave integration que configuramos na ds_tabela_campo, porque é possível ter mais de um integração, então se tivesse duas configurações uma para OS Faturamento e outra para OS Finalizada, no caso da OS Finalizada esse parâmetro iria mudar para ...&integration=1&desc='Teste 2'.

Tipos de Integração

A integração foi segmentada em três partes de acordo com uma análise interna. Para todos eles a chave importante que vai dentro da chave action é update_type: "batch|eachRow|batchWithEntryPoint"

Com excessão do tipo batchWithEntryPoint que vai ter uma chave a mais que é custom_entry_point, com o entrypoint a ser executado.

Batch

integracao_batch.png

O que já é usado em boas parte dos clientes e o PADRÃO, no caso o usuário executa uma ação na tela que altera todos os dados para um determinado status temporário configurado, depois o scheduler pega todo mundo que está com o status temporário e altera para o definitivo realizado todas as instruções necessárias, como rodar o entry point, salvar no histórico de alteração e etc. O seu objetivo é não travar a tela do usuário e executar no plano de fundo.

{
  "id-control": "fixed", // Não faz parte da Config. de Integração
  "id-value": 9999, // Não faz parte da Config. de Integração
  "menu_name": "Suportes (EIT)", // Não faz parte da Config. de Integração
  "integration": [
    {
      "text": "PCP Aprovar",
      "icon_class": "fa fa-upload",
      "field_status": "STATUS",
      "field_description": "PROC_DESC",
      "field_date": "PROC_DH",
      "filter": {
        "field": "sql_condition",
        "text": "PCP Aprovar",
        "value": " STATUS IN (2, 9)",
        "description": "Pendente"
      },
      "action": {
        "before_integration": {
          "STATUS": "12",
          "PROC_DESC": "Liberado para Aprovação do PCP",
          "PROC_DH": "NFS_CURRENT_TIMESTAMP"
        },
        "text": "PCP Aprovar",
        "text_button": "Aprovar",
        "error_message": "Erro ao atualizar",
        "success_message": "Valores atualizados com sucesso",
        "update_type": "batch" // Importante da configuração
      }
    }
  ]   
}

Tela de OS

  1. Na tela de OS é selecionados todos os itens que desejam ser integrados ou estornado com status da os com valor da PROC_ST definido no filter, definido na tela de integração:

Selecionar itens para enviar para integração com mais de uma condição

Também tem a opção de ter mais de uma condição para enviar para integração, o comum é enviar todos os registros com PROC_ST igual a 0, igual na nessa configuração:

"filter": {
  "field": "PROC_ST",
  "value": 0,
  "text": "Selecione a(s) OS(s) que já foram integradas e serão liberadas para novo processo de integração"
}

Porém há casos que deve haver mais de uma condição para isso utilize a seguinte configuração:

"filter": {
  "field": "sql_condition",
  "text": "Liberar boletim para integração",
  "value": " PROC_ST = 0 AND  FIM_DH IS NOT NULL ",
  "description" : "Não integrado e fechado"
}
  • sql_condition: É uma variável do NFS, usando ela o core vai identificar no value será passado uma condição.
  • description: É uma descrição das condições que vai aparecer junto com o botão que executa o processamento.
  • value: Outro ponto importante é que no value tem que passar uma condição para ser um usado no where para obter os registros que serão integrados, no caso PROC_ST = 0 AND FIM_DH IS NOT NULL.

Por padrão as variávies de integração são PROC_ST, PROC_DESC e PROC_DH, porém se em outro sistema ser diferente (igual BEVAP) é possível declarar-las da seguinte forma no JSON da coluna OPTIONS da nfs_core_ds_table:

"field_status" : "INTEGRA_ST",
"field_description": "INTEGRA_DESC",
"field_date" : "INTEGRA_DH",

A coluna OPTIONS que possui o json de configuração foi adicionada as seguintes opções dentro da chave action:

"before_integration": {
   "PROC_ST": 2,
   "PROC_DESC": "OS em processo de integração",
   "PROC_DH": "CURRENT_TIMESTAMP"
}
  1. Ao executar a ação todos os dados selecionados passaram para o status de integração configurando dentro do before_integration na chave PROC_ST conforme foi marcado no json abaixo, nisso também será salvo o usuário que fez essa ação, além do horário e uma descrição.

  2. O próximo passa a ser feito é a configuração de uma scheduler para fazer a integração de todos os dados que foram marcados com valor 9 na PROC_ST no passo anterior com o seguinte código na coluna CODE_ENTRY_POINT:

use scheduler\services\SchedulerIntegrationService;
// Tabela onde estao os dados para integracao
$tableName = 'os';
// Usuario da tabela de OS para ser salvo na execução da integracao
$columnUser = 'UPD_USUARIO_SEQ_DB' ;
// Valor temporario ques sera pego na tabela de OS para ser integrado
$status = 9;
$nameColumnStatus = "INTEGRA_ST";
SchedulerIntegrationService::makeIntegration($result, $tableName, $columnUser, $status, $nameColumnStatus);

Foi criado o método $makeIntegration na classe SchedulerIntegrationService para abstrair toda a lógica e complexidade na hora de configurar a rotina de integração no scheduler, ele tem os seguintes parâmetros:

  • $result - É um valor que sempre é necessário passar, é onde possui informações da empresa, filial, local da scheduler configurada.
  • $tableName - Nome da tabela onde é alterado o status para integração de terceiros.
  • $columnUser - A tabela configurada no item anterior deve possuir o seq_db do usuário que fez a integração.
  • $status - Valor do Status final para um sistema de terceiro buscar os dados no nosso sistema, por padrão é 2.
  • $nameColumnStatus - Nome da coluna que existe o status, como vimos no item 1 é possível que não seja PROC_ST, então deve ser passada nessa paramêtro;

Depois do scheduler configurado os dados vão ser obtidos de 100 em 100 com valor PROC_ST configurada dentro do before_integration e alterar para o valor final que é configurado dentro da chave updates no field PROC_ST e seu value que no exemplo é 0. Com isso ao salvar irá passar pelo processo de histório do usuário com essas alterações para que não seja perdido o seu histório, assim o Pentaho ou qualquer outro sistema de terceiro venha e capture esses dados para integração em seu sistema.

Primeiramente é os valores inseridos dentro de before_integration são alterados diretamente em cada registro. Já os valores inseridos dentro de updates, serão alterados somente se existir o scheduler com a função makeIntegration, são os valores:

  • CURRENT_USER_NAME: Nome atual do usuário
  • NFS_USER_EMAIL: E-mail do usuário logado no sistema
  • NFS_CURRENT_TIMESTAMP: Data atual do servidor
  • NFS_USER_TIMEZONE: Timezone do usuário

Each Row

integracao_eachrow.png

Um jeito mais simples, onde o usuário não precisa do scheduler porque os dados são poucos a serem integrados, a execução já acontece no momento da ação de integrar e já altera para o status final. Lembrando que é necessário configurar o a chave update dentro de action como está na documentação.

{
  "id-control": "fixed", // Não faz parte da Config. de Integração
  "id-value": 9999, // Não faz parte da Config. de Integração
  "menu_name": "Suportes (EIT)", // Não faz parte da Config. de Integração
  "integration": [
    {
      "text": "Limpar Programação ",
      "icon_class": "fa fa-upload",
      "field_status": "STATUS",
      "field_description": "PROC_DESC",
      "field_date": "PROC_DH",
      "filter": {
        "field": "sql_condition",
        "text": "Limpar Programação",
        "value": " STATUS IN (1, 4, 9)",
        "description": "Pendente"
      },
      "action": {
        "before_integration": {
          "STATUS": "11",
          "PROC_DESC": "Liberado para Reprogramação",
          "PROC_DH": "NFS_CURRENT_TIMESTAMP"
        },
        "text": "Limpar Programação",
        "text_button": "Limpar Programação",
        "error_message": "Erro ao atualizar",
        "success_message": "Valores atualizados com sucesso",
        "custom_entry_point": "reprogramacao_eng_suporte",
        "update_type": "eachRow"
      }
    }
  ]   
}

Batch With EntryPoint

integracao_batchwithep.png

Uma evolução do Batch, que ao invés de rodar um scheduler, é executado um custom_entry_point definido na action, o seu filer_or_domain vai ser igual a CUSTOM_ENTRY_POINT e o valor da coluna NOME que vai ser preenchida na configuração.

É necessário ativar uma parâmetro na nfs_core_par_parametros com o nome QUEUE_ENTRY_POINT e valor igual 1 e rodar o comando no Telegram para criar novos processamentos.

INSERT INTO nfs_core_par_parametros 
(EMPRESA, FILIAL, `LOCAL`, NOME, CONTEUDO, TIPO) 
VALUES (9999, 9999, 9999, 'QUEUE_ENTRY_POINT', '1', 1);

É criado processo para poder executar o entry point o mais rápido possível. Logo é enviado um processo para uma Fila no RabbitMQ para o Supervisor processar isso num processo isolado. Nome da Fila:

dominio_do_seu_cliente_entry_point

Que roda diretamente o entry que for configurado na integração e para isso foi criada um novo campo dentro do actions chamado custom_entry_point.

Exemplo:

{
  "id-control": "fixed", // Não faz parte da Config. de Integração
  "id-value": 9999, // Não faz parte da Config. de Integração
  "menu_name": "Suportes (EIT)", // Não faz parte da Config. de Integração
  "integration": [ 
    {
      "text": "Programar",
      "icon_class": "fa fa-upload",
      "field_status": "STATUS",
      "field_description": "PROC_DESC",
      "field_date": "PROC_DH",
      "filter": {
        "field": "sql_condition",
        "text": "Programar",
        "value": " STATUS IN (0, 7)",
        "description": "Pendente"
      },
      "action": {
        "before_integration": {
          "STATUS": "10",
          "PROC_DESC": "Liberado para Programação",
          "PROC_DH": "NFS_CURRENT_TIMESTAMP"
        },
        "text": "Programar",
        "text_button": "Programar",
        "error_message": "Erro ao atualizar",
        "success_message": "Valores atualizados com sucesso",
        "custom_entry_point": "programacao_eng_suporte", // Entry point chamado
        "update_type": "batchWithEntryPoint" // Tipo Importante
      }
    }
  ]   
}

Exemplo com os 3 tipos Juntos

Configuração na COLUNA OPTIONS da nfs_core_ds_tabela_campo

{
  "id-control": "fixed",
  "id-value": 9999,
  "menu_name": "Ordem de Serviço",
  "integration": [
    {
      "text": "Programar",
      "icon_class": "fa fa-upload",
      "field_status": "STATUS",
      "field_description": "PROC_DESC",
      "field_date": "PROC_DH",
      "filter": {
        "field": "sql_condition",
        "text": "Programar",
        "value": " STATUS IN (0, 7)",
        "description": "Pendente"
      },
      "action": {
        "before_integration": {
          "STATUS": "10",
          "PROC_DESC": "Liberado para Programação",
          "PROC_DH": "NFS_CURRENT_TIMESTAMP"
        },
        "text": "Programar",
        "text_button": "Programar",
        "error_message": "Erro ao atualizar",
        "success_message": "Valores atualizados com sucesso",
        "custom_entry_point": "programacao_eng_suporte",
        "update_type": "batchWithEntryPoint"
      }
    },
    {
      "text": "Limpar Programação ",
      "icon_class": "fa fa-upload",
      "field_status": "STATUS",
      "field_description": "PROC_DESC",
      "field_date": "PROC_DH",
      "filter": {
        "field": "sql_condition",
        "text": "Limpar Programação",
        "value": " STATUS IN (1, 4, 9)",
        "description": "Pendente"
      },
      "action": {
        "before_integration": {
          "STATUS": "11",
          "PROC_DESC": "Liberado para Reprogramação",
          "PROC_DH": "NFS_CURRENT_TIMESTAMP"
        },
        "text": "Limpar Programação",
        "text_button": "Limpar Programação",
        "error_message": "Erro ao atualizar",
        "success_message": "Valores atualizados com sucesso",
        "custom_entry_point": "reprogramacao_eng_suporte",
        "update_type": "eachRow"
      }
    },
    {
      "text": "Aprovar",
      "icon_class": "fa fa-upload",
      "field_status": "STATUS",
      "field_description": "PROC_DESC",
      "field_date": "PROC_DH",
      "filter": {
        "field": "sql_condition",
        "text": "Aprovar",
        "value": " STATUS IN (2, 9)",
        "description": "Pendente"
      },
      "action": {
        "before_integration": {
          "STATUS": "12",
          "PROC_DESC": "Liberado para Aprovação",
          "PROC_DH": "NFS_CURRENT_TIMESTAMP"
        },
        "text": "Aprovar",
        "text_button": "Aprovar",
        "error_message": "Erro ao atualizar",
        "success_message": "Valores atualizados com sucesso",
        "update_type": "batch"
      }
    }
  ]   
}

Queries do FIELD_ACITON e veja como é a Configuração Field Action porque também é necessário vincular ele a um grupo ou usuário para terem acesso.

INSERT INTO nfs_core_ds_field_action (`TABELA`, `DESCRICAO`, `CONFIG`,  `INS_DH`, `ATIVO`, `DELETED`, `NAME`) VALUES('OS', 'Ação de Programar OS', '{
    "url": "/t/os?p1=proc_st&v1=4&c1=eq&v1r=Selecione a(s) OS(s) que devem ser programadas&integration=0&desc=''Teste''",
    "text": "Programar",
    "icon_class": "fa fa-eject",
    "button_class": "btn-lg btn-default ajaxify tooltips",
    "tooltip":"Liberar os para integração"
}', '2019-07-30 18:42:56.000', 1, 0, NULL);


INSERT INTO nfs_core_ds_field_action (`TABELA`, `DESCRICAO`, `CONFIG`,  `INS_DH`, `ATIVO`, `DELETED`, `NAME`) VALUES('OS', 'Ação de Limpar', '{
    "url": "/t/os?p1=proc_st&v1=4&c1=eq&v1r=Selecione a(s) OS(s) para limpar sua programação&integration=0&desc=''Teste''",
    "text": "Limpar",
    "icon_class": "fa fa-eject",
    "button_class": "btn-lg btn-default ajaxify tooltips",
    "tooltip":"Liberar os para integração"
}', '2019-07-30 18:42:56.000', 1, 0, NULL);


INSERT INTO nfs_core_ds_field_action (`TABELA`, `DESCRICAO`, `CONFIG`,  `INS_DH`, `ATIVO`, `DELETED`, `NAME`) VALUES('OS', 'Ação de Aprovação de OS', '{
    "url": "/t/os?p1=proc_st&v1=4&c1=eq&v1r=Selecione a(s) OS(s) que já foram encerradas para aprovar sua integração&integration=0&desc=''Teste''",
    "text": "Aprovar",
    "icon_class": "fa fa-eject",
    "button_class": "btn-lg btn-default ajaxify tooltips",
    "tooltip":"Liberar os para integração"
}', '2019-07-30 18:42:56.000', 1, 0, NULL);

Login

Logo da empresa

Utiliza tabelas da base de dados nfs_cloud

A regra de apresentação é:

Se existir dados base64 ou http no campo nfs_hosts.HOST_LOGO, este conteúdo será apresentado quando a url (nfs_hosts.HOST) for acessada.

Se for criado o registro na nfs_empresas_imagens, preferencialmente usar o mesmo conteúdo da tabela nfs_hosts.HOST_LOGO

Preferencialmente usar os links http direcionando para imagens do site do próprio cliente, com isso evitamos críticas quanto ao direto de uso de imagens.

PRODUCT, MODULE E BUSINESS_MODULE do HOST

Informar no campo BUSINESS_MODULE (Módulo de Negócio) qual a configuração correta desse host.

Ex.:

  • Product (Produto): Smartos
SELECT x.* FROM nfs_cloud.nfs_products x
WHERE SEQ_DB = 1
  • Module (Modulos): Smartos (Génerico), Dealer e Crane
SELECT x.* FROM nfs_cloud.nfs_modules x
WHERE PRODUCT = 1
  • Business Module (Módulo de Negócio): SmartOS, Dealer, JD, CS, NH, CSNH, MDS, Fendt e Crane
SELECT x.* FROM nfs_cloud.nfs_business_modules x
WHERE MODULE in (SELECT SEQ_DB  FROM nfs_cloud.nfs_modules x WHERE PRODUCT = 1)

LOGIN Background

Tabela: nfs_cloud.nfs_empresas_imagens

A regra de exibição das imagens de background é de acordo com o business module do cliente que fica definido no nfs_cloud.nfs_hosts e associada a nfs_cloud.nfs_empresas_imagens. Coleta na seguinte ordem e só apresenta 1 opção:

1 - Se a coluna HOST estiver associada

2 - Quando preenche username/login e vai para o proximo campo e com o domínio (@simova.com.br - exemplo) na coluna DOMAIN

3 - Se a coluna BUSINESS_MODULE estiver preenchida

4 - Se a coluna MODULE estiver preenchida

5 - Se a coluna PRODUCT estiver preenchida

6 - Nenhuma opção anterior, então serão exibidas todas as imagens do registro com DOMINIO = "NFS"

Pode ser informada uma imagem existente no NFS, ou o base64 no formato de array.

Abaixo um exemplo de configuração do campo LOGIN_BACKGROUND da tabela nfs_cloud.nfs_empresas_imagens:

[
	"101",
	"102",
	"103",
	"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...HP7X51xc9lR1n5hAAAAAElFTkSuQmCC",
	"data:image/png;base64,iVBORw0KGqweqwwwSUhEUgAA...HP7X51xc9lR1n5hAAAAAElFTkSuQmCC",
	"data:image/png;base64,iVBORw0KGgoAAAAasdaEUgAA...HP7X51xc9lR1n5hAAAAAElFTkSuQmCC",
]

A primeira imagem do array é SEMPRE exibida primeiro e depois, se há mais de 4 imagens, é feita uma apresentação aleatória de todas as outras.

As imagens atuais disponíveis são:

100 101 102 103 104 105 106 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216

A partir da Versão 2.24.0 do NFS CORE é possível configurar a apresentação de novidades ao cliente fazer o acesso. No usuário agora tem a coluna SHOW_NEWS que pode ter os seguintes valores:

  • 0: Mostra a tela de novidades ao logar na filial/local
  • 1: Mostra novamente a tela de novidades, porém com a opção de não exibir mais
  • 2: Não mostra mais a tela de novidades ao logar, somente é possível acessar pelo menu do perfil

Para cadastrar um novidade basta ir no Cadastro de Notícias e preencher os seguintes campos:

  • Título: Será o titulo do texto.
  • Descrição: É uma descrição da novidade que vem logo abaixo do título.
  • Sequencial: É valor que representa a ordem das novidades deve iniciar em 1.
  • Detalhes: Objetivo do detalhes é por informações mais detalhadas do que da descrição.
  • Imagem: Todos os itens acima serão exibidos ao lado esquerdo, já a imagem será exibida ao lado direito e seu tamanho máximo deve ser de 1085 pixels de Largura e 815 pixels de Altura, sendo 1085 x 815.

Para deixar a mesmas novidades para todos de uma empresa, entre no banco e altere filial e local para 9999.

Informações de tabelas necessárias caso não exista pode ser encontrada no Jira NFS-540.

Marte

Ativar utilização do MARTE

[!IMPORTANT]

É obrigatório que o projeto tenha as tabelas EQUIPAMENTO e FUNCIONARIO declaradas no DS!

Incluir o parâmentro

INSERT INTO `nfs_core_par_parametros` (`EMPRESA`, `FILIAL`, `LOCAL`, `NOME`, `CONTEUDO`, `TIPO`)
VALUES (1, 9999, 9999, 'MARTE_ENABLED', '1', 1);

Executar o checkDS

O NFSCore criará as tabelas abaixo e a declaração destas no ds_tabela e ds_tabela_campo são feitas de forma automática:

MARTE
MARTE_REPORT_POS
MARTE_REPORT_AX
MARTE_IGNICAO
MARTE_TTS

A tabela MARTE tem um campo FK para a tabela EQUIPAMENTO! A tabela MARTE_IGNICAO tem FKs para as tabelas EQUIPAMENTO e FUNCIONARIO!

Se o projeto não tiver essas tabelas, falar com equipe dev NFS/CORE para conhecer as formas declaração dos campos VEICULO_EQUIPAMENTO e FUNCIONARIO!

O menu é criado automaticamente. Para fazer a liberação de acesso, criar um registro para o grupo correspondente e informar o MENU_SEQ_DB 10002.

EXEMPLO:

EMPRESA; FILIAL; LOCAL;  MENU_SEQ_DB;    SIUD;   GRUPO_SEQ_DB;  ATIVO;  DELETED
4;       9999;   9999;   10002;          1111;   2;             1;      0

As tabelas do MARTE são adicionadas automaticamente ao parâmetro "ignore" no filter do menu Cadastros. Se o nome ou tipo do menu (Cadastros/CRUD) for alterado essa condição de filter não será válida.

Configuração do Painel MARTE

Configuração do DS para uso do MARTE

As tabelas BOLETIM e APONTAMENTO que possuem dados do MARTE devem ter a declaração de "main_table" na coluna OPTIONS:

{
    "main_table": "EQP"
}

ou

{
    "main_table": "EQUIPAMENTO"
}

Com essa informação serão criados os seguintes campos na tabela de tipo APONTAMENTO:

HR
HR_MANUAL
HR_MARTE
HR_CAN
KM
KM_MANUAL
KM_MARTE
KM_CAN
HR_FIM
HR_FIM_MANUAL
HR_FIM_MARTE
HR_FIM_CAN
KM_FIM
KM_FIM_MANUAL
KM_FIM_MARTE
KM_FIM_CAN

Com essa informação serão criados os seguintes campos na tabela de tipo BOLETIM:

HR_INICIAL_MANUAL
HR_INICIAL_MARTE
HR_INICIAL_CAN

KM_INICIAL
KM_INICIAL_MANUAL
KM_INICIAL_MARTE
KM_INICIAL_CAN

HR_FINAL
HR_FINAL__MANUAL
HR_FINAL__MARTE
HR_FINAL__CAN

KM_FINAL
KM_FINAL__MANUAL
KM_FINAL__MARTE
KM_FINAL__CAN

Para o NFS fazer o processamento de forma automática é preciso colocar a configuração na nfs_core_par_parametros_mobile: Atenção para os campos INICIAL E FINAL quando é BOLETIM!

marte_param_mobile.png

O FIELD_XMOVA é nome do campo no JSON que vem do SimovaApps conténdo o JSON do MARTE.

Posição padrão - Marte


Se por algum motivo o módulo obter um posição indefinida, para não exibir o mesmo em um local desconhecido pode-se configurar posições padrões para cada local, filial ou empresa.

A configuração é feita na tabela core_par_parametros com a chave GIS_DEFAULT_POSITION e o conteúdo conforme exemplo.

Exemplo

{
 	"EMPRESA": {
		"1": {
 			"PLAT": -23.199681933867332,
 			"PLON": -45.891634424539085,
			"LABEL": " Falha ao obter dados GIS. (Empresa)"
		}		
 	},
	"FILIAL": {
		"2": {
 			"PLAT": -23.199681933867332,
 			"PLON": -45.891634424539085,
			"LABEL": " Falha ao obter dados GIS. (Filial)"
 		},
		"3": {
			"PLAT": -23.209345905867465,
 			"PLON": -45.90876913590114,
			"LABEL": " Falha ao obter dados GIS. (Filial)"
		}
 	},
	"LOCAL": {
		"3": {
			"PLAT": -23.205389513925493,
 			"PLON": -45.906072532338456,
			"LABEL": " Falha ao obter dados GIS. (Local)"
		}
	}
}

A posição é considerada indefinida quando:

  • Latitude e longitude estão 0.
  • Latitude ou longitude estão null.

Ao detectar o equipamento sem localização o NFS irá procurar se há uma posição padrão para o local do equipamento, se não houver irá procurar se há uma posição padrão para a filial, não encontrando posição padrão configurada na filial o mesmo irá procurar se a empresa tem uma posição padrão configurada, caso detectado a falta de configuração padrão o mesmo será exibido na Simova.

Visualmente no mapa, quando o equipamento pegar uma posição padrão veremos o seguinte detalhe:

marte_pos_padrao.png

Essa mensagem pode ser personalizada para cada posição padrão que for configurada, basta informar no campo LABEL no objeto de configuração. Sem o LABEL o sinal visual no painel lateral não será exibido.

Também um ícone informando algum problema com o equipamento será exibido no painel lateral.

marte_pos_padrao_side_.png

Verificar se existe as colunas na tabela nfs_qig_details, caso não exista pode ser inserido pelo sql abaixo:

ALTER TABLE `nfs_qig_details` ADD COLUMN `TEMPLATE` ENUM('1','2', '3') NULL DEFAULT NULL AFTER `PANEL_QIG_SEQ_DB`;
ALTER TABLE `nfs_qig_details` CHANGE COLUMN `TEMPLATE` `TEMPLATE` ENUM('1','2','3') NULL DEFAULT '1' AFTER `PANEL_QIG_SEQ_DB`;

Exemplo para configurar

Colunas com os seguintes valores:

EMPRESA: 9999
FILIAL: 9999
LOCAL: 9999
PANEL_QIG_SEQ_DB: 60
TEMPLATE: 2
DISPLAY_NAME: Ignições
SCRIPT_PHP:
DETAILS_TEMPLATE:
MAIN_TABLE_OPTIONS: {
  "TABLE": "EQP_VEIC",
  "FILTER": "",
  "ORDER": "",
  "DISPLAY_FIELDS": [
	"NOME"
  ]
}
SECONDARY_TABLE_OPTIONS:
ADDITIONAL_TABLES:
FILTERS: {
  "inicio": {
    "type": "Date",
    "options": {
      "label": "De",
      "required": false,
      "attr": {
        "data-filter": "BOLETIM.INI_DH"
      }
    }
  },
  "fim": {
    "type": "Date",
    "options": {
      "label": "Até",
      "required": false,
      "attr": {
        "data-filter": "BOLETIM.INI_DH"
      }
    }
  }
}
ENABLE: 1

Processo de Compilação de Dados de Telemetria

Nesse processos os dados de Telemetria são compilados e armazenados na tabela app_marte_telemetry_report, que é criada para os clientes que possuem a propriedade MARTE_ENABLED=1, porém é necessário executar o processo descrito no item Linha de Comando para o devido preenchimento e compilação dos dados para a tabela.

Junto com esse processo será gerado também um resumo diário do dispositivo marte. Este resumo será armazenado na tabela app_marte_telemetry_summary.

[!NOTE] Nesse processo os dados de Telemetria são cruzados com os dados de posicionamento registrados na tabela app_marte_report_pos.

Linha de Comando

Para executar a compilação usa-se a seguinte linha de comando:

$ php marte/nfs_marte_daily.php --host=<host-do-cliente>
  [--date=dd/mm/YYYY]
  [--marte=<MARTE_SEQ_DB>]
  [--geom=0||1]
  [--limit=200]
  [--debug=0||1]

Onde:

  • string host = cliente.simova.cloud: subdomínio do cliente;
  • string date = dd/mm/YYYY: data do arquivo de telemetria a processar;
  • int marte = SEQ_DB: SEQ_DB do dispositivo Marte, caso queira processar apenas os dados de um dispositivo;
  • int geom = 0 || 1: se os dados de geolocalização GIS/GEOM devem ser processados durante a compilação dos dados (padrão=0);
  • int limit = 200 || N: Número de registros de telemetria que serão processados por execução (padrão=200);
  • int debug = 0 || 1: exibir mais informações no (terminal/log) da execução do processo (padrão=0).

[!IMPORTANT]

  1. Apenas o parâmetro host é OBRIGATÓRIO;
  2. O limit só será aplicado se os parâmetros date e marte não forem especificados, definindo quantos registros de telemetria serão processados por execução;
  3. Se date for especificado processará apenas registros da data definida, do contrário serão processados os N Registros de Telemetria, definidos no limit, com status PROC_ST=0 (não processado);
  4. O uso da opção geom requer mais definições (Processos de Geolocalização GIS/GEOM) para processar os dados de Geolocalização GIS/GEOM;

[!TIP]

  1. Ao reprocessar registros de telemetria os dados atuais dessa data serão excluídos/sobrescrito;
  2. Para reprocessar basta executar especificando o parâmetro date da telemetria desejada.

Agendamento CRON:


0 6,15,23 * * * php /home/ubuntu/bitbucket-deploy/nfs/marte/nfs_marte_daily.php --host=suzanosa.simova.cloud --geom=1 --debug=1 >> /home/ubuntu/log-marte-suzanosa-daily.txt

[!TIP]
Os agendamentos atuais registram o resultado no arquivo de log especificado; este arquivo deve ser removido manualmente para preservar espaço em disco.

Cálculos Automáticos do Marte

Atualmente são previstos cálculos durante o processo de compilação, sendo eles Cálculo de Produção e Cálculo de Distância Percorridas, ambos em apontamentos produtivos.

Para isso, faz-se necessário o parâmetro MARTE_CALCULO:

{
  "KM": {
    // PRODUCAO ou KM
    "ENTIDADE": "EQP", // nome da tabela da entidade equipamento
    "BOLETIM": {
      // referências do boletim (atualmente não usada)
      "TABLE": "EQP_BOLETIM",
      "FLAG": "FLAG_KM_PROCESSADO"
    },
    "APONTAMENTO": {
      // referências do apontamento
      "TABLE": "EQP_APT", // nome da tabela de apontamento
      "FIELD": "MEDICAO_KM", // coluna na tabela de apontamentos para registrar o resultado do cálculo
      "TABLE_OPER": "OPER", // nome da tabela de operações do relacionamento com apontamentos
      "TABLE_OPER_FLAG": "FLAG_PRODUTIVA" // flag na tabela de operações que registra produtividade (0 || 1)
    }
  }
}

[!IMPORTANT]

Composição do Cálculo de Produção

  • Variáveis usadas no Cálculo de Produção:
$larguraFaixa = 0 || `Coluna LARGURA_FAIXA do Apontamento`;
$qtdLinha = 0 || `Coluna QTDE_LINHA do Apontamento`;
$faixaTrabalho = $larguraFaixa \* $qtdLinha;
# 10.000 metros quadrados => Hectare
$metrosLineares = 10000 / (1 || $faixaTrabalho);
  • Cálculo Efetivo:
$distancia = GisUtils::getDistanceInMeters($lastLat, $lastLon, $latitude, $longitude);
// calcula area
$area = $distancia \* $faixaTrabalho;
// calcular producao
$producal = $distancia / $metrosLineares;

Estrutura da Tabela app_marte_telemetry_report

CREATE TABLE `app_marte_telemetry_report` (
  -- colunas padrão do NFS para tabelas APP
  `MARTE_SEQ_DB` bigint(20) unsigned NOT NULL, -- SEQ_DB do dispositivo MARTE
  `DEVICE_NUM` int(11) DEFAULT NULL, -- Número do dispositivo
  `VEICULO_EQUIPAMENTO_SEQ_DB` bigint(20) unsigned DEFAULT NULL, -- SEQ_DB do equipamento
  `REFERENCE_SEQ_DB` bigint(20) unsigned NOT NULL, -- referência usada no processo
  `EVT_DH` timestamp NOT NULL, -- DH do evento MARTE
  `IGN_ON` tinyint(1) unsigned DEFAULT NULL, -- Ignição OFF || ON (0 || 1)
  `ENGINE_ON` tinyint(1) unsigned DEFAULT NULL, -- Morto OFF || ON (0 || 1)
  `RPM` smallint(5) unsigned DEFAULT NULL, -- RPM
  `SPEED` smallint(5) DEFAULT NULL, -- Velocidade
  `WORKSTATE` tinyint(1) unsigned DEFAULT NULL, -- WORKSTATE
  `HR_IGN` int(4) unsigned DEFAULT NULL, -- Horímetro ignição total
  `HR_MOTOR` int(4) unsigned DEFAULT NULL, -- Horímetro motor total
  `HR_AUX1` int(4) unsigned DEFAULT NULL, -- Horímetro AUX 1
  `HR_AUX2` int(4) unsigned DEFAULT NULL, -- Horímetro AUX 2
  `ODOMETRO_TRIP` decimal(10,2) DEFAULT NULL, -- Odômetro Viagem
  `ODOMETRO_TOTAL` decimal(10,2) DEFAULT NULL, -- Odômetro Total
  `ALTITUDE` smallint(5) DEFAULT NULL, -- Altitude registrada
  `TIL_SEC` int(4) unsigned DEFAULT NULL, -- Tempo de ignição em segundos
  `TED_SEC` int(4) unsigned DEFAULT NULL, -- Tempo em movimento em segundos
  `AUX_CT21` int(4) unsigned DEFAULT NULL, --
  `GPS_SPEED` smallint(5) unsigned DEFAULT NULL, -- Velocidade GPS
  `CAN_SPEED` smallint(5) unsigned DEFAULT NULL, -- Velocidade CAN
  `CAN_RPM` smallint(5) unsigned DEFAULT NULL, -- RPM CAN
  `CAN_TEMPERATURE` smallint(5) DEFAULT NULL, -- Temperatura CAN
  `CAN_HORIMETER` int(4) unsigned DEFAULT NULL, -- Horímetro CAN
  `CAN_ODOMETER` int(4) unsigned DEFAULT NULL, -- Odômetro CAN
  `CAN_ECONOMY` int(4) unsigned DEFAULT NULL, -- Economia CAN
  `CAN_FUEL` int(4) unsigned DEFAULT NULL, -- Nível de Combustível CAN
  `CAN_OIL_PRESSURE` int(4) unsigned DEFAULT NULL, -- Pressão do Óleo CAN
  `CAN_CT31` int(4) unsigned DEFAULT NULL, --
  `POSICAO_PLAT` decimal(12,8) DEFAULT NULL, -- Latitude registrado no evento
  `POSICAO_PLON` decimal(12,8) DEFAULT NULL, -- Longitude registrada no evento
  `POSICAO_GEOM` point DEFAULT NULL, -- GEOM registrado no evento
  `ACCELERATION` int(4) unsigned DEFAULT NULL, -- Aceleração
  `IND_IN_E0` tinyint(1) unsigned DEFAULT NULL,
  `IND_IN_E1` tinyint(1) unsigned DEFAULT NULL,
  `IND_IN_E2` tinyint(1) unsigned DEFAULT NULL,
  `IND_IN_E3` tinyint(1) unsigned DEFAULT NULL,
  `IND_IN_E4` tinyint(1) unsigned DEFAULT NULL,
  `IND_IN_E5` tinyint(1) unsigned DEFAULT NULL,
  `MAIN_POWER` decimal(5,2) DEFAULT NULL,
  `IN_HEX` varchar(2) DEFAULT NULL,
  `IN_BIN` char(8) DEFAULT NULL,
  `DEVICE_DH` timestamp NULL DEFAULT NULL, -- DH do dispositivo
  `EVT_GMT_DH` timestamp NULL DEFAULT NULL, -- DH do evento GMT
  `GIS_OBJECT_MASTER_SEQ_DB` bigint(20) unsigned DEFAULT NULL, -- Posição GIS Master
  `GIS_OBJECT_SUB_MASTER_SEQ_DB` bigint(20) unsigned DEFAULT NULL, -- Posição GIS Submaster (setor)
  `GIS_OBJECT_DETAIL_SEQ_DB` bigint(20) unsigned DEFAULT NULL, -- Posição GIS Detail (talhão)
  `GIS_ST` tinyint(4) DEFAULT '0', -- Status do processo GIS/GEOM
  `APONTAMENTO_SEQ_DB` bigint(20) unsigned DEFAULT NULL, -- Apontamento relacionado
  `DISTANCIA` int(11) DEFAULT '0', -- Resultado calculado Distância
  `AREA` decimal(8,4) DEFAULT '0.0000', -- Resultado calculada Área
  `PRODUCAO` decimal(8,4) DEFAULT '0.0000', -- Resultado calculado Produção
  /**
   * índices do sistema criados pelo core
   * e
   * índices para otimização do processo
   */
  PRIMARY KEY (`REFERENCE_SEQ_DB`,`MARTE_SEQ_DB`,`EVT_DH`),
  KEY `app_marte_telemetry_report_reference_gis_st_idx` (`REFERENCE_SEQ_DB`, `GIS_ST`) USING BTREE,
  KEY `app_marte_telemetry_report_gis_st_master_idx` (`GIS_ST`, `GIS_OBJECT_MASTER_SEQ_DB`) USING BTREE,
  KEY `app_marte_telemetry_report_efl_veiculo_idx` (`EMPRESA`, `FILIAL`, `LOCAL`, `VEICULO_EQUIPAMENTO_SEQ_DB`, `EVT_DH`) USING BTREE,
  KEY `app_marte_telemetry_report_apontamento_idx` (`APONTAMENTO_SEQ_DB`, `GIS_ST`) USING BTREE,
  KEY `app_marte_telemetry_report_equipamento_gis_evt_dh` (`VEICULO_EQUIPAMENTO_SEQ_DB`, `EVT_DH`, `GIS_ST`) USING BTREE,
  KEY `app_marte_telemetry_report_empresa_filial_gis` (`EMPRESA`, `FILIAL`, `GIS_ST`, `GIS_OBJECT_DETAIL_SEQ_DB`) USING BTREE,
  KEY `app_marte_telemetry_report_reference_gis_position_idx` (`REFERENCE_SEQ_DB`, `GIS_ST`, `POSICAO_GEOM`(25)) USING BTREE,
  KEY `app_marte_telemetry_report_empresa_idx` (`EMPRESA`, `FILIAL`, `LOCAL`, `GIS_ST`) USING BTREE,
  KEY `app_marte_telemetry_report_marte_idx` (`MARTE_SEQ_DB`, `EVT_DH`) USING BTREE,
  KEY `app_marte_telemetry_report_gis_st_submaster_idx` (`GIS_ST`, `GIS_OBJECT_SUB_MASTER_SEQ_DB`) USING BTREE,
  KEY `app_marte_telemetry_report_gis_st_detail_idx` (`GIS_ST`, `GIS_OBJECT_DETAIL_SEQ_DB`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC;

Processos de Geolocalização GIS/GEOM

[!CAUTION]

Para a execução do Processamento de dados de Geolocalização GIS/GEOM, usado atualmente apenas no Cliente Suzano, temos as seguintes observações:

  • Usa evento MySQL ev_gis_update na base do Cliente (para definição de posicionamento GEOM);
  • Colunas TALHAO_GIS_ST e TALHAO_GIS na tabela de Apontamento do processo;
  • Tabela app_gis_object preenchida devidamente.

Estrutura da Tabela app_marte_telemetry_summary

"CREATE TABLE `app_marte_telemetry_summary` (
  ...
  `MARTE_SEQ_DB` bigint unsigned NOT NULL, -- SEQ_DB do dispositivo Marte
  `DEVICE_NUM` int DEFAULT NULL, -- Número do dispositivo
  `VEICULO_EQUIPAMENTO_SEQ_DB` bigint unsigned NOT NULL, -- SEQ_DB do equipamento
  `TELEMETRY_SEQ_DB` bigint unsigned NOT NULL, -- SEQ_DB do registro de telemetria
  `MARTE_DATE` date NOT NULL, -- data da telemetria
  `TOTAL_TIME` int unsigned DEFAULT '0', -- tempo total de rastreamento
  `ENGINE_OFF_TIME` int unsigned DEFAULT '0', -- tempo total de motor desligado
  `ENGINE_ON_TIME` int unsigned DEFAULT '0', -- tempo total de motor ligado
  `MOVING_TIME` int unsigned DEFAULT '0', -- tempo total de deslocamento
  `IDLE_TIME` int unsigned DEFAULT '0', -- tempo total de motos ocioso
  `AVG_SPEED` decimal(5,2) DEFAULT '0.00', -- velocidade média durante o deslocamento
  `MAX_SPEED` smallint unsigned DEFAULT '0', -- velocidade máxima registrada
  `TOTAL_DISTANCE` decimal(8,2) unsigned DEFAULT '0.00', -- deslocamento total do dia
  `TOTAL_AREA` decimal(8,4) unsigned DEFAULT '0.0000', -- area total de operação
  `TOTAL_PRODUCAO` decimal(8,4) unsigned DEFAULT '0.0000', -- produção total do dia
  ...
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;",

[!IMPORTANT] Os cálculos de Área Total e Produção Total seguem as premissas descritas no item Cãlculos Automáticos do Marte.

Relatório Telemetria

Relatório criado pelo core para SUZANO MVI.

Relatório tem como base os dados da tabela marte_report_pos.

Sobre o relatório

Por não utilizar dos mesmos dados que a telemetria do NFS, o relatório pode ter variações nos montantes, pois esse relatório irá considerar dados entre o inicio do primeiro evento do dia, e o termino do último evento do dia.

  • Motor Ligado -> Com a ignição ON, é considerado motor ligado quando a velocidade for maior que 0, ou o deslocamento for maior que 0 ou que o RPM form maior ou igual a 300.
  • Motor Ocioso -> Com a ignição ON, é considerado ocioso quando a velocidade for 0 e o deslocamento também for 0, e o RPM deve estar maior que 300.
  • Motor Desligado -> Quando a ignição estiver OFF, ou nao se enquadrar como motor ligado ou ocioso.
  • Tempo em Deslocamento -> Com a ignição ON, e o deslocamento maior que 0.
  • Velocidade Máxima -> Maior velocidade atingida no dia.
  • Velocidade Média -> É obtida a partir do deslocamento total e o tempo em deslocamento.

Report

FILTERS

{
	"inicio": {
		"type": "Date",
		"options": {
			"label": "De",
			"required": true
		}
	},
	"fim": {
		"type": "Date",
		"options": {
			"label": "Até",
			"required": true
		}
	},
	"eqp": {
		"type": "Table",
		"options": {
			"label": "Equipamento",
			"multiple": true,
			"required": false
		}
	},
	"eqp_classe": {
		"type": "Table",
		"options": {
			"label": "Classe Operacional",
			"multiple": true,
			"required": false
		}
	}
}

QUERIES

{
	"equipamento_list":{
		"dump": "",
		"select":[
			"e.SEQ_DB"
		],
		"from":"eqp e",
		"where":[
			"e.SEQ_DB in (:eqp)"
		],
		"order_by": [
			["e.CODIGO","ASC"],
			["e.DESCRICAO","ASC"]
		]
	},
	"tele_marte":{
		"dump":"",
		"select":[
			"i.VEICULO_EQUIPAMENTO_SEQ_DB",
			"DATE_FORMAT(i.INI_DH,'%d/%m/%Y') DIA",
			"ROUND((SUM(i.VEL_MED_GPS)/COUNT(i.SEQ_DB)/5),2)MEDIA",
			"ROUND((MAX(i.VEL_MAX_GPS)),2)MAXIMA",
			"ROUND(sum(i.DISTANCIA_CALCULADA_PONTOS),2)DISTANCIA"
		],
		"from":"marte_ignicao i",
		"where":[
			"i.INI_DH BETWEEN :inicioIniDb and :fimFimDb",
			"i.VEICULO_EQUIPAMENTO_SEQ_DB in (:eqp)",
			"e.EQP_CLASSE_SEQ_DB in (:eqp_classe)"
		],
		"inner_join":[
			["i","eqp","e","i.VEICULO_EQUIPAMENTO_SEQ_DB = e.SEQ_DB"]
		],
		"group_by": [
			"i.VEICULO_EQUIPAMENTO_SEQ_DB",
			"DATE(i.INI_DH)"
		],
		"order_by": [
			["i.INI_DH","ASC"]
		],
		"group_result_by": "VEICULO_EQUIPAMENTO_SEQ_DB"
	}
}

TEMPLATE

{% extends 'reports/base.twig' %}
{% from 'reports/macros.twig' import img64 %}

{% block content %}
	<table class="table table-condensed" id="telemetriacore">
		<thead>
			<tr style="background-color:#BDBDBD;">
				<th colspan = "3" style="vertical-align: middle; text-align: left;">{{img64(images['suzano64'].image_base64,'50px')}}</th>
				<th colspan = "4" style="vertical-align: middle; text-align: left;font-size:30px"><strong>RELATÓRIO DE TELEMETRIA</strong></th>
				<th colspan = "2" style="vertical-align: middle; text-align: right;">
					Data/Hora Emissão: <strong>{{ 'now'|date('d/m/Y H:i:s') }}</strong><br>
					Período: <strong>{{ parameters.inicio }} à {{ parameters.fim }}</strong><br>
				</th>
			</tr>
			<tr style="background-color:#305496;">
				<th style="vertical-align: middle; text-align: center; border:1px solid #000000; color:white;">Eqp/Veic</th>
				<th style="vertical-align: middle; text-align: center; border:1px solid #000000; color:white;">Data</th>
				<th style="vertical-align: middle; text-align: center; border:1px solid #000000; color:white;">Tempo Motor Desligado</th>
				<th style="vertical-align: middle; text-align: center; border:1px solid #000000; color:white;">Tempo Motor Ligado</th>
				<th style="vertical-align: middle; text-align: center; border:1px solid #000000; color:white;">Tempo Em Deslocamento</th>
				<th style="vertical-align: middle; text-align: center; border:1px solid #000000; color:white;">Tempo Motor Ocioso</th>
				<th style="vertical-align: middle; text-align: center; border:1px solid #000000; color:white;">Velocidade Média (km/h)</th>
				<th style="vertical-align: middle; text-align: center; border:1px solid #000000; color:white;">Velocidade Máxima (km/h)</th>
				<th style="vertical-align: middle; text-align: center; border:1px solid #000000; color:white;">Distância (km)</th>
			</tr>
		</thead>
		<tbody>
			{%for e in equipamento_list%}
				{%set media = 0.0%}
				{%set maxima = 0.0%}
				{%set distancia = 0.0%}
				{%set total_desligado = 0.0%}
				{%set total_ocioso = 0.0%}
				{%set total_motor_ligado = 0.0%}
				{%set total_deslocamento = 0.0%}
				{%set total_geral = 0.0%}
				{%set total_media = 0.0%}
				{%set total_maxima = 0.0%}
				{%set total_distancia = 0.0%}
				{%set registros = 0%}
				{%set possuiApontamento = false%}

				{% for m in marte_pos %}
					{% if e.SEQ_DB == m.VEICULO_EQUIPAMENTO_SEQ_DB %}
						{%set possuiApontamento = true%}
						{%set total_geral = total_geral + m.TEMPO_TOTAL%}
						{%set total_desligado = total_desligado + m.MOTOR_DESLIGADO%}
						{%set total_ocioso = total_ocioso + m.OCIOSO%}
						{%set total_motor_ligado = total_motor_ligado + m.MOTOR_LIGADO%}
						{%set total_deslocamento = total_deslocamento + m.DESLOCAMENTO%}
						{%set total_media = total_media + m.AVGSPEED%}
						{%set total_maxima = total_maxima + m.MAXSPEED%}
						{%set total_distancia = total_distancia + m.TOTDESLOCAMENTO%}
						{%set registros = registros + 1%}

						<tr>
							<td style="vertical-align: middle; text-align: center; font-size:15px">{{m.EQUIPAMENTO}}</td>
							<td style="vertical-align: middle; text-align: center;font-size:15px">{{m.DATA}}</td>
							<td style="vertical-align: middle; text-align: center;font-size:15px">{{m.MOTOR_DESLIGADO|seconds_to_time}}</td>
							<td style="vertical-align: middle; text-align: center;font-size:15px">{{m.MOTOR_LIGADO|seconds_to_time}}</td>
							<td style="vertical-align: middle; text-align: center;font-size:15px">{{m.DESLOCAMENTO|seconds_to_time}}</td>
							<td style="vertical-align: middle; text-align: center;font-size:15px">{{m.OCIOSO|seconds_to_time}}</td>
							<td style="vertical-align: middle; text-align: center;font-size:15px">{{m.AVGSPEED}}</td>
							<td style="vertical-align: middle; text-align: center;font-size:15px">{{m.MAXSPEED}}</td>
							<td style="vertical-align: middle; text-align: center;font-size:15px">{{m.TOTDESLOCAMENTO}}</td>
						</tr>
					{% endif %}
				{% endfor %}

				{% if possuiApontamento %}
					<tr style="background-color:#F2F2F2;">
						<td style="vertical-align: middle; text-align: center;font-size:15px"><strong>Total Geral - h</strong></td>
						<td></td>
						<td style="vertical-align: middle; text-align: center;font-size:15px"><strong>{{total_desligado|seconds_to_time}}</strong></td>
						<td style="vertical-align: middle; text-align: center;font-size:15px"><strong>{{total_motor_ligado|seconds_to_time}}</strong></td>
						<td style="vertical-align: middle; text-align: center;font-size:15px"><strong>{{total_deslocamento|seconds_to_time}}</strong></td>
						<td style="vertical-align: middle; text-align: center;font-size:15px"><strong>{{total_ocioso|seconds_to_time}}</strong></td>
						{%if registros == 0 %}
							<td style="vertical-align: middle; text-align: center;font-size:15px"><strong>{{(0)|round(2)}}</strong></td>
							<td style="vertical-align: middle; text-align: center;font-size:15px"><strong>{{(0)|round(2)}}</strong></td>
						{%else%}
							<td style="vertical-align: middle; text-align: center;font-size:15px"><strong>{{(total_media / registros)|round(2)}}</strong></td>
							<td style="vertical-align: middle; text-align: center;font-size:15px"><strong>{{(total_maxima / registros)|round(2)}}</strong></td>
						{%endif%}
						<td style="vertical-align: middle; text-align: center;font-size:15px"><strong>{{total_distancia}}</strong></td>
					</tr>
				{% endif %}
			{%endfor%}
		</tbody>
	</table>
{% endblock %}

EXPORT_OPTIONS

{
	"pdf": {
		"orientation": "Landscape",
		"footer-right": "[title] - [page]/[topage]"
	},
	"data_table": {
		"output": "telemetriacore",
		"buttons": [
			{
				"extend": "excel",
				"text": "<i class='fa fa-file-excel-o'></i> Exportar para Excel",
				"className": "btn-sm btn-primary"
			},{
				"extend": "pdf",
				"text": "<i class='fa fa-file-pdf-o'></i> Exportar para PDF",
				"className": "btn-sm btn-primary"
			},{
				"extend": "copy",
				"text": "<i class='fa fa-clipboard'></i> Copiar para Clipboard",
				"className": "btn-sm btn-primary"
			},{
				"extend": "print",
				"text": "<i class='fa fa-print'></i> Imprimir",
				"className": "btn-sm btn-primary"
			}
		]
	},
	"images": ["suzano64"]
}

ENTRYPOINT

FILE_OR_DOMAIN

REPORTS

CODE

$parametros = $this->parameters;

$eqpSeqDb = $parametros['eqp'];
$ini = $parametros['inicioIniDb'];
$fim = $parametros['fimFimDb'];
$eqpClasseSeqDb = $parametros['eqp_classe'];

$qb = NFSQueryBuilder::select([
	"a.SEQ_DB",
	"a.EVT_DH INI_DH", 
	"a.IGN_ON", 
	"a.GPS_SPEED", 
	"a.VEICULO_EQUIPAMENTO_SEQ_DB", 
	"a.EVT_DH FIM_DH",
	"a.POSICAO_PLAT",
	"a.POSICAO_PLON",
	"a.RPM"
])
	->from('MARTE_REPORT_POS', 'a')
	->orderBy('a.VEICULO_EQUIPAMENTO_SEQ_DB', 'ASC')
	->addOrderBy('a.EVT_DH', 'ASC');

if (!empty($eqpSeqDb)) {
	$qb->andWhere("a.VEICULO_EQUIPAMENTO_SEQ_DB in (:seqDb)");
	$qb->setParameter('seqDb', $eqpSeqDb);
}

if (!empty($ini)) {
	$qb->andWhere("a.EVT_DH >= (:ini)");
	$qb->setParameter('ini', $ini);
}

if (!empty($fim)) {
	$qb->andWhere("a.EVT_DH <= (:fim)");
	$qb->setParameter('fim', $fim);
}

$rowsAll = $qb->get();
$results = [];

foreach ($rowsAll as $r) {
	
	$v_eqp = $r["VEICULO_EQUIPAMENTO_SEQ_DB"];
	$ini_dh = $r["INI_DH"];
	$seq = $r["SEQ_DB"];
	
    if (!isset($results[$v_eqp][$ini_dh])) {
        $results[$v_eqp][$ini_dh] = $r;
    } else {
        $current = $results[$v_eqp][$ini_dh];
        if ($current["SEQ_DB"] < $seq) {
            $results[$v_eqp][$ini_dh] = $r;
        }
    }
}

// Remover registros com mesmo evt_dh duplicados (por veículo_equip)
$rows = [];

foreach ($results as $group) {
    foreach ($group as $r) {
        $rows[] = $r;
    }
}

// transforma em um array não associativo
$rows = array_values($rows);

$equipamento = NFSQueryBuilder::select([
	"e.SEQ_DB", 
	"CONCAT(e.CODIGO, ' :: ', e.DESCRICAO) EQUIPAMENTO",
])
	->from('eqp', 'e')
	->orderBy('e.SEQ_DB', 'ASC');

if (!empty($eqpSeqDb)) {
	$equipamento->andWhere("e.SEQ_DB in (:seqDb)");
	$equipamento->setParameter('seqDb', $eqpSeqDb);
}

if (!empty($eqpClasseSeqDb)) {
	$equipamento->andWhere("e.EQP_CLASSE_SEQ_DB in (:classeSeqDb)");
	$equipamento->setParameter('classeSeqDb', $eqpClasseSeqDb);
}

$rowsEqp = $equipamento->get();

$valores = [];
$horaInicio = 0;
$horaFim = 0;
$data = "";
$eqp = 0;
$eqpDescricao = '';
$contador = 0;
$contadorSaida = 0;
$duracao = 0;
$tempoTotal = 0;
$tempoMotorDesligado = 0;
$tempoMotorLigado = 0;
$tempoEmDeslocamento = 0;
$tempoMotorOcioso = 0;
$numRegistros = count($rows);
$deslocamentoMetros = 0;
$totalDeslocamento = 0;
$maxSpeed = 0;
$latitudeFrom = '';
$longitudeFrom = '';
$latitudeTo = '';
$longitudeTo = '';
$newDate = true;
	
foreach ($rows as $key => $r) {
	$horaInicio = strtotime($r["INI_DH"]);
	$currentEqp = $r['VEICULO_EQUIPAMENTO_SEQ_DB'];
	
	if (isset($rows[($contador + 1)])) {
		$horaInicioNextEqp = strtotime($rows[($contador + 1)]['INI_DH']);
		$nextEqp = $rows[($contador + 1)]['VEICULO_EQUIPAMENTO_SEQ_DB'];
	}
		
	
	if (isset($nextEqp)) {
		if ($eqp == 0 || $eqp == $nextEqp) {
			if ($eqp == 0) {
				$eqp = $currentEqp; // recebeu seqdb do equipamento first
				foreach ($rowsEqp as $re) {
					if ($nextEqp == $re['SEQ_DB']) {
						$eqpDescricao = $re['EQUIPAMENTO']; // apenas pega a descrição da tabela equipamento
						break;
					}
				}
			}
			
			if ($data == "" || $data == date("d/m/Y", $horaInicioNextEqp)) {
				if ($data == "" || $newDate == true) { // primeiro do relatório
					$data = date("d/m/Y", $horaInicio);
					$hora = date("H:i:s", $horaInicio);
					
					list($horas, $minutos, $segundos) = explode(':', $hora);
					$timeBefore = $horas * 3600 + $minutos * 60 + $segundos;
					$tempoMotorDesligado = $timeBefore;
					$newDate = false;
				}
			} else if (date("d/m/Y", $horaFim) != date("d/m/Y", $horaInicio)) {
				// só zerar pois se trata da transição	
				$tempoTotal = 0;
				$tempoMotorDesligado = 0;
				$tempoMotorLigado = 0;
				$tempoEmDeslocamento = 0;
				$tempoMotorOcioso = 0;
				$maxSpeed = 0;
				$data = date("d/m/Y", $horaInicioNextEqp);
				
			} else {			
				$today = date("Y-m-d");
				$day = date("Y-m-d", $horaFim);
				
				if ($today > $day) {
					list($horas, $minutos, $segundos) = explode(':', date("H:i:s", $horaFim));
					$timeAfter = $horas * 3600 + $minutos * 60 + $segundos;

					$fimDoDia = 23 * 3600 + 59 * 60 + 59;
					$tempoRestante = $fimDoDia - $timeAfter;				
					$tempoMotorDesligado = $tempoMotorDesligado + $tempoRestante;
				}				
				
				$valores[$contadorSaida]["VEICULO_EQUIPAMENTO_SEQ_DB"] = $eqp;
				$valores[$contadorSaida]["EQUIPAMENTO"] = $eqpDescricao;
				$valores[$contadorSaida]["DATA"] = $data;
				$valores[$contadorSaida]["TEMPO_TOTAL"] = $tempoTotal;
				$valores[$contadorSaida]["MOTOR_DESLIGADO"] = $tempoMotorDesligado;
				$valores[$contadorSaida]["MOTOR_LIGADO"] = $tempoMotorLigado;
				$valores[$contadorSaida]["DESLOCAMENTO"] = $tempoEmDeslocamento;
				$valores[$contadorSaida]["OCIOSO"] = $tempoMotorOcioso;
				$valores[$contadorSaida]["MAXSPEED"] = $maxSpeed;
				$valores[$contadorSaida]["TOTDESLOCAMENTO"] = round(($totalDeslocamento / 1000),2);
				
				if ($totalDeslocamento > 0 && $tempoEmDeslocamento > 0) {
					$avgSpeed = round(($totalDeslocamento / 1000) / ($tempoEmDeslocamento / 3600), 2);
				} else {
					$avgSpeed = 0;
				}

				$valores[$contadorSaida]["AVGSPEED"] = $avgSpeed;
				
				$contadorSaida++;

				$tempoTotal = 0;
				$tempoMotorDesligado = 0;
				$tempoMotorLigado = 0;
				$tempoEmDeslocamento = 0;
				$tempoMotorOcioso = 0;
				$maxSpeed = 0;
				$totalDeslocamento = 0;
				$newDate = true;
				$data = date("d/m/Y", $horaInicioNextEqp);
			} 
			
		} else {			
			$today = date("Y-m-d");
			$day = date("Y-m-d", $horaFim);

			if ($today > $day) {
				list($horas, $minutos, $segundos) = explode(':', date("H:i:s", $horaFim));
				$timeAfter = $horas * 3600 + $minutos * 60 + $segundos;

				$fimDoDia = 23 * 3600 + 59 * 60 + 59;
				$tempoRestante = $fimDoDia - $timeAfter;				
				$tempoMotorDesligado = $tempoMotorDesligado + $tempoRestante;
			}
			
			$valores[$contadorSaida]["VEICULO_EQUIPAMENTO_SEQ_DB"] = $eqp;
			$valores[$contadorSaida]["EQUIPAMENTO"] = $eqpDescricao;
			$valores[$contadorSaida]["DATA"] = $data;
			$valores[$contadorSaida]["TEMPO_TOTAL"] = $tempoTotal;
			$valores[$contadorSaida]["MOTOR_DESLIGADO"] = $tempoMotorDesligado;
			$valores[$contadorSaida]["MOTOR_LIGADO"] = $tempoMotorLigado;
			$valores[$contadorSaida]["DESLOCAMENTO"] = $tempoEmDeslocamento;
			$valores[$contadorSaida]["OCIOSO"] = $tempoMotorOcioso;
			$valores[$contadorSaida]["MAXSPEED"] = $maxSpeed;
			$valores[$contadorSaida]["TOTDESLOCAMENTO"] = round(($totalDeslocamento / 1000),2);
			
			if ($totalDeslocamento > 0 && $tempoEmDeslocamento > 0) {
				$avgSpeed = round(($totalDeslocamento / 1000) / ($tempoEmDeslocamento / 3600), 2);
			} else {
				$avgSpeed = 0;
			}
		
			$valores[$contadorSaida]["AVGSPEED"] = $avgSpeed;
			
			$contadorSaida++;

			$tempoTotal = 0;
			$tempoMotorDesligado = 0;
			$tempoMotorLigado = 0;
			$tempoEmDeslocamento = 0;
			$tempoMotorOcioso = 0;
			$maxSpeed = 0;
			$totalDeslocamento = 0;
			$newDate = true;
			$data = date("d/m/Y", $horaInicio);	
			foreach ($rowsEqp as $re) {
				if ($nextEqp == $re['SEQ_DB']) {
					$eqpDescricao = $re['EQUIPAMENTO'];
					break;
				}
			}
		}
	} else {
		if ($eqp == 0 || $eqp == $curEqp) {
			
		
			if ($eqp == 0) {
				$eqp = $currentEqp; // recebeu seqdb do equipamento first
				foreach ($rowsEqp as $re) {
					if ($currentEqp == $re['SEQ_DB']) {
						$eqpDescricao = $re['EQUIPAMENTO']; // apenas pega a descrição da tabela equipamento
						break;
					}
				}
			}
			if ($data == "" || $data == date("d/m/Y", $horaInicio)) {
				if ($data == "") {
					$data = date("d/m/Y", $horaInicio);
				}
			} else {
				$valores[$contadorSaida]["VEICULO_EQUIPAMENTO_SEQ_DB"] = $eqp;
				$valores[$contadorSaida]["EQUIPAMENTO"] = $eqpDescricao;
				$valores[$contadorSaida]["DATA"] = $data;
				$valores[$contadorSaida]["TEMPO_TOTAL"] = $tempoTotal;
				$valores[$contadorSaida]["MOTOR_DESLIGADO"] = $tempoMotorDesligado;
				$valores[$contadorSaida]["MOTOR_LIGADO"] = $tempoMotorLigado;
				$valores[$contadorSaida]["DESLOCAMENTO"] = $tempoEmDeslocamento;
				$valores[$contadorSaida]["OCIOSO"] = $tempoMotorOcioso;
				$valores[$contadorSaida]["MAXSPEED"] = $maxSpeed;
				$valores[$contadorSaida]["TOTDESLOCAMENTO"] = round(($totalDeslocamento / 1000),2);
				
				if ($totalDeslocamento > 0 && $tempoEmDeslocamento > 0) {
					$avgSpeed = round(($totalDeslocamento / 1000) / ($tempoEmDeslocamento / 3600), 2);
				} else {
					$avgSpeed = 0;
				}

				$valores[$contadorSaida]["AVGSPEED"] = $avgSpeed;
				
				$contadorSaida++;

				$tempoTotal = 0;
				$tempoMotorDesligado = 0;
				$tempoMotorLigado = 0;
				$tempoEmDeslocamento = 0;
				$tempoMotorOcioso = 0;
				$maxSpeed = 0;
				$totalDeslocamento = 0;
				$data = date("d/m/Y", $horaInicio);
			} 
			
		} else {
			$valores[$contadorSaida]["VEICULO_EQUIPAMENTO_SEQ_DB"] = $eqp;
			$valores[$contadorSaida]["EQUIPAMENTO"] = $eqpDescricao;
			$valores[$contadorSaida]["DATA"] = $data;
			$valores[$contadorSaida]["TEMPO_TOTAL"] = $tempoTotal;
			$valores[$contadorSaida]["MOTOR_DESLIGADO"] = $tempoMotorDesligado;
			$valores[$contadorSaida]["MOTOR_LIGADO"] = $tempoMotorLigado;
			$valores[$contadorSaida]["DESLOCAMENTO"] = $tempoEmDeslocamento;
			$valores[$contadorSaida]["OCIOSO"] = $tempoMotorOcioso;
			$valores[$contadorSaida]["MAXSPEED"] = $maxSpeed;
			$valores[$contadorSaida]["TOTDESLOCAMENTO"] = round(($totalDeslocamento / 1000),2);

			if ($totalDeslocamento > 0 && $tempoEmDeslocamento > 0) {
				$avgSpeed = round(($totalDeslocamento / 1000) / ($tempoEmDeslocamento / 3600), 2);
			} else {
				$avgSpeed = 0;
			}
		
			$valores[$contadorSaida]["AVGSPEED"] = $avgSpeed;
			
			$contadorSaida++;

			$tempoTotal = 0;
			$tempoMotorDesligado = 0;
			$tempoMotorLigado = 0;
			$tempoEmDeslocamento = 0;
			$tempoMotorOcioso = 0;
			$maxSpeed = 0;
			$totalDeslocamento = 0;
			$data = date("d/m/Y", $horaInicio);	
			foreach ($rowsEqp as $re) {
				if ($currentEqp == $re['SEQ_DB']) {
					$eqpDescricao = $re['EQUIPAMENTO'];
					break;
				}
			}
		}
	}
	
	if ($contador < ($numRegistros - 1) && $eqp == $rows[($contador + 1)]["VEICULO_EQUIPAMENTO_SEQ_DB"]) {
		$horaFim = strtotime($rows[($contador + 1)]["INI_DH"]);
		$horaFim_ = $rows[($contador + 1)]["INI_DH"];
	} else {
		
		$horaFim = strtotime($r["INI_DH"]);
		$horaFim_ = $r["INI_DH"];
	}
	
	if (date("d/m/Y", $horaInicio) == date("d/m/Y", $horaFim)) {
		$duracao = floatval($horaFim - $horaInicio);
	} else {
		$duracao = 0;
	}
	
	if ($horaInicio == $horaFim) {
		$r["GPS_SPEED"] = 0;
	}

	//ver se houve deslocamento (new)
	if ($contador < ($numRegistros - 1)) {
		if ($r['VEICULO_EQUIPAMENTO_SEQ_DB'] == $rows[($contador + 1)]['VEICULO_EQUIPAMENTO_SEQ_DB']) {
			$latitudeFrom = $r["POSICAO_PLAT"];
			$longitudeFrom = $r["POSICAO_PLON"];

			$latitudeTo = $rows[$contador + 1]["POSICAO_PLAT"];
			$longitudeTo = $rows[$contador + 1]["POSICAO_PLON"];

			$rad = M_PI / 180;
			// Calculate distance from latitude and longitude
			$theta = $longitudeFrom - $longitudeTo;
			$dist = sin($latitudeFrom * $rad) * sin($latitudeTo * $rad) + cos($latitudeFrom * $rad) * cos($latitudeTo * $rad) * cos($theta * $rad);

			//transformando km em metro
			$kms = acos($dist) / $rad * 60 * 1.853;
			$deslocamentoMetros = (int)($kms * 1000);
		} else {
			$latitudeFrom = $r["POSICAO_PLAT"];
			$longitudeFrom = $r["POSICAO_PLON"];

			$latitudeTo = $r["POSICAO_PLAT"];
			$longitudeTo = $r["POSICAO_PLON"];

			$rad = M_PI / 180;
			// Calculate distance from latitude and longitude
			$theta = $longitudeFrom - $longitudeTo;
			$dist = sin($latitudeFrom * $rad) * sin($latitudeTo * $rad) + cos($latitudeFrom * $rad) * cos($latitudeTo * $rad) * cos($theta * $rad);

			//transformando km em metro
			$kms = acos($dist) / $rad * 60 * 1.853;
			$deslocamentoMetros = (int)($kms * 1000);
		}	
			
	} else {
		$latitudeFrom = $r["POSICAO_PLAT"];
		$longitudeFrom = $r["POSICAO_PLON"];

		$latitudeTo = $r["POSICAO_PLAT"];
		$longitudeTo = $r["POSICAO_PLON"];

		$rad = M_PI / 180;
		// Calculate distance from latitude and longitude
		$theta = $longitudeFrom - $longitudeTo;
		$dist = sin($latitudeFrom * $rad) * sin($latitudeTo * $rad) + cos($latitudeFrom * $rad) * cos($latitudeTo * $rad) * cos($theta * $rad);

		//transformando km em metro
		$kms = acos($dist) / $rad * 60 * 1.853;
		$deslocamentoMetros = (int)($kms * 1000);
	}
	
	if (date("d/m/Y", $horaInicio) != date("d/m/Y", $horaFim)) {
		$deslocamentoMetros = 0;
	}
	
	if ($r["IGN_ON"] == "0") {
		$tempoMotorDesligado += $duracao;
	} else {		
		if ($r["GPS_SPEED"] <= "0" && $deslocamentoMetros <= "0" && $r["RPM"] >= 300) {		
			$tempoMotorOcioso += $duracao;
			$tempoMotorLigado += $duracao;
			
		} else if ($r["GPS_SPEED"] > "0" || $deslocamentoMetros > "0") {
			$tempoEmDeslocamento += $duracao;
			$tempoMotorLigado += $duracao;
			
			if ($r["GPS_SPEED"] > 0) {
				if ($maxSpeed < $r["GPS_SPEED"])
				$maxSpeed = $r["GPS_SPEED"];
			}
			
			if ($deslocamentoMetros > 0) {	
				$totalDeslocamento += $deslocamentoMetros;
			} 
		} else {
			$tempoMotorDesligado += $duracao;
		}
	}
	
	$tempoTotal += $duracao;
	
	if ($contador == ($numRegistros - 1)) {
		
		$today = date("Y-m-d");
		$day = date("Y-m-d", $horaFim);

		if ($today > $day) {
			list($horas, $minutos, $segundos) = explode(':', date("H:i:s", $horaFim));
			$timeAfter = $horas * 3600 + $minutos * 60 + $segundos;

			$fimDoDia = 23 * 3600 + 59 * 60 + 59;
			$tempoRestante = $fimDoDia - $timeAfter;				
			$tempoMotorDesligado = $tempoMotorDesligado + $tempoRestante;
		}
		
		$valores[$contadorSaida]["VEICULO_EQUIPAMENTO_SEQ_DB"] = $eqp;
		$valores[$contadorSaida]["EQUIPAMENTO"] = $eqpDescricao;
		$valores[$contadorSaida]["DATA"] = $data;
		$valores[$contadorSaida]["TEMPO_TOTAL"] = $tempoTotal;
		$valores[$contadorSaida]["MOTOR_DESLIGADO"] = $tempoMotorDesligado;
		$valores[$contadorSaida]["MOTOR_LIGADO"] = $tempoMotorLigado;
		$valores[$contadorSaida]["DESLOCAMENTO"] = $tempoEmDeslocamento;
		$valores[$contadorSaida]["OCIOSO"] = $tempoMotorOcioso;
		$valores[$contadorSaida]["MAXSPEED"] = $maxSpeed;
		$valores[$contadorSaida]["TOTDESLOCAMENTO"] = round(($totalDeslocamento / 1000),2);		
		
		if ($totalDeslocamento > 0 && $tempoEmDeslocamento > 0) {
			$avgSpeed = round(($totalDeslocamento / 1000) / ($tempoEmDeslocamento / 3600), 2);
		} else {
			$avgSpeed = 0;
		}
		
		$valores[$contadorSaida]["AVGSPEED"] = $avgSpeed;

	}
	
	if (isset($rows[($contador + 1)]['VEICULO_EQUIPAMENTO_SEQ_DB'])) {
		$eqp = $rows[($contador + 1)]['VEICULO_EQUIPAMENTO_SEQ_DB'];
	} else {
		$eqp = $r['VEICULO_EQUIPAMENTO_SEQ_DB'];
	}	
	$contador++;
}

$this->queryData['marte_pos'] = $valores;

Last Updated

  • Jira: https://simova.atlassian.net/browse/NFSCORE-1165

NFS API

O objetivo da NFS Api é fornecer uma API configurável do formato dos dados do NFS, então é possível realizar o SINCRONISMO onde obtém os dados em JSON e transforma para a ESTRUTURA DO NFS inserindo ou atualizado os dados. A mesma coisa acontece na INTEGRAÇÃO que a partir de um FILTRO obtém os dados no formato do NFS e transforma para JSON.

Fluxo da API

Nessa sessão vamos mostrar o fluxo dos dados, que são praticamente dois a Sincronismo (Dados que vem para o NFS) e Integração (Dados gerados dentro do NFS e disponibilizados).

nfs_api_flow.png

Agente de Sincronismo

  1. Realiza autenticação (Ver mais em Autenticação)
  2. Retorna um Token para ser enviado no header de comunicação que comprava a válidade e autenticidade dos dados.
  3. Envia para o NFS os dados a serem sincronizados e o token no header.
  4. Etapa que acontece dentro do NFS representada de maneira macro: 4.1. Recebe os dados no formato JSON 4.2. Busca pelo EntryPoint que tem o de/para das chaves do json para as colunas do NFS 4.3. Após realizar o de/para os dados ficam no tipo que é legível para o NFS 4.4. Insere/Atualiza os dados no banco de dados 4.5. Retorna o resultado no formato do NFS 4.6. Retorna o uma mensagem de sucesso/validação/erro para camada de apresentação da API
  5. Retorna um JSON com a mensagem num formato padronizado de sucesso/validação/erro

Agente de Integração

  1. Realiza autenticação (Ver mais em Autenticação)
  2. Retorna um Token para ser enviado no header de comunicação que comprava a válidade e autenticidade dos dados.
  3. Envia para o NFS um filtro em JSON para obter os dados e o token no header.
  4. Etapa que acontece dentro do NFS representada de maneira macro: 4.1. Recebe os dados no formato JSON 4.2. Busca pelo EntryPoint que tem o de/para das chaves do json para as colunas do NFS 4.3. Após realizar o de/para os dados ficam no tipo que é legível para o NFS 4.4. Insere/Atualiza os dados no banco de dados 4.5. Retorna o resultado no formato do NFS 4.6. Retorna o uma mensagem de sucesso/validação/erro para camada de apresentação da API
  5. Retorna um JSON com um array com os dados.

Programas externos para tests

Para nossos testes e exemplos estamos usando o curl

Como usar curl no Windows

Também possível usar:

Entry Point

Todo o processo de Sincronismo e Integração é configurado por entry points do tipo JSON

dbeaver_entrypoint_doc_api.png

Tabela de Configuração Principais dos entry points

ColunaValorDescrição
FILE_OR_DOMAINAPIPara integração e sincronismo sempre será esse valor.
ACTIONsync ou integrationsync são para configurações de sincronismo e integration para as de integração. Como pode ter tabelas de nomes iguais foi criado essas duas actions.
TABELATABELANome da Tabela no NFS, se é app_operacao então é a tabela OPERACAO, igual nos entry points que são criados hoje.
CODECODIGO_JSONCódigo JSON de configuração, serão explicados na parte de sincronismo e integração
TYPEJSONO Tipo hoje é sempre em JSON

Sempre testar o código JSON para verificar se ele é válido isso pode te ajudar a ganhar tempo onde iria perder procurando porque uma configuração não esteja funcionando. Recomendo o Json Lint.

Autenticação

A autenticação é a JWT (Json Web Token) onde seus parâmetros obrigatórios são user, password e empresa e retorna um token que é válido por 15 minutos, após esse termino é necessário validar novamente.

O método de requisição é POST e no header deve sempre conter o content-type: application/json.

Tabela de Parâmetros

NomeObrigatórioDescrição
nomenãoNome do usuário
usersimE-mail do usuário válido para fazer o sincronismo
passwordsimSenha do usuário
empresasimNo campo empresa deve vim a SIGLA da Filial do NFS
filialnãoNo campo filial deve ser passada a SIGLA do Local no NFS

ATENÇÃO Não é possível criar usuários por qualquer método, por isso o usuário deve ser cadastrado previamente no NFS. É sempre indicado ao que criar um usuário verificar se ele tem acesso ao sistema e as filias liberadas. {.is-danger}

Em nossos exemplos vamos usar o sistema smartosapi.h.simova.cloud.

Requisição

curl --request POST \
  --url https://smartosapi.h.simova.cloud/nfs/api/v1/auth \
  --header 'content-type: application/json' \
  --data '{
	"nome": "dalton",
	"user": "dalton@simova.com.br",
	"password": "123",
	"empresa" : "SIMOVA" // Filial do NFS
}'

Retorno

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzbWFydG9zYXBpLmguc2ltb3ZhLmNsb3VkIiwibmFtZSI6ImRhbHRvbiIsImVtYWlsIjoiZGFsdG9uQHNpbW92YS5jb20uYnIiLCJmaWxpYWwiOiJGSUxJQUwgMSIsImxvY2FsIjpudWxsLCJleHAiOmZhbHNlfQ==.eqL8BGcaLLnbkd9V3nONJXOfVgpy8j6f6P+wcTKb7\/w=",
  "user": "dalton@simova.com.br"
}

insomnia_auth_doc_api.png

Imagem de exemplo do Insomnia de uma requisição para obter um token

Constantes na Autenticação

É possível colocar constantes na autenticação, isso pode ajudar tanto o cliente como a Simova, e é simples, basta adicionar o seguinte parâmetro (nfs_core_par_parametros) API_VALIDATION com seu valor um json, exemplo:

{
    "env": 2
}

Logo esse env igual a 2 deve estar no body do request:

{
	"user": "my.user@simova.com.br",
	"password": "myPassS",
	"empresa": "MyBranchSigla", 
	"env": "2"
}

Se não tiver a chave env vai gerar um erro, ou se o valor for diferendo do configurado nos parâmetros.

Sincronismo

A definifição de Sincronismo é quando o dado vem do sistema de terceiros para o NFS.

A busca de dados no banco para saber se será feito um insert/update é definido pelos campos que são definidos como Primary Key (Chave Primária), então é bom sempre fazer uma análise nos dados que serão recebidos e verificar se ele é uma chave primária ou não, isso não quer dizer que ele será uma PK no DB do NFS e sim serão valores unicos.

Estrutura

Estrutura Pai

{
  "alias": "NomeTabelaApelido",
  "table": "TABELA_NOME",
  "field": {
  }
}
ChaveObrigatórioValorDescrição
aliasSimApelidoTabelaDeve ser um apelido para tabela sem espaço.
tableSimTABELA_NOMEÉ a Tabela Nome que está na nfs_core_ds_tabela, usada para referênciar a uma tabela do NFS
fieldSimConjunto de JsonÉ onde ficam configurados os json de cada campo que vira do sincronismo, veja na tabela abaixo a configuração desses campos de forma detalhada, basicamento é o nome do campo que vem do terceiro e suas propriedades.
deleteExistDataNãotrue/falsePor padrão é false e se for definido como true, os dados existentes são deletados e inseridos novos.
Compartilhado

Para inserir um dado que é compartilhado, a primeira coisa é conferir na nfs_core_sys_tabela_compartilhada e ver se suas tabelas estão compartilhadas para filial e local, se não pode não funcionar. O segundo item é configurar na Api na hierarquia principal é possível usar a propriedade filial e local da seguinte forma:

{
  "alias": "NomeTabelaApelido",
  "table": "TABELA_NOME",
  "filial": { "share": true}, 
  "field": {
  }
}

Vale ressaltar que é independente, no caso acima a filial inserida sempre vai ser 9999.

{
  "alias": "NomeTabelaApelido",
  "table": "TABELA_NOME",
  "local": { "share": true}, 
  "field": {
  }
}

por local ou até ambos

{
  "alias": "NomeTabelaApelido",
  "table": "TABELA_NOME",
  "filial": { "share": true}, 
  "local": { "share": true}, 
  "field": {
  }
}

Estrutura do Field

Será descrito com mais detalhes o que pode conter na configuração do fields.

{
  "alias": "NomeTabelaApelido",
  "table": "TABELA_NOME",
  "field": {
    "CodigoEmpresa": {
      "column": "FILIAL",
      "type": "Varchar",
      "size": 100,
      "description": "Filial da Operacao",
      "primaryKey": true,
      "required" : true
    },
    "CodigoOperacao": {
      "column": "CODIGO",
      "type": "Varchar",
      "size": 100,
      "description": "CodigoOperacao da Operacao",
      "primaryKey": true,
      "required" : true
    }
  }
}

Como é possível observar CodigoEmpresa e CodigoOperacao, são campos que vem num json que é enviado por um sistema de terceiro e para cada um deles vai ter uma configuração.

ChaveObrigatórioValorDescrição
columnSimSTRINGNome da Coluna no NFS.
typeSimVarcharTipo da Coluna, geralmente é varchar
sizeSimINTEGERTamanho maximo do dado que está sendo sincronizado, ainda não é usado esse valor, mas será útil para validações futuras
descriptionNãoSTRINGDescrição da chave/Coluna, pode ser que isso ajude na manutenção para outras pessoas entenderem o que foi realizado, para que ser, de onde pertence.
primaryKeyNãotrue/falseIndica que aquela chave/coluna é chave primária, pode até não ser no banco mas ela é importanta para fazer buscas verificando se existe o campo no banco do NFS para ver se é necessário fazer um update ou insert
requiredNãotrue/falseÉ um atributo importante, se marcado como true exije que o sistema de terceiro envie o valor, se não enviar será enviado uma mensagem de erro dizendo que o campo é obrigatório. Lembrando que se o campo não é obrigado não é necessário usar esse atributo
fieldMsgReturnNãoSTRINGÉ usado como um apelido para o campo de mensagem na primeira validação que é quando há uma pré condição para fazer um sincronismo, por exemplo, no caso de OS se ela já foi integrada não pode ser mais atualizada pelo sincronismo, e o campo é CodigoOS mas o cliente deseja que seja somente OS com isso na mensagem de retorno vai Exibir OS ao invés de CodigoOS, veja mais em mensagem de retorno
fkNãotrue/falseIndica que a coluna é uma FK
tableOriginNãoSTRINGÉ somente usada quando o campo é marcado com "fk" : true e indica qual a tabela de origem.
columnOriginNãoSTRINGÉ somente usada quando o campo é marcado com "fk" : true, "tableOrigin" : "ESTADO" e indica qual é a coluna que representa, no caso pode ser que seja enviada a sigla do estado ficando "fk" : true, "tableOrigin" : "ESTADO", "columnOrigin" : "SIGLA" , com isso será salvo o ESTADO_SEQ_DB com o valor da SIGLA enviado Exemplo de config com FK.
fieldsCompNãoJson ArrayUsado quando a FK depende de mais de um campo que é enviado, o que chamamos de fk composta, na tabela de equipamento para ele ser unico depende CodigoLinhaEquip(LINHA), CodigoModeloEquip(MODELO), Chassi(CHASSI) veja no exemplo Exemplo de Configuração com FK Composta
multipleFKNãoJson ArrayUsado quando é o campo FK da tabela origem também é um FK veja no Exemplo de Configuração Multiple FK
parentNãotrue/falseUtilizado para quando precisa obter o valor de um informação que esteja no json pai, veja mais em Exemplo de Configuração de Parent

Tipos do Field

  • varchar: Tipo básico que representa strings
  • boolean: Tipo verdadeiro ou falso e aceita os seguintes valores verdadeiros: 1, yes, s, sim, ativo, a, verdadeiro. Tudo diferente desses valores vai ser considerado como falso.
  • int: Tipo inteiro
  • float: Tipo ponto flutuante

Se for passado um tipo que não existe acima vai ser considerado como uma string.

Exemplos

Configuração de Operação [Simples]

{
  "alias": "OperacaoServico",
  "table": "OPERACAO",
  "field": {
    "CodigoEmpresa": {
      "column": "FILIAL",
      "type": "Varchar",
      "size": 100,
      "description": "Filial da Operacao",
      "primaryKey": true,
      "required" : true
    },
    "CodigoLinhaOper": {
      "column": "CODIGO_LINHA",
      "type": "Varchar",
      "size": 100,
      "description": "Codigo Linha da Operacao",
      "primaryKey": true,
      "required" : true
    },
    "CodigoOperacao": {
      "column": "CODIGO",
      "type": "Varchar",
      "size": 100,
      "description": "CodigoOperacao da Operacao",
      "primaryKey": true,
      "required" : true
    },
    "Descricao": {
      "column": "DESCRICAO",
      "type": "Varchar",
      "size": 100,
      "description": "Descricao da Operacao",
      "required" : true
    },
    "DescricaoCompl": {
      "column": "DESCRICAO_COMPLETA",
      "type": "Varchar",
      "size": 100,
      "description": "Descricao Completa da Operacao"
    },
    "TipoOperacao": {
      "column": "TIPO_OPERCAO",
      "type": "Varchar",
      "size": 2,
      "description": "Tipo da Operacao",
      "required" : true
    }
  }
}

Exemplo de Configuração Cliente

{
  "alias": "Cliente",
  "table": "CLIENTE",
  "field": {
    "CodigoEmpresa": {
      "column": "FILIAL",
      "type": "Varchar",
      "size": 100,
      "description": "Filial do Cliente",
      "primaryKey": true,
      "required": true
    },
    "CodigoCliente": {
      "column": "CODIGO",
      "type": "Varchar",
      "size": 20,
      "description": "Código do Cliente Endereço no DMS 999999EEE (Pessoa/Ender)",
      "primaryKey": true,
      "required": true
    },
    "Nome": {
      "column": "DESCRICAO",
      "type": "Varchar",
      "description": "Nome do Cliente",
      "required": true
    },
    "Tipo": {
      "column": "TPPESSOA",
      "type": "Varchar",
      "size": 10,
      "description": "Tipo (Fisica/Juridica) do Cliente",
      "required": true
    },
    "CpfCnpj": {
      "column": "CNPJCPF",
      "type": "Varchar",
      "size": 100,
      "description": "Cpf ou Cnpj do Cliente",
      "required": true
    },
    "NomeEndereco": {
      "column": "ENDERECO",
      "type": "Varchar",
      "size": 100,
      "description": "Endereço do Cliente",
      "required": true
    },
    "Logradouro": {
      "column": "LOGRADOURO",
      "type": "Varchar",
      "size": 100,
      "description": "Logadouro do Cliente",
      "required": true
    },
    "Numero": {
      "column": "NUMERO",
      "type": "Varchar",
      "size": 100,
      "description": "Numero do Cliente"
    },
    "Complemento": {
      "column": "COMPLEMENTO",
      "type": "Varchar",
      "size": 100,
      "description": "Complemento do Cliente"
    },
    "Bairro": {
      "column": "BAIRRO",
      "type": "Varchar",
      "size": 30,
      "description": "Bairro do Cliente"
    },
    "Cidade": {
      "column": "MUNICIPIO",
      "type": "Varchar",
      "size": 30,
      "description": "Municipio do Cliente",
      "required": true
    },
    "UF": { // Exemplo FK
      "column": "ESTADO_SEQ_DB",
      "type": "Varchar",
      "size": 30,
      "description": "Numero do Cliente",
      "fk": true,
      "tableOrigin": "ESTADO",
      "columnOrigin": "SIGLA",
      "required": true
    }
  }
}

Exemplo de configuração com FK Composta e validação de OS

{
  "alias": "OrdemServico",
  "table": "OS",
  "conditions": [
   "STATUS_OS_SEQ_DB IN (3)"
  ],
  "msgForCondictions" : "OS já integrado ou encerrada!",
  "field": {
    "CodigoEmpresa": {
      "column": "FILIAL",
      "type": "Varchar",
      "size": 100,
      "description": "Filial da OS",
      "primaryKey": true,
      "required" : true
    },
    "FilialOS": {
      "column": "LOCAL",
      "type": "Varchar",
      "size": 100,
      "description": "Local da OS",
      "primaryKey": true,
      "required" : true
    },
    "CodigoOS": {
      "column": "CODIGO",
      "type": "Varchar",
      "size": 20,
      "description": "Codigo da OS",
      "primaryKey": true,
      "required" : true
    },
    "CodigoCliente": {
      "column": "CLIENTE_SEQ_DB",
      "type": "Varchar",
      "description": "Cliente da OS",
      "fk": true,
      "tableOrigin": "CLIENTE",
      "columnOrigin": "CODIGO",
      "required" : true
    },
    "CodigoLinhaEquip": { // Exemplo de Obter o equipamento da OS
      "column": "EQUIPAMENTO_SEQ_DB",
      "type": "Varchar",
      "description": "Codigo da Linha da OS",
      "fk": true,
      "tableOrigin": "EQUIPAMENTO",
      "columnOrigin": "LINHA",
      "fkComp": true,
      "fieldsComp": {
        "CodigoModeloEquip": {
          "column": "MODELO",
          "type": "Varchar",
          "size": 100,
          "description": "Codigo Modelo da OS"
        },
        "Chassi": {
          "column": "CHASSI",
          "type": "Varchar",
          "size": 100,
          "description": "Chassi da OS"
        }
      }
    },
    "DescricaoProblema": {
      "column": "DESCRICAO_PROBLEMA",
      "type": "Varchar",
      "size": 100,
      "description": "Descricao Problema da OS",
      "required" : true
    },
    "DataAbertura": {
      "column": "DATA_ABERTURA",
      "type": "Varchar",
      "size": 100,
      "description": "Descricao Problema da OS",
      "required" : true
    },
    "Chassi": {
      "column": "CHASSI",
      "type": "Varchar",
      "size": 100,
      "description": "Chassi da OS"
     },
    "Linha": {
      "column": "LINHA",
      "type": "Varchar",
      "size": 100,
      "description": "Linha da OS",
      "required" : true
    },
    "Modelo": {
      "column": "MODELO",
      "type": "Varchar",
      "size": 100,
      "description": "Modelo da OS",
      "required" : true
    },
    "IdOS": {
      "column": "",
      "type": "Varchar",
      "size": 100,
      "description": "SEQ_DB da OS no NFS"
    },
    "Servicos": {
      "type": "lovn",
      "table": "SERVICO",
      "aliasOrigin": "Operacao",
      "tableLovn": "OS_SERVICO"
    },
    "Tecnicos": {
      "type": "lovn",
      "aliasOrigin": "Tecnico",
      "tableLovn": "OS_TECNICO"
    },    
    "AgendaTecnicos": {
      "type": "lovn",
      "aliasOrigin": "AgendaTecnicos",
      "tableLovn": "OS_TECNICO_DETALHE"
    },
    "Pecas": {
      "type": "lovn",
      "table": "PECA",
      "aliasOrigin": "Peça",
      "tableLovn": "OS_PECA"
    }
  }
}

Sincronismo de LOVN

Nesse exemplo o Sincronismo de OS tem LOVN com Serviço (OS x Serviço), Técnico (OS x Técnico), Agenda de Técnico (OS x Agenda Tecnico), Peças (OS x Peça).

Como é possível observar nos exemplos abaixo na configuração da LOVN somente é necessário:

"NomeChave": { 
   "type": "lovn",
   "aliasOrigin": "AliasDoEntryPointConfiguracao",
   "tableLovn": "NOME_TABLE_LOVN"
}

A request de envio será parecida:

       "AgendaTecnicos": // Detalhe importante que é sempre um json array
        [
            {
                "AgendaId": 39564,
                "CodigoEmpresaTec": "PALM",
                "CodigoTecnico": 161,
                "DataHoraInicio": "2023-02-02T09:10:00",
                "DataHoraTermino": "2023-02-02T11:10:00"
            }
        ],

Exemplo de Request de OS

[
    {
        "CodigoEmpresa": "PALM",
        "FilialOS": 1,
        "CodigoOS": "00001",
        "CodigoCliente": "0011",
        "CodigoLinhaEquip": "0119",
        "Linha": "COLHEITADEIRAS ",
        "CodigoModeloEquip": "CR5.85              ",
        "Modelo": "CR5.85                        ",
        "Chassi": "JHFY3585JHJF09160   ",
        "DescricaoProblema": "CORREIA DO ALTERNADOR",
        "DataAbertura": "2023-02-02T08:41:21",
        "IdOS": 0,
        "NomeContato": "CONTATO FICTICIO",
        "TelefoneContato": "(12) 123456789",
        "Servicos":
        [
            {
                "SeqServico": "001-0001-0000",
                "CodigoEmpresaOper": "PALM",
                "CodigoLinhaOper": "0119",
                "CodigoOperacao": "9999999     ",
                "DescricaoOperacao": "VERIFICAR CORREIA DO ALTERNADOR",
                "TempoOperacao": 120,
                "Deslocamento": 0
            },
            {
                "SeqServico": "KM",
                "CodigoEmpresaOper": "PALM",
                "CodigoLinhaOper": "0119",
                "CodigoOperacao": "DESLOCAMENTO",
                "DescricaoOperacao": "Deslocamento",
                "TempoOperacao": 1,
                "Deslocamento": 1
            }
        ],
        "Tecnicos":
        [
            {
                "CodigoEmpresaTec": "PALM",
                "CodigoTecnico": 161
            }
        ],
        "AgendaTecnicos":
        [
            {
                "AgendaId": 39564,
                "CodigoEmpresaTec": "PALM",
                "CodigoTecnico": 161,
                "DataHoraInicio": "2023-02-02T09:10:00",
                "DataHoraTermino": "2023-02-02T11:10:00"
            }
        ],
        "Pecas":
        [
            {
                "CodigoEmpresaPec": "PALM",
                "CodigoPeca": "87699094       ",
                "Descricao": "CORREIA DE BORR",
                "DescricaoCompl": "CORREIA DE BORR",
                "Quantidade": 1
            }
        ]
    }
]

Configuração de OS

{
  "alias": "OrdemServico",
  "table": "OS",
  "conditions": [
    "STATUS_OS_SEQ_DB IN (3)"
  ],
  "msgForCondictions": "OS já integrado ou encerrada!",
  "field": {
    "CodigoEmpresa": {
      "column": "FILIAL",
      "type": "Varchar",
      "size": 100,
      "description": "Filial da OS",
      "primaryKey": true,
      "required": true
    },
    "FilialOS": {
      "column": "LOCAL",
      "type": "Varchar",
      "size": 100,
      "description": "Local da OS",
      "primaryKey": true,
      "required": true
    },
    "NomeContato": {
      "column": "NOME_CONTATO",
      "type": "Varchar",
      "size": 60,
      "description": "Nome do contato da OS"
    },
    "TelefoneContato": {
      "column": "TELEFONE_CONTATO",
      "type": "Varchar",
      "size": 20,
      "description": "Telefone do contato da OS"
    },
    "CodigoOS": {
      "column": "CODIGO",
      "type": "Varchar",
      "size": 20,
      "description": "Codigo da OS",
      "primaryKey": true,
      "required": true
    },
    "CodigoCliente": {
      "column": "CLIENTE_SEQ_DB",
      "type": "Varchar",
      "description": "Cliente da OS",
      "fk": true,
      "tableOrigin": "CLIENTE",
      "columnOrigin": "CODIGO",
      "required": true
    },
    "CodigoModeloEquip": {
      "column": "EQUIPAMENTO_SEQ_DB",
      "type": "Varchar",
      "description": "Codigo",
      "fk": true,
      "tableOrigin": "EQUIPAMENTO",
      "columnOrigin": "MODELO_EQUIPAMENTO_SEQ_DB",
      "fkComp": true,
      "multipleFK": {
        "CodigoModeloEquip": {
          "column": "MODELO_EQUIPAMENTO_SEQ_DB",
          "type": "Varchar",
          "description": "Codigo do Modelo",
          "fk": true,
          "tableOrigin": "MODELO_EQUIPAMENTO",
          "columnOrigin": "CODIGO",
          "required": true,
          "primaryKey": true,
          "fieldsComp": {
            "CodigoEmpresa": {
              "column": "FILIAL",
              "type": "Varchar",
              "size": 100,
              "description": "filial do tipo equipamento",
              "primaryKey": true,
              "required": true,
              "parent" : true
            },
            "CodigoLinhaEquip": {
              "column": "TIPO_EQUIPAMENTO_SEQ_DB",
              "type": "Varchar",
              "description": "Codigo da Linha",
              "fk": true,
              "tableOrigin": "TIPO_EQUIPAMENTO",
              "columnOrigin": "CODIGO",
              "required": true,
              "fieldsComp": {
                "CodigoEmpresa": {
                  "column": "FILIAL",
                  "type": "Varchar",
                  "size": 100,
                  "description": "filial do tipo equipamento",
                  "primaryKey": true,
                  "required": true,
                  "parent" : true
                }
              }
            }
          }
        }
      },
      "fieldsComp": {
        "CodigoEmpresa": {
          "column": "FILIAL",
          "type": "Varchar",
          "size": 100,
          "description": "filial do tipo equipamento",
          "primaryKey": true,
          "required": true,
          "parent" : true
        },
        "Chassi": {
          "column": "CHASSI",
          "type": "Varchar",
          "size": 100,
          "description": "Chassi da OS",
          "primaryKey": true
        }
      }
    },
    "DescricaoProblema": {
      "column": "OBSERVACAO",
      "type": "Varchar",
      "size": 100,
      "description": "Descricao Problema da OS",
      "required": true
    },
    "DataAbertura": {
      "column": "DATA_ABERTURA",
      "type": "Varchar",
      "size": 100,
      "description": "Descricao Problema da OS",
      "required": true
    },
    "Chassi": {
      "column": "CHASSI",
      "type": "Varchar",
      "size": 100,
      "description": "Chassi da OS"
    },
    "Linha": {
      "column": "",
      "type": "Varchar",
      "size": 100,
      "description": "Linha da OS",
      "required": true
    },
    "Modelo": {
      "column": "",
      "type": "Varchar",
      "size": 100,
      "description": "Modelo da OS",
      "required": true
    },
    "IdOS": {
      "column": "",
      "type": "Varchar",
      "size": 100,
      "description": "SEQ_DB da OS no NFS"
    },
    "Servicos": {
      "type": "lovn",
      "aliasOrigin": "Operacao",
      "tableLovn": "OS_SERVICO"
    },
    "Tecnicos": {
      "type": "lovn",
      "aliasOrigin": "Tecnico",
      "tableLovn": "OS_TECNICO"
    },
    "AgendaTecnicos": {
      "type": "lovn",
      "aliasOrigin": "AgendaTecnicos",
      "tableLovn": "OS_TECNICO_DETALHE"
    },
    "Pecas": {
      "type": "lovn",
      "aliasOrigin": "Peça",
      "tableLovn": "OS_PECA"
    }
  }
}

Configuração OS Servico

Como é possível observar também na configuração você vai encontrar uma referência para a tabela de OS_SEQ_DB que é a referência para SERVICO, isso acontece porque primeiro é inserido/atualizado a OS (Com isso temos o seu SEQ_DB), depois é necessário passar isso na configuração para ser relacionado.

Apesar de ser uma FK, vamos deixar como INT porque vai pegar o valor do SEQ_DB que está me memória, também deixaremos o primaryKey para saber que tem que buscar com esse valor para saber se vai atualizar ou inserir um novo registro.

  "OS": {
      "column": "OS_SEQ_DB",
      "type": "int",
      "description": "Tempo da da Tecnico com OS",
      "primaryKey": true
    },

Configuração completa da OS_SERVICO.

{
  "alias": "Servicos",
  "table": "OS_SERVICO",
  "field": {
    "CodigoEmpresaOper": {
      "column": "FILIAL",
      "type": "Varchar",
      "size": 100,
      "description": "Filial da Operacao",
      "primaryKey": true,
      "required": true
    },
    "CodigoLinhaOper": {
      "column": "SERVICO_SEQ_DB",
      "type": "Varchar",
      "description": "Codigo da Linha da Operacao",
      "primaryKey": true,
      "fk": true,
      "tableOrigin": "SERVICO",
      "columnOrigin": "CODIGO_LINHA",
      "fkComp": true,
      "fieldsComp": {
        "CodigoOperacao": {
          "column": "CODIGO",
          "type": "Varchar",
          "size": 100,
          "description": "CodigoOperacao da Operacao"
        },
        "SeqServico": {
          "column": "SEQ_OPERACAO",
          "type": "Varchar",
          "size": 100,
          "description": "SeqServico da Operacao com OS"
        },
        "CodigoOS": {
          "column": "CODIGO_OS",
          "type": "varchar",
          "parent": true,
          "description": "Codigo da OS do pai"
        },
        "CodigoEmpresaOper": {
          "column": "FILIAL",
          "type": "Varchar",
          "size": 100,
          "description": "Filial da Operacao",
          "primaryKey": true,
          "required": true,
          "parent": true
        },
        "FilialOS": {
          "column": "LOCAL",
          "type": "Varchar",
          "size": 100,
          "description": "Local da OS",
          "primaryKey": true,
          "required": true,
          "parent": true
        }
      },
      "required": true
    },
    "SeqServico": {
      "column": "SEQ_OPERACAO",
      "type": "Varchar",
      "size": 100,
      "description": "SeqServico da Operacao com OS",
      "primaryKey": true,
      "required": true
    },
    "DescricaoOperacao": {
      "column": "DESCRICAO_OPERACAO",
      "type": "Varchar",
      "size": 100,
      "description": "SeqServico da Operacao com OS",
      "required": true
    },
    "TempoOperacao": {
      "column": "TEMPO_OPERACAO",
      "type": "int",
      "description": "Tempo da da Operacao com OS"
    },
    "OS": {
      "column": "OS_SEQ_DB",
      "type": "int",
      "description": "Tempo da da Operacao com OS",
      "primaryKey": true
    },
    "CodigoOS": {
      "column": "",
      "type": "varchar",
      "parent": true,
      "description": "Codigo da OS do pai"
    },
    "FilialOS": {
      "column": "LOCAL",
      "type": "Varchar",
      "size": 100,
      "description": "Local da OS",
      "primaryKey": true,
      "required": true,
      "parent": true
    }
  }
}

Configuração OS x Técnico

Como é possível observar também na configuração você vai encontrar uma referência para a tabela de OS_SEQ_DB que é a referência para TECNICO, isso acontece porque primeiro é inserido/atualizado a OS (Com isso temos o seu SEQ_DB), depois é necessário passar isso na configuração para ser relacionado.

Para mais detalhes ver Configuração OS Servico.

{
  "alias": "Tecnicos",
  "table": "OS_TECNICO",
  "field": {
    "CodigoEmpresaTec": {
      "column": "FILIAL",
      "type": "Varchar",
      "size": 100,
      "description": "Filial do Tecnico com OS",
      "primaryKey": true,
      "required" : true
    },
    "CodigoTecnico": {
      "column": "FUNCIONARIO_SEQ_DB",
      "type": "Varchar",
      "description": "Técnico da OS",
      "primaryKey": true,
      "fk": true,
      "tableOrigin": "FUNCIONARIO",
      "columnOrigin": "CRACHA",
      "required" : true
    },
    "OS": {
      "column": "OS_SEQ_DB",
      "type": "int",
      "description": "Tempo da da Tecnico com OS",
      "primaryKey": true
    },
    "DataAgendada": {
      "column": "DATA_ABERTURA",
      "type": "Varchar",
      "size": 100,
      "description": "Data de Abertura da OS",
      "required" : true
    },
    "FilialOS": {
      "column": "LOCAL",
      "type": "Varchar",
      "size": 100,
      "description": "Local da OS",
      "required" : true,
      "primaryKey" : true,
      "parent": true
    }
  }
}

Configuração Agenda Técnico

Nessa configuração existe "deleteExistData": true uma opção que deleta o que existe e cria um novo registro.

Nessa configuração já é diferente, usamos a OS_SEQ_DB para encontrar o registro da OS_TECNICO.

Para mais detalhes ver Configuração OS Servico.

{
  "alias": "AgendaTecnicos",
  "table": "OS_TECNICO_DETALHE",
  "deleteExistData": true, // Detalhe importante
  "field": {
    "CodigoEmpresaTec": {
      "column": "FILIAL",
      "type": "Varchar",
      "size": 100,
      "description": "Filial do Tecnico com OS",
      "primaryKey": true,
      "required": true
    },
    "FilialOS": {
      "column": "LOCAL",
      "type": "Varchar",
      "size": 100,
      "description": "Local da OS",
      "primaryKey": true,
      "required" : true,
      "parent": true
    },
    "CodigoTecnico": {
      "column": "OS_TECNICO_SEQ_DB",
      "type": "Varchar",
      "description": "Codigo Tenico OS",
      "fk": true,
      "tableOrigin": "OS_TECNICO",
      "columnOrigin": "FUNCIONARIO_SEQ_DB",
      "fkComp": true,
      "multipleFK": {
        "CodigoTecnico": {
          "column": "FUNCIONARIO_SEQ_DB",
          "type": "Varchar",
          "description": "Codigo do Funcionario da OS x Tecnico",
          "fk": true,
          "tableOrigin": "FUNCIONARIO",
          "columnOrigin": "CRACHA",
          "required": true,
          "fieldsComp": {
            "CodigoEmpresaTec": {
		      "column": "FILIAL",
		      "type": "Varchar",
		      "size": 100,
		      "description": "Filial do Tecnico com OS"
		    }
          }
        }
      },
      "primaryKey": true,
      "fkComp": true,
      "required": true,
      "fieldsComp": {
        "OS": {
          "column": "OS_SEQ_DB", // SEQ_DB da OS inserido ou atualizado
          "type": "int",
          "description": "Tempo da da Tecnico com OS"
        },
        "CodigoEmpresaTec": {
          "column": "FILIAL",
          "type": "Varchar",
          "description": "Filial do Tecnico com OS",
          "parent": true
        },
        "FilialOS": {
          "column": "LOCAL",
          "type": "Varchar",
          "size": 100,
          "description": "Local da OS",
          "parent": true
        }
      }
    },
    "DataHoraInicio": {
      "column": "DATA_INICIAL",
      "type": "Varchar",
      "size": 100,
      "description": "Data de Inicia de Agendamento da OS",
      "required": true
    },
    "DataHoraTermino": {
      "column": "DATA_FINAL",
      "type": "Varchar",
      "size": 100,
      "description": "Data de Inicia de Agendamento da OS",
      "required": true
    },
    "AgendaId": {
      "column": "SEQ_AGENDA",
      "type": "Varchar",
      "size": 12,
      "description": "Id da Agenda",
      "required": true
    }
  }
}

Configuração Peças

Como é possível observar também na configuração você vai encontrar uma referência para a tabela de OS_SEQ_DB que é a referência para PECA, isso acontece porque primeiro é inserido/atualizado a OS (Com isso temos o seu SEQ_DB), depois é necessário passar isso na configuração para ser relacionado.

Para mais detalhes ver Configuração OS Servico.

{
  "alias": "Pecas",
  "table": "OS_PECA",
  "field": {
    "CodigoEmpresaPec": {
      "column": "FILIAL",
      "type": "Varchar",
      "size": 100,
      "description": "Filial do Peca com OS",
      "primaryKey": true,
      "required" : true
    },
    "CodigoEmpresa": {
      "column": "FILIAL",
      "type": "Varchar",
      "size": 100,
      "description": "Filial do Peca com OS",
      "primaryKey": true,
      "required" : true
    },
    "CodigoPeca": {
      "column": "PECA_SEQ_DB",
      "type": "Varchar",
      "description": "Peca da OS",
      "primaryKey": true,
      "fk": true,
      "tableOrigin": "PECA",
      "columnOrigin": "CODIGO",
      "required" : true
    },
    "OS": {
      "column": "OS_SEQ_DB",
      "type": "int",
      "description": "OS_SEQ_DB de Peca com OS",
      "primaryKey": true
    },
    "Quantidade": {
      "column": "QTD_REQUISITADA",
      "type": "varchar",
      "description": "Quantidade da Peca com OS"
    },
    "FilialOS": {
      "column": "LOCAL",
      "type": "Varchar",
      "size": 100,
      "description": "Local da OS",
      "primaryKey": true,
      "required" : true,
      "parent": true
    }
  }
}

Sincronismo dos dados em uma única chamada

É possível enviar todos os dados da OS na própria OS sem precisar realizar um sincronismo de equipamento, funcionário, cliente antes, basicamente é enviar na chave um array com seus respectivos dados. Então no equipamento ao invés de mandar o código dele, peça poara ser enviado um array com os dados do equipamento, mesma coisa para o funcionário, cliente e etc.

Para isso também é necessário ter as configurações da API do Funcionário, Equipamento e Cliente como se fosse enviar eles separadamente.

Exemplo de json que é enviado para API

Nesse caso a chave para trazer o equipamento é ChassiEquipamento, porém nesse caso em especial e é um bom caso, o equipamento também precisa do Modelo e ele do Tipo, então criou-se uma dependência mais profunda. Equipamento -> Modelo -> Tipo.

Como comentei na introdução dessa funcionalidade o cliente também é enviado, como um array com todos os seus dados, então o sincronismo vai buscar o cliente, se existir faz a atualização, se não insere.

[
	{
		"Filial": "Brasília",
		"Local": "Brasília",
		"CodigoOS": "9991089",
		"DataAbertura": "2022-07-08 12:00:00",
		"Ativo": "1",
		"Observacao": "Teste de OS Dalton",
		"ChassiEquipamento": [
			{
				"ChassiEquipamento": "dasd10X",
				"CodigoEquipamento": "123321",
				"Ativo": 1,
				"Descricao": "Super Ultra Keyboard",
				"CodigoModeloEquipamento": [
					{
						"CodigoModeloEquipamento": "12344321",
						"Descricao": "Keyboard S-line",
						"CodigoTipoEquipamento": [
							{
								"CodigoTipoEquipamento": "789987",
								"Ativo": 1,
								"Descricao": "Split"
							}
						]
					}
				],
				"SeqCliente": "1234567890"
			}
		],
		"CodigoCliente": [
			{
				"CodigoCliente": "12131415",
				"Nome": "Simovers",
				"CpfCnpj": "0987654321",
				"Loja": "Simova Grande Arujá",
				"NroProprietario": "123456",
				"Idenficacao": "Street1",
				"NomeEndereco": "Avenue San Juan",
				"InscricaoEstadual": "CA",
				"Municipio": "San Jose dos Fields",
				"Bairro": "Garden of Mounts",
				"Telefone": "1234567890",
				"Celular": "0987654321",
				"CEP": "18878-555",
				"Email": "xpto@simova.com.br",
				"UF": "SP"
			}
		],
		"SeqStatusOs": "1",
		"SeqTipoOs": "1"
	}
]

Configuração de OS

{
    "alias": "OrdemServico",
    "table": "OS",
    "field":
    {
        "CodigoOS":
        {
            "column": "CODIGO",
            "type": "varchar",
            "size": 8,
            "description": "Codigo da OS",
            "primaryKey": true,
            "required": true
        },
        "Filial":
        {
            "column": "FILIAL",
            "type": "int",
            "size": 10,
            "description": "Filial da OS"
        },
        "Local":
        {
            "column": "LOCAL",
            "type": "int",
            "size": 10,
            "description": "Local da OS"
        },
        "Ativo":
        {
            "column": "ATIVO",
            "type": "int",
            "size": 10,
            "description": "OS Ativo"
        },
        "Observacao":
        {
            "column": "OBSERVACAO",
            "type": "text",
            "description": "Observacao da OS"
        },
        "DataAbertura":
        {
            "column": "DATA_ABERTURA",
            "type": "timestamp",
            "description": "Data de abertura da OS"
        },
        "SeqStatusOs":
        {
            "column": "STATUS_OS_SEQ_DB",
            "type": "Bigint",
            "size": 20,
            "description": "Sequencial do status da OS",
            "fk": true,
            "tableOrigin": "STATUS_OS",
            "columnOrigin": "SEQ_DB"
        },
        "CodigoCliente":
        {
            "column": "CLIENTE_SEQ_DB",
            "type": "varchar",
            "size": 20,
            "description": "Código do cliente",
            "fk": true,
            "tableOrigin": "CLIENTE",
            "columnOrigin": "CODIGO",
            "fkComp": true,
            "fieldsComp":
            {
                "Loja":
                {
                    "column": "LOJA",
                    "type": "Varchar",
                    "size": 100,
                    "description": "Código da Loja do Cliente"
                }
            }
        },
        "ChassiEquipamento":
        {
            "column": "EQUIPAMENTO_SEQ_DB",
            "type": "varchar",
            "size": 20,
            "description": "Sequencial do equipamento",
            "fk": true,
            "tableOrigin": "EQUIPAMENTO",
            "columnOrigin": "CHASSI"
        },
        "SeqTipoOs":
        {
            "column": "TIPO_OS_SEQ_DB",
            "type": "Bigint",
            "size": 20,
            "description": "Sequencial do tipo de OS",
            "fk": true,
            "tableOrigin": "TIPO_OS",
            "columnOrigin": "SEQ_DB"
        }
    }
}

Configuração de Cliente

{
  "alias": "Cliente",
  "table": "CLIENTE",
  "field": {
    "CodigoCliente" : {
      "column": "CODIGO",
      "type": "Varchar",
      "size": 20,
      "description": "Código do Cliente/Endereço no DMS 999999EEE (Pessoa/Ender)",
      "primaryKey": true
    },
    "Nome": {
      "column": "DESCRICAO",
      "type": "Varchar",
      "size": 250,
      "description": "Nome do Cliente"
    },
    "CodigoEmpresa": {
       "column": "",
       "type": "Varchar",
       "filial": true,
       "description": "Filial NFS"
    },
    "CpfCnpj": {
      "column": "CNPJCPF",
      "type": "Varchar",
      "size": 100,
      "description": "Cpf ou Cnpj do Cliente"
    },
    "Loja": {
      "column": "LOJA",
      "type": "Varchar",
      "size": 20,
      "description": "Loja do Cliente"
    },
    "NroProprietario": {
      "column": "NRO_PROPRIETARIO",
      "type": "Varchar",
      "size": 50,
      "description": "Nro proprietario do Cliente"
    },
    "Identificacao": {
      "column": "IDENTIFICACAO",
      "type": "Varchar",
      "size": 50,
      "description": "Identificacao do Cliente"
    },
    "NomeEndereco": {
      "column": "ENDERECO",
      "type": "Varchar",
      "size": 100,
      "description": "Endereço do Cliente"
    },
    "InscricaoEstadual": {
      "column": "INSCRICAO_ESTADUAL",
      "type": "Varchar",
      "size": 100,
      "description": "Inscricao estadual do Cliente"
    },
    "Municipio": {
      "column": "MUNICIPIO",
      "type": "Varchar",
      "size": 250,
      "description": "Municipio do Cliente"
    },
    "Bairro": {
      "column": "BAIRRO",
      "type": "Varchar",
      "size": 30,
      "description": "Bairro do Cliente"
    },
    "Telefone": {
      "column": "TELEFONE",
      "type": "Varchar",
      "size": 100,
      "description": "Telefone do Cliente"
    },
    "Celular": {
      "column": "CELULAR",
      "type": "Varchar",
      "size": 100,
      "description": "Celular do Cliente"
    },
    "CEP": {
      "column": "CEP",
      "type": "Varchar",
      "size": 100,
      "description": "CEP do Cliente"
    },
    "Email": {
      "column": "E_MAIL",
      "type": "Varchar",
      "size": 100,
      "description": "Email do Cliente"
    },
    "UF": {
      "column": "ESTADO_SEQ_DB",
      "type": "Varchar",
      "size": 30,
      "description": "Numero do Cliente",
      "fk": true,
      "tableOrigin": "ESTADO",
      "columnOrigin": "SIGLA"
    }
  }
}

Configuração de Equipamento

{  
  "alias": "Equipamento",
  "table": "EQUIPAMENTO",
  "field": {
    "ChassiEquipamento": {
      "column": "CHASSI",
      "type": "varchar",
      "size": 20,
      "description": "Chassi do equipamento",
      "primaryKey": true,
      "required": true
    },
    "CodigoEquipamento": {
      "column": "CODIGO",
      "type": "varchar",
      "size": 250,
      "description": "Codigo do equipamento"
    },
    "Ativo": {
      "column": "ATIVO",
      "type": "int",
      "size": 10,
      "description": "Modelo de equipamento Ativo"
    },
    "Descricao": {
      "column": "DESCRICAO",
      "type": "varchar",
      "size": 250,
      "description": "Descricao do modelo do equipamento"
    },
    "CodigoModeloEquipamento": {
      "column": "MODELO_EQUIPAMENTO_SEQ_DB",
      "type": "varchar",
      "size": 20,
      "description": "Sequencial do modelo do equipamento",
      "fk": true,
      "tableOrigin": "MODELO_EQUIPAMENTO",
      "columnOrigin": "CODIGO"
    },
    "SeqCliente": {
      "column": "CLIENTE_SEQ_DB",
      "type": "varchar",
      "size": 20,
      "description": "Sequencial do cliente",
      "fk": true,
      "tableOrigin": "CLIENTE",
      "columnOrigin": "CODIGO"
    }
  }
}

Configuração de Modelo do Equipamento

{
  "alias": "ModeloEquipamento",
  "table": "MODELO_EQUIPAMENTO",
  "field": {
    "CodigoModeloEquipamento": {
      "column": "CODIGO",
      "type": "varchar",
      "size": 50,
      "description": "Codigo do modelo do equipamento",
      "primaryKey": true,
      "required": true
    },
    "Ativo": {
      "column": "ATIVO",
      "type": "int",
      "size": 10,
      "description": "Modelo de equipamento Ativo"
    },
    "Descricao": {
      "column": "DESCRICAO",
      "type": "varchar",
      "size": 250,
      "description": "Descricao do modelo do equipamento"
    },
    "CodigoTipoEquipamento": {
      "column": "TIPO_EQUIPAMENTO_SEQ_DB",
      "type": "Bigint",
      "size": 20,
      "description": "Sequencial do tipo do equipamento",
      "fk": true,
      "tableOrigin": "TIPO_EQUIPAMENTO",
      "columnOrigin": "CODIGO"
    }
  }
}

Configuração do Tipo do Modelo do Equipamento

{
  "alias": "TipoEquipamento",
  "table": "TIPO_EQUIPAMENTO",
  "field": {
    "CodigoTipoEquipamento": {
      "column": "CODIGO",
      "type": "varchar",
      "size": 50,
      "description": "Codigo do tipo de equipamento",
      "primaryKey": true,
      "required": true
    },
    "Ativo": {
      "column": "ATIVO",
      "type": "int",
      "size": 10,
      "description": "Tipo de equipamento Ativo"
    },
    "Descricao": {
      "column": "DESCRICAO",
      "type": "varchar",
      "size": 250,
      "description": "Descricao do tipo de equipamento"
    }
  }
}

Exemplo de OS_TENICO_DETALHE MultipleFK [Caso Especial]

Na OS_TECNICO_DETALHE as FK são:

  • OS_TENICO, porém o que compõe essa chave é CodigoTecnico (FUNCIONARIO_SEQ_DB) e OS
  • OS_SEQ_DB valor da OS que foi inserido, lembrando que quando é lovn primeiro insere os dados sem relacionamento e depois o seu relacionamento.
  • FILIAL filial é enviada no json se sincronismo e o LOCAL é obtido a partir do pai.

Então o fluxo é o seguinte, se não existe OS insere um nova e se existe atualiza, daí tem o valor da OS no NFS, depois disso é feito a relação entre OS_TECNICO_DETALHE, onde já existe o valor da OS e primeiro é encontrado o SEQ_DB da OS_TECNICO e no fim insere OS_TECNICO_DETALHE.

Uma particulariedade desse caso é o seguinte, esse exemplo de configuração foi feito para o MDS e eles não possuem um controle de ativo e desativo da Agenda então sempre é excluído todos os dados e enviado, para isso é usada a opção "deleteExistData": true, deleta e insere novamente.

{
  "alias": "AgendaTecnicos",
  "table": "OS_TECNICO_DETALHE",
  "deleteExistData": true,
  "field": {
    "CodigoEmpresaTec": {
      "column": "FILIAL",
      "type": "Varchar",
      "size": 100,
      "description": "Filial do Tecnico com OS",
      "primaryKey": true,
      "required": true
    },
    "CodigoTecnico": {
      "column": "OS_TECNICO_SEQ_DB",
      "type": "Varchar",
      "description": "Codigo Tenico OS",
      "fk": true,
      "tableOrigin": "OS_TECNICO",
      "columnOrigin": "FUNCIONARIO_SEQ_DB",
      "fkComp": true,
      "multipleFK": { // Exemplo MultipleFK
        "CodigoTecnico": {
          "column": "FUNCIONARIO_SEQ_DB",
          "type": "Varchar",
          "description": "Codigo do Funcionario da OS x Tecnico",
          "fk": true,
          "tableOrigin": "FUNCIONARIO",
          "columnOrigin": "CRACHA",
          "required": true,
          "fieldsComp": {
            "OS": {
              "column": "OS_SEQ_DB",
              "type": "int",
              "description": "Tempo da da Tecnico com OS"
            }
          }
        }
      },
      "primaryKey": true,
      "fkComp": true,
      "required": true,
      "fieldsComp": {
        "OS": {
          "column": "OS_SEQ_DB",
          "type": "int",
          "description": "Tempo da da Tecnico com OS"
        },
        "CodigoEmpresaTec": {
          "column": "FILIAL",
          "type": "Varchar",
          "description": "Filial do Tecnico com OS"
        }
      }
    },
    "DataHoraInicio": {
      "column": "DATA_INICIAL",
      "type": "Varchar",
      "size": 100,
      "description": "Data de Inicia de Agendamento da OS",
      "required": true
    },
    "DataHoraTermino": {
      "column": "DATA_FINAL",
      "type": "Varchar",
      "size": 100,
      "description": "Data de Inicia de Agendamento da OS",
      "required": true
    },
    "AgendaId": {
      "column": "SEQ_AGENDA",
      "type": "Varchar",
      "size": 12,
      "description": "Id da Agenda",
      "required": true
    }
  }
}

Exemplo do uso da opção parent

Acontece bastante em estrutura que são interligadas, então é enviado num sincronismo de OS o seu relacionamento com serviço, e ainda mais o serviço se não existe é adicionado ou atualizado e daí criado o relacionamento com OS x Serviço é criado. Para isso foi criada a opção parent.

No caso no Sincronismo de OS tem uma LOVN com Serviço, com isso para o serviço não ficar sem Local o mesmo é obtido a partir da OS então é usado

 "FilialOS": {
    "column": "LOCAL",
    "type": "Varchar",
    "size": 100,
    "description": "Local da OS",
    "primaryKey": true,
    "required" : true,
    "parent" : true
  }

Lembrando que se não haver o campo de FilialOS no json o mesmo não será usado.

Exemplo completo:

{
  "alias": "OperacaoServico",
  "table": "SERVICO",
  "field": {
    "CodigoEmpresa": {
      "column": "FILIAL",
      "type": "Varchar",
      "size": 100,
      "description": "Filial da Operacao",
      "primaryKey": true,
      "required" : true
    },
    "CodigoEmpresaOper": {
      "column": "FILIAL",
      "type": "Varchar",
      "size": 100,
      "description": "Filial da Operacao",
      "primaryKey": true,
      "required" : true
    },
    "CodigoLinhaOper": {
      "column": "CODIGO_LINHA",
      "type": "Varchar",
      "size": 100,
      "description": "Codigo Linha da Operacao",
      "primaryKey": true,
      "required" : true
    },
    "CodigoOperacao": {
      "column": "CODIGO",
      "type": "Varchar",
      "size": 100,
      "description": "CodigoOperacao da Operacao",
      "primaryKey": true,
      "required" : true
    },
    "Descricao": {
      "column": "DESCRICAO",
      "type": "Varchar",
      "size": 100,
      "description": "Descricao da Operacao",
      "required" : true
    },
    "DescricaoCompl": {
      "column": "DESCRICAO_COMPLETA",
      "type": "Varchar",
      "size": 100,
      "description": "Descricao Completa da Operacao"
    },
    "TipoOperacao": {
      "column": "TIPO_OPERCAO",
      "type": "Varchar",
      "size": 2,
      "description": "Tipo da Operacao",
      "required" : true
    },
    "Deslocamento": {
      "column": "FLAG_DESLOCAMENTO",
      "type": "Varchar",
      "size": 2,
      "description": "Flag indica Deslocamento"
    },
    "SeqServico": {
      "column": "SEQ_OPERACAO",
      "type": "Varchar",
      "size": 100,
      "description": "SeqServico da Operacao com OS",
      "primaryKey": true,
      "required" : true
    },
     "DescricaoOperacao": {
      "column": "DESCRICAO",
      "type": "Varchar",
      "size": 100,
      "description": "SeqServico da Operacao com OS",
      "required" : true
    },
    "TempoOperacao": {
      "column": "TEMPO_OPERACAO",
      "type": "int",
      "description": "Tempo da da Operacao com OS"
    },
    "CodigoOS": {
      "column": "CODIGO_OS",
      "type": "varchar",
      "parent" : true,
      "primaryKey": true,
      "description": "Codigo da OS do pai"
    },
    "CodigoOS": {
      "column": "CODIGO_OS",
      "type": "varchar",
      "parent" : true,
      "primaryKey": true,
      "description": "Codigo da OS do pai"
    },
	    "FilialOS": {
	      "column": "LOCAL",
	      "type": "Varchar",
	      "size": 100,
	      "description": "Local da OS",
	      "primaryKey": true,
	      "required" : true,
	      "parent" : true
	    }
  }
}

Sincronismo de Cliente com CURL

curl --request POST \
  --url http://smartosapi.h.simova.cloud/nfs/api/v1/sync/cliente \
  --header 'authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzbWFydG9zYXBpLmguc2ltb3ZhLmNsb3VkIiwibmFtZSI6ImRhbHRvbiIsImVtYWlsIjoiZGFsdG9uQHNpbW92YS5jb20uYnIiLCJmaWxpYWwiOiJGSUxJQUwgMSIsImxvY2FsIjpudWxsLCJleHAiOiIyMDIwLTAxLTAzIDE2OjM4OjI1In0=.5HyjR60die0HkQ6OWDF93os3gwWk60KojwWnlGiw5ds=' \
  --header 'content-type: application/json' \
  --cookie PHPSESSID=1ss7cakdkkct1714321be413n2 \
  --data '[
	{
		"CodigoEmpresa": "FILIAL 1",
		"CodigoCliente": "008432008",
		"Nome": "34 GAS LTDA ",
		"NomeEndereco": "ROD BR 386 ",
		"Bairro": "JD PRIMAVERA ",
		"Cidade": "Sao Jose",
		"UF": "SP"
	}
]'

Retorno

[
  {
    "type": "success",
    "msg": "Cadastro de Cliente inserido com sucesso",
    "field": "",
    "id": "12693"
  }
]

Erros Frequentes de Sincronismo

Este item já existe! Você pode editá-lo ou inserir um novo

Quando aparecer a seguinte mensagem:

[
  {
    "type": "error",
    "msg": "Erro ao inserir dados de NOME_TABELA : Este item já existe! Você pode editá-lo ou inserir um novo",
    "field": "",
    "id": ""
  }
]

Provavelmente foi tentado inserir um novo registro e ocorreu esse erro, a PRIMEIRA coisa a fazer é OLHAR para ver se não tem ERRO no log, filtre pelo DOMAIN que está trabalhando e level igual a ERROR. A maioria dos casos vai ser onde um registro foi marcado como deleted ou ativo igual a 0, porém, existe um CONSTRAINT no banco de dados que impede de inserir um novo registro, então a API tentou inserir um novo registro, mas no caso o MYSQL não permitiu a nova inserção por violar uma constraint definida no banco de dados. Para resolver isso cabe o Dev avaliar se é melhor mudar algo na API ou excluir a constraint do banco de dados.

Os erros no log quando é de constraint geralmente são:

SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry VALORES_CONSTRAINT for key NOME_CONSTRAINT

Exemplo:

sql = INSERT INTO APP_PLANEJAMENTO_VIAGEM ( SEQ_DB,  EMPRESA,  FILIAL,  LOCAL,  SOURCE,  UPD_DH,  INS_DH,  ID,  NUMERO_OR,  DATA,  DATA_ENVIO_INTERFACE,  SERIE_OR,  CENTRO,  MAQUINA,  VIAGENS,  POSICAO_DEPOSITO,  MAQUINA_SOLICITANTE,  NUMERO_DEPOSITO,  TIPO_MOVIMENTO,  TIPO_DEPOSITO,  ROTAS,  POSICAO_ORIGEM,  AGENTE_FRETE,  MATERIAL,  DEPOSITO,  DESC_ENVIO_INTERFACE,  FLAG_OR_MANUAL,  FLAG_CONTINGENCIA,  CODIGO_INTEGRACAO,  PEDIDO,  PROC_ST,  PROC_DH,  PROC_DESC,  RO,  ATIVO,  DELETED,  INS_USUARIO_SEQ_DB,  UPD_USUARIO_SEQ_DB) VALUES (0,1,3,3,NULL,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP, (SELECT (IFNULL(MAX(T.ID), 0) + 1) FROM APP_PLANEJAMENTO_VIAGEM T  WHERE T.EMPRESA = 1  AND T.FILIAL IN (3, 9999)  AND T.LOCAL IN (3, 9999) ),'0000000','2023-05-16',null,'A','1369','ECM7A87','1','','','MA2','SC','I2A',NULL,'304-11B001','0000710664','000000000019000088','MA2P','',0,'0','06','SN','0',null,'','0',1,0,47,NULL); getMessage = SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '2023-05-16 00:00:00-ECM7A87-1369-1' for key 'app_planejamento_viagem_un'

Integração

Integração é quando o dado sai do NFS e vai para o sistema de terceiros.

Ele é feito em duas partes a primeira é a obtenção dos dados no NFS por meio de um id. A outra parte é uma confirmação que de que os dados já enviados estão corretos, com isso é enviado o id novamente por meio de outra URL e os campos necessários são atualizados para não haver mais o sincronismo da mesma informação.

Rotas

MétodoURLDescrição
POST/nfs/api/v1.1/integration/{table}Realiza alteração nos dados da tabela
POST/nfs/api/v1/integration/list/{table}Obtém dados

Configuração Integração

integration_structure.png

A estrutura da integração pode ser dividida em 4 partes

Topo

integration_structure_1.png

ChaveObrigatórioValorDescrição
aliasSimApelidoTabelaDeve ser um apelido para tabela sem espaço.
tableSimTABELA_NOMEÉ a Tabela Nome que está na nfs_core_ds_tabela, usada para referênciar a uma tabela do NFS

field

integration_structure_2.png

Composto pelos campos enviados, geramente é um id que no NFS representa o SEQ_DB de um dado da tabela a ser sincronizada.

ChaveObrigatórioValorDescrição
columnSimSTRINGNome da Coluna no NFS.
typeSimVarcharTipo da Coluna, geralmente é varchar
sizeSimINTEGERTamanho maximo do dado que está sendo sincronizado, ainda não é usado esse valor, mas será útil para validações futuras
descriptionNãoSTRINGDescrição da chave/Coluna, pode ser que isso ajude na manutenção para outras pessoas entenderem o que foi realizado, para que ser, de onde pertence.
primaryKeyNãotrue/falseIndica que aquela chave/coluna é chave primária, pode até não ser no banco mas ela é importanta para fazer buscas verificando se existe o campo no banco do NFS para ver se é necessário fazer um update ou insert
requiredNãotrue/falseÉ um atributo importante, se marcado como true exije que o sistema de terceiro envie o valor, se não enviar será enviado uma mensagem de erro dizendo que o campo é obrigatório. Lembrando que se o campo não é obrigado não é necessário usar esse atributo

updateIntegraion

integration_structure_3.png

Um array onde ficam os campos que vão ser atualizados depois que o sincronismo for feito com sucesso.

output

integration_structure_4.png

Configuração dos dados que vão ser retornados para o sistema do cliente.

ChaveObrigatórioValorDescrição
aliasSimApelidoTabelaDeve ser um apelido para tabela sem espaço.

Exemplos

Exemplo de Sincronismo de OS

{
  "alias": "OrdemServico",
  "table": "OS",
  "field": {
    "id": {
      "column": "SEQ_DB",
      "type": "int",
      "size": 100,
      "description": "SEQ_DB da OS",
      "required": true
    }
  },
  "updateIntegration": [
    "RO = 1",
    "PROC_DH = CURRENT_TIMESTAMP",
    "PROC_ST = 1",
    "PROC_DESC = 'OS Integrada com sucesso para o DMS'"
  ],
  "output": {
    "FILIAL": {
      "field": "CodigoEmpresa",
      "type": "sys",
      "returnColumns": [
        "SIGLA"
      ]
    },
    "LOCAL": {
      "field": "FilialOS",
      "type": "sys",
      "returnColumns": [
        "SIGLA"
      ]
    },
    "CODIGO": {
      "field": "CodigoOS",
      "type": "varchar"
    },
    "LINHA": {
      "field": "CodigoLinhaEquip"
    },
    "MODELO": {
      "field": "CodigoModeloEquip",
      "type": "varchar"
    },
    "STATUS_OS_SEQ_DB": {
      "field": [
        "StatusOS",
        "IDStatusOS"
      ],
      "type": "varchar",
      "fk": true,
      "tableOrigin": "STATUS_OS",
      "columns": [
        "DESCRICAO",
        "ID"
      ]
    },
    "Equipamento": {
      "table": "APONTAMENTO_EQUIPAMENTO",
      "type": "child",
      "description": "Vinculo com OS",
      "columns": {
        "CHASSI": {
          "field": "Chassi"
        },
        "HORIMETRO": {
          "field": "Horimetro"
        },
        "FOTO_CHASSI": {
          "field": "IdImagemChassi",
          "type": "image"
        },
        "FOTO_HORIMETRO": {
          "field": "IdImagemHorimetro",
          "type": "image"
        },
        "INI_DH": {
          "field": "DataLeitura"
        }
      }
    },
    "Dtac": {
      "table": "APONTAMENTO_DTAC",
      "type": "child",
      "description": "Vinculo com OS",
      "columns": {
        "CODIGO_PECA_CAUSADORA": {
          "field": "PecaCausadora"
        }
      }
    },
    "Assinaturas": {
      "table": "APONTAMENTO_ASSINATURA",
      "type": "child",
      "description": "Vinculo com OS",
      "columns": {
        "ASSINATURA_CLIENTE": {
          "field": "IdImagemAssinaturaCliente",
          "type": "image"
        },
        "NOME_CLIENTE": {
          "field": "Contato"
        },
        "TELEFONE": {
          "field": "Telefone"
        }
      }
    },
    "Problemas": {
      "table": "APONTAMENTO_FOTO",
      "type": "child",
      "description": "Vinculo com OS",
      "columns": {
        "FOTO": {
          "field": "IdImagemProblema",
          "type": "image"
        },
        "OBS": {
          "field": "DescFalha"
        }
      }
    },
    "AptHorasTrabalhadas": {
      "table": "APONTAMENTO",
      "type": "child",
      "description": "Vinculo com OS",
      "columns": {
        "SERVICO_SEQ_DB": {
          "field": [
            "SeqServico"
          ],
          "type": "varchar",
          "fk": true,
          "tableOrigin": "SERVICO",
          "columns": [
            "SEQ_OPERACAO"
          ]
        },
        "FILIAL": {
          "field": "CodigoEmpresaTec",
          "returnColumns": [
            "SIGLA"
          ]
        },
        "INI_DH": {
          "field": "DataInicialApontamento"
        },
        "FIM_DH": {
          "field": "DataFinalApontamento"
        },
        "INI_FIM_DIFF_SEC": {
          "field": "TempoApontado",
          "type": "time",
          "mask": "minute"
        },
        "AUXILIAR_SEQ_DB": {
          "field": [
            "CodigoTecnico"
          ],
          "type": "varchar",
          "fk": true,
          "tableOrigin": "FUNCIONARIO",
          "columns": [
            "CRACHA"
          ]
        }
      }
    },
    "DescFalha": {
      "table": "APONTAMENTO_DESCRICAO_SERVICO",
      "type": "child",
      "description": "Vinculo com OS",
      "columns": {
        "RECLAMACAO_CLIENTE": {
          "field": "DescricaoFalha",
          "type": "varchar"
        },
        "CAUSA": {
          "field": "DescricaoCausa",
          "type": "varchar"
        },
        "SOLUCAO": {
          "field": "DescricaoCorrecao",
          "type": "varchar"
        }
      }
    },
    "PecasAplicacas": {
      "table": "OS_PECA",
      "type": "child",
      "description": "Vinculo com OS",
      "columns": {
        "PECA_SEQ_DB": {
          "field": [
            "CodigoPeca"
          ],
          "type": "varchar",
          "fk": true,
          "tableOrigin": "PECA",
          "columns": [
            "CODIGO"
          ]
        },
        "QTD_UTILIZADA": {
          "field": "QuantidadeAplicada",
          "type": "varchar"
        }
      }
    },
    "Deslocamentos": {
      "table": "APONTAMENTO_DESLOCAMENTO",
      "type": "child",
      "description": "Vinculo com OS",
      "columns": {
        "VEICULO_SEQ_DB": {
          "field": [
            "VeiculoPlaca"
          ],
          "type": "varchar",
          "fk": true,
          "tableOrigin": "VEICULO",
          "columns": [
            "PLACA"
          ]
        },
        "FUNCIONARIO_SEQ_DB": {
          "field": [
            "Tecnico"
          ],
          "type": "varchar",
          "fk": true,
          "tableOrigin": "FUNCIONARIO",
          "columns": [
            "CRACHA"
          ]
        },
        "INI_DH": {
          "field": "DataHoraInicio"
        },
        "FIM_DH": {
          "field": "DataHoraFim"
        },
        "KM_INICIAL": {
          "field": "KmInicial"
        },
        "KM_FINAL": {
          "field": "KmFinal"
        },
        "DIFERENCA_KM": {
          "field": "KmPercorrido"
        }
      }
    },
    "Despesas": {
      "table": "APONTAMENTO_DESPESA",
      "type": "child",
      "description": "Vinculo com OS",
      "columns": {
        "FUNCIONARIO_SEQ_DB": {
          "field": [
            "Tecnico"
          ],
          "type": "varchar",
          "fk": true,
          "tableOrigin": "FUNCIONARIO",
          "columns": [
            "CRACHA"
          ]
        },
        "DESPESA_SEQ_DB": {
          "field": [
            "Despesa"
          ],
          "type": "varchar",
          "fk": true,
          "tableOrigin": "DESPESA",
          "columns": [
            "DESCRICAO"
          ]
        },
        "DESCRICAO_DESPESA": {
          "field": "DescricaoDespesa",
          "type": "varchar"
        },
        "INI_DH": {
          "field": "DataHora"
        },
        "QUANTIDADE_DESPESA": {
          "field": "Valor",
          "type": "varchar"
        }
      }
    }
  }
}

DeepFK

A Deep FK permitir você a obter dados de outras tabelas na sua tabela alvo, exemplo de problema:

  • Necessário enviar o código do Modelo relacionado ao Equipamento da Tabela OS e o Tipo Equipamento relacionado ao Modelo.

Configuração:

"EQUIPAMENTO_SEQ_DB": {
      "type": "deepFK",
      "fk": true,
      "name": "EQUIPAMENTO",
      "tableOrigin": "EQUIPAMENTO",
      "fields": {
        "codigoModeloEquip": {
          "type": "fk",
          "tableOrigin": "MODELO_EQUIPAMENTO",
          "column": "MODELO_EQUIPAMENTO_SEQ_DB",
          "fields": {
            "CODIGO": {
              "type": "varchar"
            }
          }
        },
        "codigoLinhaEquip": {
          "type": "fk",
          "tableOrigin": "MODELO_EQUIPAMENTO",
          "column": "MODELO_EQUIPAMENTO_SEQ_DB",
          "fields": {
            "TIPO_EQUIPAMENTO": {
              "type": "fk",
              "tableOrigin": "TIPO_EQUIPAMENTO",
              "column": "TIPO_EQUIPAMENTO_SEQ_DB",
              "fields": {
                "CODIGO": {
                  "type": "varchar"
                }
              }
            }
          }
        }
    }

Outra opção é ser possível também obter qualquer outra coluna da própria tabela Equipamento.

"EQUIPAMENTO_SEQ_DB": {
      "type": "deepFK",
      "fk": true,
      "name": "EQUIPAMENTO",
      "tableOrigin": "EQUIPAMENTO",
      "fields": {
        "codigoModeloEquip": {
          "type": "fk",
          "tableOrigin": "MODELO_EQUIPAMENTO",
          "column": "MODELO_EQUIPAMENTO_SEQ_DB",
          "fields": {
            "CODIGO": {
              "type": "varchar"
            }
          }
        },
        "codigoLinhaEquip": {
          "type": "fk",
          "tableOrigin": "MODELO_EQUIPAMENTO",
          "column": "MODELO_EQUIPAMENTO_SEQ_DB",
          "fields": {
            "TIPO_EQUIPAMENTO": {
              "type": "fk",
              "tableOrigin": "TIPO_EQUIPAMENTO",
              "column": "TIPO_EQUIPAMENTO_SEQ_DB",
              "fields": {
                "CODIGO": {
                  "type": "varchar"
                }
              }
            }
          }
        },
        "chassi": {
          "type": "varchar",
          "column": "CHASSI"
        }
      }
    }

Exemplo completo:

{
  "alias": "OrdemServico",
  "table": "OS",
  "ignore": {
    "local": true
  },
  "field": {
    "SeqOs": {
      "column": "SEQ_DB",
      "type": "int",
      "size": 100,
      "description": "SEQ_DB da OS",
      "required": true
    }
  },
  "conditions": [
    "PROC_ST = 1"
  ],
  "updateIntegration": [
    "RO = 1",
    "PROC_DH = CURRENT_TIMESTAMP",
    "PROC_ST = 2",
    "PROC_DESC = 'OS Integrada com sucesso para o DMS'"
  ],
  "output": {
    "FILIAL": {
      "field": "Filial",
      "type": "sys",
      "returnColumns": [
        "SIGLA"
      ]
    },
    "LOCAL": {
      "field": "Local",
      "type": "sys",
      "returnColumns": [
        "SIGLA"
      ]
    },
    "OsServico": {
      "table": "OS_SERVICO",
      "type": "child",
      "description": "Vinculo com OS",
      "columns": {
        "SEQ_DB": {
          "field": "idOsServico",
          "type": "varchar"
        }
      }
    },
    "EQUIPAMENTO_SEQ_DB": {
      "type": "deepFK",
      "fk": true,
      "name": "EQUIPAMENTO",
      "tableOrigin": "EQUIPAMENTO",
      "fields": {
        "codigoModeloEquip": {
          "type": "fk",
          "tableOrigin": "MODELO_EQUIPAMENTO",
          "column": "MODELO_EQUIPAMENTO_SEQ_DB",
          "fields": {
            "CODIGO": {
              "type": "varchar"
            }
          }
        },
        "codigoLinhaEquip": {
          "type": "fk",
          "tableOrigin": "MODELO_EQUIPAMENTO",
          "column": "MODELO_EQUIPAMENTO_SEQ_DB",
          "fields": {
            "TIPO_EQUIPAMENTO": {
              "type": "fk",
              "tableOrigin": "TIPO_EQUIPAMENTO",
              "column": "TIPO_EQUIPAMENTO_SEQ_DB",
              "fields": {
                "CODIGO": {
                  "type": "varchar"
                }
              }
            }
          }
        },
        "chassi": {
          "type": "varchar",
          "column": "CHASSI"
        }
      }
    },
    "CODIGO": {
      "field": "CodigoOS",
      "type": "varchar"
    },
    "STATUS_OS_SEQ_DB": {
      "field": [
        "StatusOS",
        "IDStatusOS"
      ],
      "type": "fk",
      "fk": true,
      "name": "STATUS_OS",
      "tableOrigin": "STATUS_OS",
      "columns": [
        "DESCRICAO",
        "ID"
      ]
    },
    "ApontamentoEquipamento": {
      "table": "APONTAMENTO_EQUIPAMENTO",
      "type": "child",
      "description": "Vinculo com OS",
      "columns": {
        "CHASSI": {
          "field": "Chassi",
          "type": "varchar"
        },
        "HORIMETRO": {
          "field": "Horimetro",
          "type": "varchar"
        },
        "FOTO_CHASSI": {
          "field": "IdImagemChassi",
          "type": "image"
        },
        "FOTO_HORIMETRO": {
          "field": "IdImagemHorimetro",
          "type": "image"
        },
        "INI_DH": {
          "field": "DataLeitura",
          "type": "varchar"
        }
      }
    },
    "Dtac": {
      "table": "APONTAMENTO_DTAC",
      "type": "child",
      "description": "Vinculo com OS",
      "columns": {
        "NUMERO_DTAC": {
          "field": "NumeroDtac",
          "type": "varchar"
        }
      }
    },
    "PecaCausadora": {
      "table": "APONTAMENTO_PECA_CAUSADORA",
      "type": "child",
      "description": "Vinculo com OS",
      "columns": {
        "CODIGO_PECA_CAUSADORA": {
          "field": "PecaCausadora",
          "type": "varchar"
        },
        "DESCRICAO_PECA_CAUSADORA": {
          "field": "DescricaoPecaCausadora",
          "type": "varchar"
        }
      }
    },
    "Assinaturas": {
      "table": "APONTAMENTO_ASSINATURA",
      "type": "child",
      "description": "Vinculo com OS",
      "columns": {
        "ASSINATURA_CLIENTE": {
          "field": "IdImagemAssinaturaCliente",
          "type": "image"
        },
        "NOME_CLIENTE": {
          "field": "Contato",
          "type": "varchar"
        },
        "TELEFONE": {
          "field": "Telefone",
          "type": "varchar"
        }
      }
    },
    "Problemas": {
      "table": "APONTAMENTO_FOTO",
      "type": "child",
      "description": "Vinculo com OS",
      "columns": {
        "FOTO": {
          "field": "IdImagemProblema",
          "type": "image"
        },
        "OBS": {
          "field": "DescFalha",
          "type": "varchar"
        }
      }
    },
    "AptHorasTrabalhadas": {
      "table": "APONTAMENTO",
      "type": "child",
      "description": "Vinculo com OS",
      "columns": {
        "SERVICO_SEQ_DB": {
          "field": [
            "SeqServico"
          ],
          "type": "varchar",
          "fk": true,
          "tableOrigin": "SERVICO",
          "columns": [
            "SEQ_DB"
          ]
        },
        "GRUPO_SERVICO_SEQ_DB": {
          "field": [
            "CodigoServico",
            "DescServico"
          ],
          "type": "varchar",
          "fk": true,
          "tableOrigin": "GRUPO_SERVICO",
          "columns": [
            "CODIGO",
            "DESCRICAO"
          ]
        },
        "OS_SEQ_DB": {
          "field": [
            "SeqOs",
            "CodigoOs"
          ],
          "type": "varchar",
          "fk": true,
          "tableOrigin": "OS",
          "columns": [
            "SEQ_DB",
            "CODIGO"
          ]
        },
        "PARADA_SEQ_DB": {
          "field": [
            "CodigoParada",
            "DescParada"
          ],
          "type": "varchar",
          "fk": true,
          "tableOrigin": "PARADA",
          "columns": [
            "ID",
            "DESCRICAO"
          ]
        },
        "TIPO_APONTAMENTO": {
          "field": "TipoApontamento",
          "type": "int"
        },
        "FILIAL": {
          "field": "CodigoEmpresaTec",
          "type": "sys",
          "returnColumns": [
            "SIGLA"
          ]
        },
        "FLAG_OUTROS_SERVICOS": {
          "field": "FlagServicoManual",
          "type": "varchar"
        },
        "INI_DH": {
          "field": "DataInicialApontamento",
          "type": "varchar"
        },
        "FIM_DH": {
          "field": "DataFinalApontamento",
          "type": "varchar"
        },
        "INI_FIM_DIFF_SEC": {
          "field": "TempoApontado",
          "type": "time",
          "mask": "minute"
        },
        "AUXILIAR_SEQ_DB": {
          "field": [
            "CodigoTecnico"
          ],
          "type": "varchar",
          "fk": true,
          "tableOrigin": "FUNCIONARIO",
          "columns": [
            "CRACHA"
          ]
        }
      }
    },
    "DescFalha": {
      "table": "APONTAMENTO_DESCRICAO_SERVICO",
      "type": "child",
      "description": "Vinculo com OS",
      "columns": {
        "RECLAMACAO_CLIENTE": {
          "field": "DescricaoFalha",
          "type": "varchar"
        },
        "CAUSA": {
          "field": "DescricaoCausa",
          "type": "varchar"
        },
        "SOLUCAO": {
          "field": "DescricaoCorrecao",
          "type": "varchar"
        }
      }
    },
    "PecasAplicadas": {
      "table": "OS_PECA",
      "type": "child",
      "description": "Vinculo com OS",
      "columns": {
        "PECA_SEQ_DB": {
          "field": [
            "CodigoPeca"
          ],
          "type": "varchar",
          "fk": true,
          "tableOrigin": "PECA",
          "columns": [
            "CODIGO"
          ]
        },
        "QTD_UTILIZADA": {
          "field": "QuantidadeAplicada",
          "type": "varchar"
        }
      }
    },
    "Deslocamentos": {
      "table": "APONTAMENTO_DESLOCAMENTO",
      "type": "child",
      "description": "Vinculo com OS",
      "columns": {
        "VEICULO_SEQ_DB": {
          "field": [
            "VeiculoPlaca"
          ],
          "type": "varchar",
          "fk": true,
          "tableOrigin": "VEICULO",
          "columns": [
            "PLACA"
          ]
        },
        "FUNCIONARIO_SEQ_DB": {
          "field": [
            "Tecnico"
          ],
          "type": "varchar",
          "fk": true,
          "tableOrigin": "FUNCIONARIO",
          "columns": [
            "CRACHA"
          ]
        },
        "INI_DH": {
          "field": "DataHoraInicio",
          "type": "varchar"
        },
        "FIM_DH": {
          "field": "DataHoraFim",
          "type": "varchar"
        },
        "KM_INICIAL": {
          "field": "KmInicial",
          "type": "varchar"
        },
        "KM_FINAL": {
          "field": "KmFinal",
          "type": "varchar"
        },
        "DIFERENCA_KM": {
          "field": "KmPercorrido",
          "type": "varchar"
        }
      }
    },
    "Despesas": {
      "table": "APONTAMENTO_DESPESA",
      "type": "child",
      "description": "Vinculo com OS",
      "columns": {
        "FUNCIONARIO_SEQ_DB": {
          "field": [
            "Tecnico"
          ],
          "type": "varchar",
          "fk": true,
          "tableOrigin": "FUNCIONARIO",
          "columns": [
            "CRACHA"
          ]
        },
        "DESPESA_SEQ_DB": {
          "field": [
            "Despesa"
          ],
          "type": "varchar",
          "fk": true,
          "tableOrigin": "DESPESA",
          "columns": [
            "DESCRICAO"
          ]
        },
        "DESCRICAO_DESPESA": {
          "field": "DescricaoDespesa",
          "type": "varchar"
        },
        "INI_DH": {
          "field": "DataHora",
          "type": "varchar"
        },
        "QUANTIDADE_DESPESA": {
          "field": "Valor",
          "type": "varchar"
        }
      }
    }
  }
}

Integração - Consulta

É possível também realizar a consulta de dados sem que seja feita qualquer alterção neles.

Para isso é necessário 2 coisas:

1 - O método ser GET ao invés de POST

curl --request GET \
  --url http://nfs.local/nfs/api/v1/integration/apontamento \
  --header 'authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJuZnMubG9jYWwiLCJuYW1lIjoic2ltb3ZhLmFkbWluIiwiZW1haWwiOiJzaW1vdmEuYWRtaW5Ac2ltb3ZhLmNvbS5iciIsImZpbGlhbCI6IkFyYWd1YXJpIiwibG9jYWwiOm51bGwsImV4cCI6IjIwMjAtMDQtMjMgMTE6NDg6NDIifQ==.8Yc6ZupCzQIxhsbH\/V6Rzo456uudnt\/8loPgMqmKQVI=' \
  --header 'content-type: application/json' \
  --cookie PHPSESSID=bt409rccmaotg830011ja5um15 \
  --data '{
  "periodo": {
    "end": "2020-01-06 17:54:43",
    "begin" : "2020-01-01 17:54:40"
  }
}'

2 - Na configuração do updateIntegraion o mesmo deve ser vazio.

Type period

Foi criado um novo tipo também para esses casos que é o period, configuração básica é

{
  "alias": "Apontamento",
  "table": "APONTAMENTO",
  "field": {
    "periodo": {
      "column": "INI_DH",
      "type": "period",
      "size": 100,
      "description": "Periodo Busca de Apontamentos"
    }
  }
  ....
}

no Body do request deve ter a data begin (Começo) e end (Fim)

{
  "periodo": {
    "end": "2020-01-06 17:54:43",
    "begin" : "2020-01-01 17:54:40"
  }
}

OBS: Pode ser adicionado no período a filial e o local para que possa obter os dados que correspondem apenas a estes, podendo ser inserido os dois ou apenas um ou outro, caso não sejam colocados serão trazidos os dados da filial que o usuario esta autenticado, como no exemplo abaixo:

"periodo": {
    "end": "2020-01-06 17:54:43",
    "begin" : "2020-01-01 17:54:40",
    "filial": "NomeDaFilial",
    "local": "NomeDoLocal"
  }

Logo no exemplo acima vai pesquisar WHERE INI_DH >= '2020-01-06 17:54:43' and INI_DH <= '2020-01-01 17:54:40', o formato da datas deve ser YYYY-MM-DD hh:mi:ss.

Ao definir um tipo period o mesmo deve ser enviado como um json com duas chaves begin e end, nota que o end nao pode ser maior que o begin se nao é gerada uma mensagem de validaçao

[
  {
    "type": "validation",
    "msg": "Período inválido, data inicial maior que final",
    "field": "",
    "id": ""
  }
]

e nem o periodo maior que sete dias.

[
  {
    "type": "validation",
    "msg": "Período maior que 7 dias",
    "field": "",
    "id": ""
  }
]

Type user

Na integração para retornar informações do usuário como por exemplo e-mail foi criado o tipo user onde busca os dados na tabela nfs_acl_usuario.

"INS_USUARIO_SEQ_DB": {
  "field": [
    "INS_USUARIO_SEQ_DB"
  ],
  "type": "user",
  "columns": [
    "EMAIL"
  ]
}

No field um array com os apelidos que vão retornar no json na columns as colunas de retorno nfs_acl_usuario que deseja obter as informações.

Exemplo de Integração de Apontamentos

Request

curl --request GET \
  --url http://nfs.local/nfs/api/v1/integration/apontamento \
  --header 'authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJuZnMubG9jYWwiLCJuYW1lIjoic2ltb3ZhLmFkbWluIiwiZW1haWwiOiJzaW1vdmEuYWRtaW5Ac2ltb3ZhLmNvbS5iciIsImZpbGlhbCI6IkFyYWd1YXJpIiwibG9jYWwiOm51bGwsImV4cCI6IjIwMjAtMDQtMjMgMTE6NDg6NDIifQ==.8Yc6ZupCzQIxhsbH\/V6Rzo456uudnt\/8loPgMqmKQVI=' \
  --header 'content-type: application/json' \
  --cookie PHPSESSID=bt409rccmaotg830011ja5um15 \
  --data '{
  "periodo": {
    "end": "2020-01-06 17:54:43",
    "begin" : "2020-01-01 17:54:40"
  }
}'

SQL

{
  "alias": "Apontamento",
  "table": "APONTAMENTO",
  "field": {
    "periodo": {
      "column": "INI_DH",
      "type": "period",
      "size": 100,
      "description": "Periodo Busca de Apontamentos"
    }
  },
  "updateIntegration": [],
  "output": {
    "EMPRESA": {
      "field": "NOME_EMPRESA",
      "type": "sys",
      "returnColumns": [
        "SIGLA"
      ]
    },
    "FILIAL": {
      "field": "NOME_FILIAL",
      "type": "sys",
      "returnColumns": [
        "SIGLA"
      ]
    },
    "LOCAL": {
      "field": "LOCAL",
      "type": "sys",
      "returnColumns": [
        "SIGLA"
      ]
    },
    "INS_DH": {
      "field": "DT_INS_APONT",
      "type": "varchar"
    },
    "INI_DH": {
      "field": "DT_INI_APONT",
      "type": "varchar"

    },
    "FIM_DH": {
      "field": "DT_FIM_APONT",
      "type": "varchar"
    },
    "SEQ_DB_DEVICE": {
      "field": "SEQ_APONTAMENTO",
      "type": "varchar"
    },
    "ATIVO": {
      "field": "STATUS_APONT",
      "type": "varchar"
    },
    "INI_DH_TIPO": {
      "field": "TIPO_DT_INI",
      "type": "varchar"
    },
    "INI_DH_ONLINE": {
      "field": "INI_ONLINE",
      "type": "varchar"
    },
    "FIM_DH_TIPO": {
      "field": "TIPO_DT_FIM",
      "type": "varchar"
    },
    "FIM_DH_ONLINE": {
      "field": "FIM_ONLINE",
      "type": "varchar"
    },
    "OS_CRIADA": {
      "field": "COD_OS_OFFLINE",
      "type": "varchar"
    },
    "FLAG_DESLOCAMENTO": {
      "field": "FLAG_DESLOCAMENTO",
      "type": "varchar"
    },
    "POSICAO_PLAT": {
      "field": "POSICAO_LAT_APONT",
      "type": "varchar"
    },
    "POSICAO_PLON": {
      "field": "POSICAO_LON_APONT",
      "type": "varchar"
    },
    "PROC_DH": {
      "field": "DT_INTEGRACAO",
      "type": "varchar"
    },
    "UPD_DH": {
      "field": "DT_ALTERACAO",
      "type": "varchar"
    },
    "FLAG_DIAGNOSTICO": {
      "field": "FLAG_DIAGNOSTICO",
      "type": "varchar"
    },
    "INS_USUARIO_SEQ_DB": {
      "field": [
        "INS_USUARIO_SEQ_DB"
      ],
      "type": "user",
      "columns": [
        "EMAIL"
      ]
    },
     "UPD_USUARIO_SEQ_DB": {
      "field": [
        "USUARIO_ALT_APONT"
      ],
      "type": "user",
      "columns": [
        "EMAIL"
      ]
    },
    "OBSERVACAO": {
      "field": "OBSERVACAO",
      "type": "varchar"
    },
    "TIPO_APONTAMENTO": {
      "field": "TIPO_APONTAMENTO",
      "type": "varchar"
    },
    "FUNCIONARIO_SEQ_DB": {
      "field": [
        "CRACHA_FUNCIONARIO",
        "NOME_FUNCIONARIO"
      ],
      "type": "fk",
      "fk": true,
      "tableOrigin": "FUNCIONARIO",
      "columns": [
        "CRACHA",
        "NOME"
      ]
    },
    "AUXILIAR_SEQ_DB": {
      "field": [
        "CRACHA_AUXILIAR",
        "NOME_AUXILIAR"
      ],
      "type": "fk",
      "fk": true,
      "tableOrigin": "FUNCIONARIO",
      "columns": [
        "CRACHA",
        "NOME"
      ]
    },
    "OS_SEQ_DB": {
      "field": [
        "OS_COD"
      ],
      "type": "fk",
      "fk": true,
      "tableOrigin": "OS",
      "columns": [
        "CODIGO"
      ]
    },
     "CLIENTE_SEQ_DB": {
      "field": [
        "COD_CLIENTE",
        "LOJA_CLIENTE",
        "DESC_CLIENTE"
      ],
      "type": "fk",
      "fk": true,
      "tableOrigin": "CLIENTE",
      "columns": [
        "CODIGO",
        "LOJA",
        "DESCRICAO"
      ]
    },
    "GRUPO_SERVICO_SEQ_DB": {
      "field": [
        "COD_GRUPO_SERVICO",
        "DESC_GRUPO_SERVICO"
      ],
      "type": "fk",
      "fk": true,
      "tableOrigin": "GRUPO_SERVICO",
      "columns": [
        "CODIGO",
        "DESCRICAO"
      ]
    },
    "SERVICO_SEQ_DB": {
      "field": [
        "COD_SERVICO",
        "DESC_SERVICO"
      ],
      "type": "fk",
      "fk": true,
      "tableOrigin": "SERVICO",
      "columns": [
        "CODIGO",
        "DESCRICAO"
      ]
    },
    "PARADA_SEQ_DB": {
      "field": [
        "DESC_PARADA"
      ],
      "type": "fk",
      "fk": true,
      "tableOrigin": "PARADA",
      "columns": [
        "DESCRICAO"
      ]
    },
    "TIPO_SERVICO_SEQ_DB": {
      "field": [
        "CD_TIPO_SERV",
        "DESC_TIPO_SERV"
      ],
      "type": "fk",
      "fk": true,
      "tableOrigin": "TIPO_SERVICO",
      "columns": [
        "CODIGO",
        "DESCRICAO"
      ]
    },
    "TIPO_TEMPO_SEQ_DB": {
      "field": [
        "CD_TIPO_TEMP",
        "DESC_TIPO_TEMP"
      ],
      "type": "fk",
      "fk": true,
      "tableOrigin": "TIPO_TEMPO",
      "columns": [
        "CODIGO",
        "DESCRICAO"
      ]
    },
    "LOCAL_SERVICO_SEQ_DB": {
      "field": [
        "CD_LOCAL_SERV",
        "DESC_LOCAL_SERV"
      ],
      "type": "fk",
      "fk": true,
      "tableOrigin": "LOCAL_SERVICO",
      "columns": [
        "CODIGO",
        "DESCRICAO"
      ]
    },
    "SEQ_DB_DEVICE_MASTER_SEQ_DB": {
      "field": [
        "DT_INS_BOL",
        "DT_INI_BOL",
        "DT_FIM_BOL",
        "BOL_INTEGRADO"
      ],
      "type": "fk",
      "fk": true,
      "tableOrigin": "BOLETIM",
      "columns": [
        "INS_DH",
        "INI_DH",
        "FIM_DH",
        "PROCT_ST"
      ]
    }
  }
}

Alteração Pós Integração

Caso seja necessário realizar uma alteração para marcar os dados como integrados e também passar por um entry point, o melhor candidato é a rota de Alteração Pós Integração.

Rota: /nfs/api/v1.1/integration/{table}

Caso seja necessário fixar alguma informação automáticamente, use a configuração after-integration:

"after-integration": {
	  "update": {
	     "RO": 1,
	     "PROC_DH": "CURRENT_TIMESTAMP",
	     "PROC_ST": 4,
	     "PROC_DESC": "OS Integrada com Sucesso!!"
	  }
	},

Dentro da chave update, adicione as colunas e seus valores para serem alterados.

curl

curl --request POST \
  --url https://nfs.local/nfs/api/v1.1/integration/os \
  --header 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJuZnMubG9jYWwiLCJuYW1lIjoic2ltb3ZhLmFkbWluQHNpbW92YS5jb20uYnIiLCJlbWFpbCI6InNpbW92YS5hZG1pbkBzaW1vdmEuY29tLmJyIiwiZmlsaWFsIjoiVkVTUFVDSU8iLCJsb2NhbCI6IjEwQVZTUDIwIiwiZXhwIjoiMjAyNS0wOC0yOSAyMDowODozOCJ9.ZQ33N4mwsAEurobAKHDLjL4mXq1C22zuXFLGl9SO7mo=' \
  --header 'Content-Type: application/json' \
  --header 'User-Agent: insomnia/11.2.0' \
  --cookie PHPSESSID=9p3g4gne2d91p72phhu29r8eqq \
  --data '{"CodigoOt":"000043201168" }'

No exemplo do CURL a OS com código 000043201168, vai ser ter o campos alterados para:

  • RO igual a 1
  • PROD_DH igual a data atual da filial
  • PROC_ST igual a 4
  • PROC_DESC igual ao texto: OS Integrada com Sucesso!!

Exemplo da configuração completa

{
    "alias": "OrdemServico",
    "table": "OS",
  "conditions": [
    "PROC_ST IN (1)"
  ],
  "msgForConditions": "Registro já foi lido!",
    "field":
    {
        "Periodo":
        {
            "column": "INI_DH",
            "type": "period",
            "size": 100,
            "description": "Periodo Busca de Apontamentos"
        },
        "CodigoOt":
        {
            "column": "CODIGO",
            "type": "varchar",
            "size": 100,
            "description": "Codigo da OS"
        }
    },
    "updateIntegration":
    [
        "RO = 1",
        "PROC_DH = CURRENT_TIMESTAMP",
        "PROC_ST = 4",
        "PROC_DESC = 'OS Integrada com sucesso'"
    ],
    "after-integration": {
	  "update": {
	     "RO": 1,
	     "PROC_DH": "CURRENT_TIMESTAMP",
	     "PROC_ST": 4,
	     "PROC_DESC": "OS Integrada com Sucesso!!"
	  }
	},
    "output":
    {
        "FILIAL":
        {
            "field": "Filial",
            "type": "sys",
            "returnColumns":
            [
                "SIGLA"
            ]
        },
        "LOCAL":
        {
            "field": "Local",
            "type": "sys",
            "returnColumns":
            [
                "SIGLA"
            ]
        },
        "CODIGO":
        {
            "field": "CodigoOs",
            "type": "varchar"
        },
        "EQUIPAMENTO_SEQ_DB":
        {
            "field":
            [
                "Chassi"
            ],
            "type": "fk",
            "fk": true,
            "tableOrigin": "EQUIPAMENTO",
            "columns":
            [
                "CHASSI"
            ]
        },
        "Apontamento":
        {
            "table": "apontamento",
            "type": "child",
            "description": "Vinculo com OS",
            "columns":
            {
                "SEQ_DB_DEVICE_MASTER_SEQ_DB":
                {
                    "field":
                    [
                        "SeqBoletim",
                        "DataInicialBoletim",
                        "DataFinalBoletim"
                    ],
                    "type": "fk",
                    "fk": true,
                    "tableOrigin": "BOLETIM",
                    "columns":
                    [
                        "SEQ_DB_DEVICE",
                        "INI_DH",
                        "FIM_DH"
                    ]
                },
                "SEQ_DB_DEVICE":
                {
                    "field": "SequencialApontamento",
                    "type": "varchar"
                },
                "INI_DH":
                {
                    "field": "DataInicialApontamento",
                    "type": "varchar"
                },
                "FIM_DH":
                {
                    "field": "DataFinalApontamento",
                    "type": "varchar"
                },
                "TIPO_APONTAMENTO":
                {
                    "field": "TipoApontamento",
                    "type": "varchar"
                },
                "FUNCIONARIO_SEQ_DB":
                {
                    "field":
                    [
                        "NomeFuncionario",
                        "CrachaFuncionario"
                    ],
                    "type": "fk",
                    "fk": true,
                    "tableOrigin": "FUNCIONARIO",
                    "columns":
                    [
                        "NOME",
                        "CRACHA"
                    ]
                },
                "SERVICO_SEQ_DB":
                {
                    "field":
                    [
                        "CodigoServico",
                        "DescricaoServico",
                        "FlagDeslocamento"
                    ],
                    "type": "fk",
                    "fk": true,
                    "tableOrigin": "SERVICO",
                    "columns":
                    [
                        "CODIGO",
                        "DESCRICAO",
                        "FLAG_DESLOCAMENTO"
                    ]
                },
                "GRUPO_SERVICO_SEQ_DB":
                {
                    "field":
                    [
                        "CodigoGrupoServico",
                        "DescricaoGrupoServico"
                    ],
                    "type": "fk",
                    "fk": true,
                    "tableOrigin": "GRUPO_SERVICO",
                    "columns":
                    [
                        "CODIGO",
                        "DESCRICAO"
                    ]
                },
                "CLIENTE_SEQ_DB":
                {
                    "field":
                    [
                        "CodigoCliente",
                        "LojaCliente",
                        "DescricaoCliente"
                    ],
                    "type": "fk",
                    "fk": true,
                    "tableOrigin": "CLIENTE",
                    "columns":
                    [
                        "CODIGO",
                        "LOJA",
                        "DESCRICAO"
                    ]
                },
                "INI_FIM_DIFF_SEC":
                {
                    "field": "TempoApontado",
                    "type": "int"
                },
                "OS_CRIADA":
                {
                    "field": "OsCriada",
                    "type": "varchar"
                },
                "POSICAO_PLAT":
                {
                    "field": "Latitude",
                    "type": "varchar"
                },
                "POSICAO_PLON":
                {
                    "field": "Longitude",
                    "type": "varchar"
                }
            }
        },
        "aptDescricaoServico":
        {
            "table": "APONTAMENTO_DESCRICAO_SERVICO",
            "type": "child",
            "description": "Vinculo com OS",
            "columns":
            {
                "SEQ_DB_DEVICE":
                {
                    "field": "SequencialApontamento",
                    "type": "varchar"
                },
                "INI_DH":
                {
                    "field": "DataInicialApontamento",
                    "type": "varchar"
                },
                "FIM_DH":
                {
                    "field": "DataFinalApontamento",
                    "type": "varchar"
                },
                "SEQ_DB_DEVICE_MASTER_SEQ_DB":
                {
                    "field":
                    [
                        "SeqBoletim",
                        "DataInicialBoletim",
                        "DataFinalBoletim"
                    ],
                    "type": "fk",
                    "fk": true,
                    "tableOrigin": "BOLETIM",
                    "columns":
                    [
                        "SEQ_DB_DEVICE",
                        "INI_DH",
                        "FIM_DH"
                    ]
                },
                "FUNCIONARIO_SEQ_DB":
                {
                    "field":
                    [
                        "NomeFuncionario",
                        "CrachaFuncionario"
                    ],
                    "type": "fk",
                    "fk": true,
                    "tableOrigin": "FUNCIONARIO",
                    "columns":
                    [
                        "NOME",
                        "CRACHA"
                    ]
                },
                "CODIGO_OS":
                {
                    "field": "CodigoOsApontado",
                    "type": "varchar"
                },
                "OS_SEQ_DB":
                {
                    "field":
                    [
                        "CodigoOs"
                    ],
                    "type": "fk",
                    "fk": true,
                    "tableOrigin": "OS",
                    "columns":
                    [
                        "CODIGO"
                    ]
                },
                "OS_CRIADA":
                {
                    "field": "OsCriada",
                    "type": "varchar"
                },
                "CLIENTE_SEQ_DB":
                {
                    "field":
                    [
                        "CodigoCliente",
                        "DescricaoCliente",
                        "LojaCliente"
                    ],
                    "type": "fk",
                    "fk": true,
                    "tableOrigin": "CLIENTE",
                    "columns":
                    [
                        "CODIGO",
                        "DESCRICAO",
                        "LOJA"
                    ]
                },
                "RECLAMACAO_CLIENTE":
                {
                    "field": "ReclamacaoCliente",
                    "type": "varchar"
                },
                "CAUSA":
                {
                    "field": "Causa",
                    "type": "varchar"
                },
                "SOLUCAO":
                {
                    "field": "Solucao",
                    "type": "varchar"
                },
                "INI_FIM_DIFF_SEC":
                {
                    "field": "HorasApontamento",
                    "type": "varchar"
                }
            }
        }
    }
}

Imagens

As imagens não são enviadas junto com a resposta de integração, porque elas tendem a ser bem grandes, então para isso é preciso realizar em dois passos, primeiro obtem o SEQ_DB da imagem, e depois direto sua entidade obte-la.

Para isso temos dois types diferentes:

  • image: Retorna o SEQ_DB da imagem, para nós do NFS é SEQ_DB para o cliente podemos usar um apelido, igual ID
  • getImage: Retorna o base 64 da imagem

Exemplo para obter o SEQ_DB da Image

Buscamos na integração de OS a Foto do CHASSI e HORÍMETRO do Equipamento

{
  "alias": "OrdemServico",
  "table": "OS",
  "ignore": {
    "local": true
  },
  "field": {
    "SeqOs": {
      "column": "SEQ_DB",
      "type": "int",
      "size": 100,
      "description": "SEQ_DB da OS",
      "required": true
    }
  },
  "conditions": [
    "PROC_ST = 1"
  ],
  "updateIntegration": [
    "RO = 1",
    "PROC_DH = CURRENT_TIMESTAMP",
    "PROC_ST = 2",
    "PROC_DESC = 'OS Integrada com sucesso para o DMS'"
  ],
  "output": {
    "FILIAL": {
      "field": "Filial",
      "type": "sys",
      "returnColumns": [
        "SIGLA"
      ]
    },
    "LOCAL": {
      "field": "Local",
      "type": "sys",
      "returnColumns": [
        "SIGLA"
      ]
    },
    "ApontamentoEquipamento": {
      "table": "APONTAMENTO_EQUIPAMENTO",
      "type": "child",
      "description": "Vinculo com OS",
      "columns": {
        "CHASSI": {
          "field": "Chassi",
          "type": "varchar"
        },
        "HORIMETRO": {
          "field": "Horimetro",
          "type": "varchar"
        },
        "FOTO_CHASSI": {
          "field": "IdImagemChassi",
          "type": "image"
        },
        "FOTO_HORIMETRO": {
          "field": "IdImagemHorimetro",
          "type": "image"
        },
        "INI_DH": {
          "field": "DataLeitura",
          "type": "varchar"
        }
      }
    }
  }
}

Exemplo de configuração do getImage

{
  "alias": "ApontamentoEquipamento",
  "table": "APONTAMENTO_EQUIPAMENTO",
  "field": {
    "IdImagemChassi": {
      "column": "SEQ_DB",
      "type": "int",
      "size": 100,
      "description": "SEQ_DB da IMAGEM onde está a foto"
    },
    "IdImagemHorimetro": {
      "column": "SEQ_DB",
      "type": "int",
      "size": 100,
      "description": "SEQ_DB da IMAGEM onde está a foto"
    }
  },
  "output": {
    "FOTO_CHASSI": {
      "field": "ImagemChassi",
      "type": "getImage"
    },
    "FOTO_HORIMETRO": {
      "field": "ImagemHorimetro",
      "type": "getImage"
    }
  }
}

Notificações

A partir da versão x é possível criar notificações por usuário, será sempre mostra quando o usuário logar no sistema e depois pode ser vista clicando no menu de mensagem.

Como usar

Configuração

Verifica se existem as tabelas nfs_messages e nfs_usuario_messages, caso não execute:

CREATE TABLE `nfs_messages` (
  `INS_DH` timestamp NULL DEFAULT NULL,
  `DESCRIPTION` text NOT NULL,
  `SUBJECT` varchar(100) NOT NULL,
  `SEQ_DB` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `OPTIONS` text,
  `ICON` varchar(100) DEFAULT NULL,
  `LABEL_ICON` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`SEQ_DB`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

CREATE TABLE `nfs_usuario_messages` (
  `SEQ_DB` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `READED` tinyint(1) DEFAULT NULL,
  `MESSAGE_SEQ_DB` bigint(20) unsigned DEFAULT NULL,
  `USUARIO_SEQ_DB` bigint(20) unsigned DEFAULT NULL,
  `ENABLED` tinyint(1) NOT NULL DEFAULT '1',
  PRIMARY KEY (`SEQ_DB`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

Lembrando que um usuário pode ter várias mensagem.

Existindo essas tabelas, primeiro deve-se configurar a nfs_messages e depois nfs_usuario_messages

NFS_MESSAGES

  • SEQ_DB: Gerado automaticamente.
  • DESCRIPTION: Descrição da Mensagem.
  • SUBJECT: Assunto/título da mensagem.
  • OPTIONS: Um json com as configurações da notificação, no site code seven ou Metronic toastr é possível fazer um preview/demo das configurações abaixo:
{
  "positionClass": "toast-bottom-right",
  "progressBar": true,
  "timeOut": 0,
  "type": "warning",
  "extendedTimeOut": 0,
  "preventDuplicates": false
}

positionClass: Posição onde é exibida a noticação, as configurações podem ser as seguintes:

  • toast-bottom-right (Fundo direito)
  • toast-top-right (Topo direito)
  • toast-bottom-left (Fundo esquerdo)
  • toast-top-left (Topo esquerdo)
  • toast-top-full-width (Todo topo)
  • toast-bottom-full-width (Todo fundo)
  • toast-top-center (Topo centro)
  • toast-bottom-center (Fundo centro)

progressBar: Caso timeOut ou extendedTimeOut esteja configurados mostra uma barra de progresso até terminar a exibição da notificação. timeOut: Tempo em milissegundos para exibir a notificação, caso esteja 0 a notificação só será fechada após o clique. extendedTimeOut: Tempo em milissegundos que acontece depois da interação com a notificação.

type: Os tipos são:

  • success
  • info
  • warning
  • error

preventDuplicates: Evita duplicidade de notificações, deve-se tomar cuidado, pois, se ter notificações de dias difentes com mesmo tipo e descrição iguals, os mesmos não serão mostrados.

  • ICON: Ícone da notificação quando clica no menu para ser visualizar Metronic Icons
  • LABEL_ICON: Irá configurar a cor de fundo do ícone, são eles:
    • label-success
    • label-danger
    • label-info
    • label-warning

NFS_USUARIO _MESSAGES

  • SEQ_DB: Gerado automaticamente.
  • READED: Uma das mais importantes colunas, o seu valor pode ser 0, onde o usuário ainda não leu a mensagem e 1 quando o usuário já leu a mensagem, com isso a mesma não será exibida mais.
  • MESSAGE_SEQ _DB: Seq db da mensagem que deseja-se ser exibida para o usuário.
  • USUARIO_SEQ _DB: Seq db do usuário para quem será enviada a mensagem.
  • ENABLED: Se 1 ativo caso 0 desativo.

Exemplo de Uso

Para adicionar uma nova notificação é necessário primeiro inserir a mensagem na tabela nfs_messages:

INSERT INTO nfs_messages
(INS_DH, DESCRIPTION, SUBJECT, `OPTIONS`, ICON, LABEL_ICON)
VALUES('2018-03-22 14:02:39.000', '<h1>My Message</h1>', 'First message', '{
  "positionClass": "toast-bottom-right",
  "progressBar": true,
  "timeOut": 0,
  "type": "success",
"extendedTimeOut" : 0,
  "preventDuplicates": false
}', 'fa-plus', 'label-success');

E depois obter seu SEQ_DB e inserir na nfs_usuario_message ao SEQ_DB do usuário destino.

INSERT INTO nfs_homol_padrao_smartos.nfs_usuario_messages
(READED, MESSAGE_SEQ_DB, USUARIO_SEQ_DB, ENABLED)
VALUES(0, 1, 1, 1);

Assim quando o usuário entrar no sistema ela será exibida até que o Usuário leia.

Introdução

As notificações tem como função alertar os usuarios sobre eventos importantes no sistema, como:

  • Manutenção programada;
  • Lentidão no sistema;
  • Outros.

Configuração

Caso a base nfs_cloud não possua a tabela nfs_notification_banner, devemos cria-la como no SQL a seguir:

Aviso Criar a tabela na base nfs_cloud {.is-warning}

create table nfs_notification_banner(
  SEQ_DB INT primary key auto_increment,
  TYPES  ENUM ('info','warning','danger','success','top_info','top_warning','top_danger','top_success'),
  TITLE VARCHAR(100) not null,
  MESSAGE VARCHAR(1000) not null,
  INS_DH TIMESTAMP default CURRENT_TIMESTAMP,
  INI_DH TIMESTAMP  default NULL,
  FIM_DH TIMESTAMP  default NULL,
  ACTIVE tinyint default 1,
  DELETED tinyint default 0,
  HOST INT default NULL,
  MYSQL_SERVER_IP varchar(100) default NULL,
  BUSINESS_MODULES INT default NULL,
  MODULE INT default NULL,
  PRODUCT INT default NULL,
  PRIORITY ENUM ('1','2','3') default '3',
  `EMPRESA` int(11) NOT NULL DEFAULT '9999',
  `FILIAL` int(11) NOT NULL DEFAULT '9999',
  `LOCAL` int(11) NOT NULL DEFAULT '9999',
  constraint foreign key(host) references nfs_hosts(seq_db),
  constraint foreign key(business_modules) references nfs_business_modules(seq_db),
  constraint foreign key(module) references nfs_modules(seq_db),
  constraint foreign key(product) references nfs_products(seq_db))
CHARACTER SET utf8 COLLATE 'utf8_general_ci';

As notificações podem ser configuradas para serem exibidas como notificações no topo da pagina ou cards de notificação no corpo da pagina inicial, como na imagem abaixo:

An image

Configurações gerais

TITLE -> Texto que ficará em destaque;
MESSAGE -> Mensagem da notificação;
INS_DH -> Padrão data de inserção no banco;
INI_DH -> Data em que a notificação começa a ser exibida;
FIM_DH -> Data em que a notificação termina de ser exibida;
ACTIVE -> Padrão ativo(1);
DELETED -> Padrão não deletado(0);

Parametros que definem local de exibição

HOST;
MYSQL_SERVER_IP ;
BUSINESS_MODULES;
MODULE ;
PRODUCT;

Caso não seja definido, a notificação ira aparecer em todas as bases.

Prioridade

PRIORITY -> Pode ser definida entre:
- 3: Baixa prioridade;
- 2: Média prioridade;
- 1: Alta prioridade;
- Padrão: 3.
Quanto mais alta a prioridade, a notificação será posicionada em maior destaque.

Notificações topo

ATENÇÃO Essas notificações devem ser usadas apenas para mensagens de extrema importância, limitando-se a uma exibida por base {.is-danger}

Tipos(TYPES)

  • top_danger:
    • cor: vermelho
  • top_warning:
    • cor: amarelo
  • top_success:
    • cor: verde
  • top_info:
    • cor: azul

Exemplo

  INSERT INTO nfs_cloud.nfs_notification_banner
    (TYPES, TITLE, MESSAGE, INS_DH, INI_DH, FIM_DH, ACTIVE, DELETED, HOST, MYSQL_SERVER_IP, BUSINESS_MODULES, MODULE, PRODUCT, PRIORITY)
    VALUES('top_info', 'nfs-comercial01.simova.cloud', 'Homol', '2020-10-14 10:32:09', '2020-10-13 10:31:27', '2020-10-30 09:31:31', 1, 0, NULL, 'nfs-homol01.simova.cloud', NULL, NULL, NULL, '1');

Visualização

top_example_notification.png

Notificações corpo

Tipos(TYPES)

  • danger:
    • cor: vermelho
  • warning:
    • cor: amarelo
  • success:
    • cor: verde
  • info:
    • cor: azul

Exemplo

  INSERT INTO nfs_cloud.nfs_notification_banner
    (TYPES, TITLE, MESSAGE, INS_DH, INI_DH, FIM_DH, ACTIVE, DELETED, HOST, MYSQL_SERVER_IP, BUSINESS_MODULES, MODULE, PRODUCT, PRIORITY)
    VALUES('info', 'nfs-comercial01.simova.cloud', 'Homol', '2020-10-14 10:32:09', '2020-10-13 10:31:27', '2020-10-30 09:31:31', 1, 0, NULL, 'nfs-homol01.simova.cloud', NULL, NULL, NULL, '1');

Visualização

body_example_notification.png

Cores

Top:

top_colors.png

Corpo:

body_colors.png

Notificações Alerts

Como configurar

Primeira coisa é rodar o check DS para confirmar se não tem nenhum coluna do alerts a ser criada.

Criar um Grupo ou Tópico no Alerts

Antes de enviar de fato é necessário configurar um Grupo ou Tópico no Alerts para enviar mesmo que seja para um usuário, a ideia principal é utilizar essa estrutura de agrupamento.

Se o cliente usar o Alerts vai ter um menu exclusivo Simova Alerts selection_001.png

Criar Usuário

No NFS acesse Simova Alerts > Usuários SimovaAlerts

ao criar um usuário o Tipo Usuário deve ser NFS, se for Externo não vai funcionar, e depois selecionar o usuário.

Se o Grupo já existir é possível selecionar ele na Aba de Associações.

Criar Grupo

No NFS acesse Simova Alerts > Grupo de Usuários Simova Alerts

ao criar o grupo além da associações que tem um item obrigatório é necessário marcar a flag

Notificar Push ao Painel Web igual a verdadeiro.

Se o usuário já existir é possível adicionar na Aba de Associações.

Criar Tópico

No NFS acesse Simova Alerts > Tópicos de envio ao Simova Alerts

ao criar o grupo além da associações que tem um item obrigatório é necessário marcar a flag

Notificar Push ao Painel Web igual a verdadeiro.

Se o Grupo já existir é possível adicionar na Aba de Associações.

Como enviar

Para enviar pelo entry point pode ser enviado para um grupo ou tópico

Grupo

$result = Alerts::sendNotificationToGroup(
	'DESCRICAO_GRUPO',
  'TITULO', 
  'CORPO_HTML',
  $arrayOptions
 );
 
// Exemplo
$result = Alerts::sendNotificationToGroup(
  'TEST_GROUP',
  'Teste alerts '.date('d/m/Y H:i:s'),
  '<h2>My html body</h2>',
  ['TYPE_WEB_NOTIFY' => 2, 'NOTIFY_DISPLAY_TIME' => 10]
 );

Tópico

$result = Alerts::sendNotificationToTopic(
	'TEST_TOPIC',
  'TITULO', 
  'CORPO_HTML',
  $arrayOptions
 );
 
// Exemplo
Alerts::sendNotificationToTopic(
	'NOTIFICCO',
  'Teste alerts '.date('d/m/Y H:i:s'), 
  '<h2>My html body</h2>',
  ['TYPE_WEB_NOTIFY' => 2]
 );

Parâmetros

TYPE_WEB_NOTIFY : padrão = 0

É o tipo da notificação web que será exibida, pode ser os seguintes valores:

  • 0 : success, vai exibir uma mensagem de sucesso verde
  • 1 : warning, vai exibir uma mensagem de atenção laranja/amarelo
  • 2 : error, vai exibir uma mensagem de erro vermelha
  • 3 : info, vai exibir uma mensagem de informação

Se essa flag não for passada o padrão é success.

NOTIFY_DISPLAY_TIME : padrão = 3

É o tempo de exibição da notificação, pode acontece de o cliente mesmo ter o desejo de deixar notificações de erros sendo exibidas com maior tempo do que de sucesso para realmente chamar a atenção do usuário do painel.

Se esse parâmetro não for passado o valor padrão é 3.

WebPush

Notificações do navegador, podem ser recebidas fora da página do NFS.

Tabela nfs_web_push_notification precisa estar devidamente criada (check DS) para armazenar o endereço do navegador e realizar o envio da notificação.

Exemplo notificação windows 11 Alerta visual e sonoro notificacao_w11.png

Exemplo pós-alerta (sessão de notificações não lidas/visualizadas) notificacao_w11_lateral.png

A notificação web push enviada para o navegador poderá ser configurada NFS adicionando os seguintes parametros ao envio convencional do Alerts:

Grupo

Alerts::sendNotificationToGroup(
   'AL_TRANSPORTES_LTDA',
   'Título notificação '.date('d/m/Y H:i:s'),
   '<h2>Alerta push</h2>',
   ['TYPE_WEB_NOTIFY' => 2, 
   'NOTIFY_DISPLAY_TIME' => 5, 
   'PUSH' => true, /* opção que habilita o envio */
   'PUSH_TITLE' => 'Título', 
   'PUSH_BODY' => 'Corpo da notificação',
   'PUSH_URL' => 'https://www.google.com', 
   'PUSH_ICON' => './assets/img/webpush/captain_simova.png']
);

Tópico

Alerts::sendNotificationToTopic(
     'NOTIFICCO',
   'Teste alerts '.date('d/m/Y H:i:s'),
   '<h2>My html body</h2>',
   ['TYPE_WEB_NOTIFY' => 2,     
   'PUSH' => true, /* opção que habilita o envio */
   'PUSH_TITLE' => 'Título',
   'PUSH_BODY' => 'Corpo da notificação',
   'PUSH_URL' => 'https://www.google.com', 
   'PUSH_ICON' => './assets/img/webpush/captain_simova.png'] 
  );

  • PUSH: Opção que habilita o envio da notificação também para o navegador, o comportamento padrão é mantido e passa a enviar a notificação também para os navegadores.
  • PUSH_TITLE: Substitui o título padrão para um texto que pode ser personalizado.
  • PUSH_BODY: Substitui o corpo padrão para um texto que pode ser personalizado.
  • PUSH_URL: Quando omitida essa propriedade ao clicar na notificação será aberta a rota "/nfs../assets/images/notifications", pode ser configurado "/" para direcionar a Home, ou incluir uma url completa para que seja feito o redirecionamento pra ela.
  • PUSH_ICON: Permite incluir um icone a notificação, mantendo o padrão do exemplo pode-se usar uma das imagens disponibilizadas pelo core.

Icons (Web Push Notify):

Ícones disponíveis para notificação (pb = preto e branco), pode receber link externo, precisa ser 192x192px.

  • Simova: './assets/img/webpush/simova.png'
  • Capitão simova: './assets/img/webpush/captain_simova.png'
  • Alerts: './assets/img/webpush/alerts.png'
  • SmartOS: './assets/img/webpush/smartos.png' ou './assets/img/webpush/smartos_pb.png'
  • Bob Agro: './assets/img/webpush/bobagro.png' ou './assets/img/webpush/bobagro_pb.png'
  • Contru Mobil: './assets/img/webpush/construmobil.png' ou './assets/img/webpush/construmobil_pb.png'

Comportamento (Web Push Notify):

Notificações web são enviadas para um determinado dispositivo/browser, o envio é independente de usuário logado NFS.

O navegador do usuário precisa estar com as notificações habilitadas para o domínio para que os dados do endpoint do dispositivo seja salvo e vinculado ao usuário simova.

Com o navegador habilitado a exibir notificações de xxx.simova.cloud, quando o usuário fizer o login, os dados do depoente do dispositivo serão atribuídos ao usuário que fez login, e a partir daí qualquer notificação que esse usuário receber pelo NFS uma notificação push será enviada para esse dispositivo.

A notificação chega independente do usuário estar logado ou não no NFS, estar com o NFS aberto ou não no navegador, porém o browser precisa estar em execução para que a notificação chegue corretamente.

Compartilhamento de dispositivos (Web Push Notify):

Cada usuário NFS só terá um endpoint vinculado ao seu usuário.

O NFS nunca permitirá dois usuários vinculados ao mesmo endpoint (dispositivo), ou seja, se eu logar no google chrome em uma determinada máquina que está com as notificações habilitadas, o endpoint desse dispositivo ficará atrelado ao meu usuário NFS, fazendo com que qualquer notificação enviada para meu usuário NFS chegue nesse dispositivo que acabei de logar e para mais nenhum, se outro usuário logar nesse mesmo dispositivo, também no google chrome, o meu usuário que antes estava vinculado a essa máquina ficará sem nenhum dispositivo vinculado, e o dispositivo passa a estar vinculado ao usuario que fez o login depois.

O mesmo se aplica para browsers, se eu habilitei para receber notificações tanto no chrome quanto no Edge, minhas notificações chegarão no último navegador que eu loguei, abordagem necessária para que não fiquem chegando notificações duplicadas ou triplicadas no mesmo dispositivo.

Uma forma de reduzir problemas, é orientar usuários com relação a máquinas compartilhadas sugerindo que não seja habilitado as notificações web push em navegadores cujo a máquina é compartilhada, o interessante é usar o recurso em dispositivos individuais.

Acesso a Noticações

Vai aparecer um ícone de um sino ao lado do logo da Empresa, onde vai mostrar a quantidade total de mensagens não lidas.

A mensagem ao ser enviada só mostrar uma vez e depois fica no contador até o usuário ler nesse popup ou acessar a tela de Notificações. {.is-warning}

É possível pesquisar pelo título e/ou corpo da mensagem, então se você colocar o horário 18:01:16 vai filtrar por todas as mensagens desse horário ou pelo corpo da mensagem ERROuuu segue a mesma ideia.

screenshot_from_2021-08-16_09-50-50.png

Para acessar a Tela de Notificações é possíl ir em ver todas no popup que é exibido ao clicar no sino ou clicar no nome do usuário e ir em Notificações.

screenshot_from_2021-08-16_09-51-27.png

Acreditamos que seja bom ter um local onde possa ser vista todas as mensagem enviadas, é sempre mostrado primeiro as Não lida e ao clicar é exibido seu conteúdo com outras informações como o horário de Envio e Leitura.

screenshot_from_2021-08-16_09-51-53.png

Painel

Sobre

O componente Árvore de Decisões permite aos usuários configurar e interagir com menus de contexto personalizados, atribuindo ações a serem realizadas no painel de acordo com o status de um card. demo

Índice

1. Preparando o Ambiente:

1.1 Habilitando e Criando as Tabelas Decision

Para que a funcionalidade da arvore de decisões funcione corretamente, precisamos habilitar alguns parametros na tabela nfs_core_par_parametros, sendo estes:

menuJson.png

INSERT INTO nfs_core_par_parametros(EMPRESA, FILIAL, `LOCAL`, NOME, CONTEUDO, TIPO)
VALUES(9999, 9999, 9999, 'DECISION_TREE_ENABLED', '1', 1);

Este parâmetro é responsável por criar algumas tabelas no sistema, sendo estas:

  • app_decision_tree_json: Tabela que contem os JSONs abertos cadastrados no escalation(preenchida via agendamento\scheduller);
  • nfs_decision_tree_config: Tabela de configurações da arvore de decisões(menus, botão do topo, etc);
  • nfs_decision_tree_n_panel: Tabela que faz o vinculo da arvore de decisões ao painel.
  • nfs_decision_tree_api_log: Log de requisições feitas do Escalation para o NFS.

Após a inserção do parâmetro será necessário executar o create DS/DDL

1.2 Permissões de Acesso

Para que o icone fique disponivel para um usuario ou grupo de usuario, será necessario adicionar permissão na tabela DECISION_TREEpara os mesmos:

INSERT INTO nfs_acl_grupo_permissao(EMPRESA, FILIAL, `LOCAL`, MENU_SEQ_DB, TABELA_NOME, SIUDT, GRUPO_SEQ_DB, CAMPOS_PERMITIDOS, INS_DH, ATIVO, DELETED, SIUD)
VALUES(1, 9999, 9999, NULL, 'DECISION_TREE', '11110', 1, '', '2020-11-19 16:16:53', 1, 0, NULL);

Lembre-se de alterar o seqDb do grupo(ou usuario caso seja um insert na tabela nfs_acl_usuario_permissao) conforme a sua necessidade

1.3 Token de Autenticação

Para que as ações criadas no menu de contexto possam se comunicar com o serviço externo, será necessario gerar um token unico para cada local que vai utilizar a arvore de decisão

Para gerar o token, será necessario solicitar ao responsavel pelo serviço externo(Escaltion) uma chave unica para a FILIAL do ambiente (esta chave deverá ser registrada no campo TREE_KEY na tabela nfs_org_filial).

Pode ser necessario executar o Create DS do core para adicionar o novos campos referente ao token nas tabelas ORG

Após cadastrar essa chave, ao acessar o ADMIN CONSOLE, um usuario de nivel ADMINISTRADOR ou SUPER ADMINISTRADOR terá acesso a um novo botão no card NFS ENVIRONMENT que será responsavel pela geração do token no ambiente:

tokenBtn

generated

Este token ficará salvo na tabela nfs_org_local e expira após 12 horas

1.4 Configurações Escalation/API:

Essa configurações permitem alterações nas tratativas do sistema para que o ambiente fique compativel e consiga se comunicar com a API externa (atualmete Escalation).

Essas configurações podem ser definidas atavés dos parametros TICKET_API_PARAMS:

1.4.1 Nome da Arvore de Decisões:

O nome da arvore de decisões será utilizado para enviar ou receber informações da API de tickets(Escalation):

{
  "typeName":"arvoreDecisao"
}

arvoreDecisao será utilizado por padrão caso esse atributo não tenha sido definido na configuração

1.4.2 Definindo "apelidos" para Empresa, Filial e Local

Caso seja necessario utilizar uma sigla para EFL diferente da padrão ja definida no sistema, podemos usar o parametro "efl" para criar um alias para um local Especifico.

{
   "efl":{
      "3":{
         "LOCAL":"MA",
         "FILIAL":"LOGISTICA",
         "EMPRESA":"SAL"
      }
   }
}

Note que a estrutura é definida pelo parametro "efl", contendo o seqDb do local como chave e qual o apelido será usado para EFL daquele local

1.4.3 Campo Principal na API

Essa configuração, define qual o campo retornado no JSON da API será utilizado para verificar se o QII possui arvore de decisão aberta.

{
  "mainField": "EQUIPAMENTO"
}

o campo EQUIPAMENTO será utilizado por padrão caso esse atributo não tenha sido definido na configuração

2. Configurações do Arvore de Decisões:

tabela nfs_decision_tree_config

A configuração do menu, permite definir quais os items vão aparecer no menu de contexto e quais as condições necessárias para que isso aconteça.

2.1 Estrutura:

Campo CONFIG

{
  "label":"",
  "menu":[],
  "cardIcon":{
    "styles":[]
  },
  "topButton":{}
}

2.1.1 label

Apelido dado a arvore de decisão - *utilizar camelCase

Ao utilizar com escalation o apelido devera ser o mesmo ao nome definido para a arvore no próprio escalation.

2.1.2 menu

Itens que vão aparecer no menu de contexto:

Este parâmetro tem como valor um array de objetos, sendo cada objeto um item do menu:

{
  "menu": [
    {
      "condition": "",
      "entryPoint": "",
      "icon": "",
      "text": "",
      "url": ""
    },
    {
      "condition": "",
      "entryPoint": "",
      "icon": "",
      "text": "",
      "url": "",
      "type":""
    }
  ]
}

Legenda:

PropriedadeDescriçãoExemplo
conditionCampo do QII necessário para exibição do itemF7,F12,DECISION_TREE_OPEN,outros...
entryPointEntryPoint que sera usado para tratar os dados antes de enviar para o serviço que trata da arvore de decisõesnewTicket,listTickets,newAlertTicket
iconIcone que fica antes do texto do item no menufa fa-plus-circle, fa fa-list
textTexto do item no menulistar tickets, abrir novo ticket, abrir novo ticket de alerta
urlLocal para qual os dados e o usuário será redirecionado após a execução do entryPointhttps://foobar/list, https://foobar/newTicket
*type - (não obrigatório)Tipo de requisição, atualmente é usado para diferir uma requisição de listagem de uma de edição ou criaçãolist (exatamente assim)

2.1.3 cardIcon

Estilos custom para o ícone de arvore de decisão:

{
  "cardIcon": {
    "styles": [
      {
        "condition": "DECISION_TREE_OPEN",
        "style": "color:green;"
      },
      {
        "condition": "F12",
        "style": "color:red;"
      }
    ]
  }
}

Legenda:

PropriedadeDescriçãoExemplo
conditionCondição necessária para aplicação do estiloF12, DECISION_TREE_OPEN
styleEstilo a ser aplicado caso a condição seja verdadeiracolor:red;border:1px solid #00FF00; font-size: 30px;

2.1.4 topButton

Configuração do botão de lista do topo, esse botão por hora só exibe todos as arvores abertas: topButton.png

{
  "topButton": {
    "title": "Mostrar todos os tickets",
    "url": "https://escalation-qa.simova.cloud/ext/tickets"
  }
}
  • title: titulo exibido ao fazer hover no botão
  • url: pagina que contem todos os tickets

2.1.5 Exemplo configuração completa

{
    "label": "arvoreDecisao",
  "cardIcon": {
    "styles": [
      {
        "condition": "DECISION_TREE_OPEN",
        "style": "color:green;"
      },
      {
        "condition": "F12",
        "style": "color:red;"
      }
    ]
  },
  "menu": [
    {
      "condition": "",
      "entryPoint": "openTicket",
      "icon": "fa fa-plus-circle",
      "text": "Abrir Chamado",
      "url": "https://escalation-qa.simova.cloud/ext/tickets/edit"
    },
    {
      "condition": "F12",
      "entryPoint": "openTicket",
      "icon": "fa fa-plus-circle",
      "text": "Abrir Chamado Tempo Excedido",
      "url": "https://escalation-qa.simova.cloud/ext/tickets/edit"
    },
    {
      "condition": "DECISION_TREE_OPEN",
      "entryPoint": "listTicket",
      "icon": "fa fa-list",
      "text": "Listar Chamados",
      "type": "list",
      "url": "https://escalation-qa.simova.cloud/ext/tickets"
    }
  ],
  "topButton": {
    "url": "https://escalation-qa.simova.cloud/ext/tickets",
    "title": "Mostrar todos os tickets"
  }
}

contextMenu.png

3. Entry Point:

exemplo de entry point para editar inserir um novo ticket:

$this->outputValues = array(
  "typeName" => "arvoreDecisao",
  "insDh" => "2024-07-08 08:42:44",
  "statusCode" => "1",
  "values" => array(
    array(
      fieldName => "idApt",
      value => mt_rand(0, 999999)
    ),
    array(
      fieldName => "eqp",
      value => $this->inputValues['qii']['QII_SEQ_DB']
    )
  )
);

Os campos devem ser enviados conforme os nome e estrutura definida no Escalation

exemplo de entry point de listagem:

$this->outputValues = array(
  "status" => "1" 
);
VariavelDescrição
$this->inputValuesValores do QII que trigou a ação do EntryPoint
$this->outputValuesArray contendo os campos que vão ser enviados para preenchimentos dos campos do ticket

4. Árvores Abertas

Para manter o registro das árvores que estão abertas no escalation, podemos configurar um agendamento que vai busca-las e as armazenar na tabela app_decision_tree_json.

Para isso basta criar o agendamento(nfs_core_scheduller) com o codigo como no exemplo abaixo:

$treeService = new DecisionTreeService();
$treeService->updateOpenTrees();

Feito isso, podemos verificar se um QII possui árvore aberta através do field DECISION_TREE_OPEN:

{
 "cardIcon": {
    "styles": [
      {
        "condition": "DECISION_TREE_OPEN",
        "style": "color:green;"
      }
    ]
  },
}

Exemplo onde alteramos a cor do icone da arvore de decisão no painel caso o equipamento possua ticket aberto.

5. Sincronismo com o Escalation

Para enviar os dados e torna-los disponiveis no Escalation, devemos agendar um sincronismo no NFS, isso pode ser feito utilizando o serviço EscalationApiService do NFSCore como no exemplo abaixo:

/** Sincronismo de Frentes */
/**
 * Busca os registros da tabela APP_FRENTE
 */
$allFrentes = Dao::table('FRENTE')->select('SEQ_DB','CODIGO','DESCRICAO','ATIVO')->all()->get();
$listaAllFrentes = array_column($allFrentes, null, 'SEQ_DB');
$listaFinal = [
	'tableName' => "silv_modulo",
	'fkDisplayColumnName' => "description",
	'fkColumnName' => "sync_id",
	'companyCode' => "SILV",
	'branchCode' => "SILVICULTURA",
	'siteCode' => "DEV"
];
$listaDados = [];
foreach($listaAllFrentes as $frente){
	$arrayInput = [
	  'fkColumn' => $frente['SEQ_DB'],
	  'fkDisplayField' => $frente['CODIGO'].' - '.$frente['DESCRICAO'],
	  'active' => $frente['ATIVO']
  ];

	array_push($listaDados,$arrayInput);
}
$listaFinal['values'] = $listaDados;

/**
 * Cria o serviço de comunicação com o Escalation
 * e efetua o sincronismo
 */
$escalationApiService = new EscalationApiService();
$escalationApiService->sync($listaFinal);

6. Envio dos tickets do Escalation para o NFS:

Caso seja necessario salvar os dados dos tickets ao efetuar alguma modificação no Escalation, um usuario de API deverá ser criado no ambiente alvo com acesso aos locais que possuem a arvore de decisão.

Feito isso sera possivel obter o token e efetuar a integração dos dados do escalation com o NFS.

6.1 Autenticação

/* Dados do usuario cadastrado no ambiente e local */
{
  "user": "escalation-api-user",
  "pass": "senha123",
  "branchCode": "localA"
}
Rota/nfs/api/escalation/auth
TipoPOST

6.2 Integração

/**
* Dados do ticket
* Usar dados reais, exemplo meramente ilustrativo.
*/
{
  "id": 10036,
  "seq": 8,
  "type":{...},
  "values":{...}
}
Rota/nfs/api/escalation/integration
TipoPOST
HeaderAuthorization: Bearer tokenGeradoNoAuthAqui

Checklist

O checklist depende da configuração de 3 entrypoints cujo suas configurações e características estão sendo detalhados abaixo.

Entry point: Tela Lista de Checklists

Filtro e Painel

Filtros e exibição da lista de checklists.

Rota: /checklist
Ex: https://smartos-interno.h.simova.cloud/checklist

Valores de entrada

$this->inputValues['data']; /* Data atual padrão, ou, data filtrada */
$this->inputValues['equipamento']; /* id do Equipamento filtrado */
$this->inputValues['tecnico']; /* ARRAY com o id de Técnicos filtrados */
$this->inputValues['os']; /* id da Os filtrada */

Saida::Array

A saída é composta por dois itens, o DATA e o FILTER_DATA, que irão retornar as informações para exibir a listagem de checklists, e os dados para as opções dos filtros. Para o FILTER_DATA serão enviados os arrays compostos sempre pelo ID e a DESCRICAO, atualmente podem ser configurados os filtros para TECNICO, OS e EQP.

[
	"DATA" => [ /** Array com os dados que serão exibidos na listagem */
		[
			"SEQ_CHECKLIST" => 10, /* seq checklist */
			"NOME_TEC" => "Branco", /* Nome tecnico */
			"DH" => "2023-07-12 12:22:09" /* DH */
		],
		[
			"SEQ_CHECKLIST" => 10, /* seq checklist */
			"NOME_TEC" => "Branco", /* Nome tecnico */
			"DH" => "2023-07-12 12:22:09" /* DH */
		]
	],
	"FILTER_DATA" => [/** Array com os dados que serão usados nas options dos filtros */
		"TECNICO" => [
			[
				"ID" => 12,
				"DESCRICAO" => "Jorge Wagner"
			],
			[
				"ID" => 22,
				"DESCRICAO" => "Fábio Simplicio Silva"
			],

		],
		"OS" => [
			[
				"ID" => 224,
				"DESCRICAO" => "OS224"
			],
			[
				"ID" => 785,
				"DESCRICAO" => "OS785"
			]
		],
		"EQP" => [
			[
				"ID" => 1,
				"DESCRICAO" => "Caminhão"
			],
			[
				"ID" => 2,
				"DESCRICAO" => "Trator"
			]
		]
	]
]

Entrypoint (Exemplo)

FILE_OR_DOMAIN: CHECKLIST
ACTION: checklistGrid

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

//Filtros
$data_filtro = $this->inputValues['data'] ?? null;
$data_atual = date("Y-m-d H:i:s");
// $equipamento_filtro = $this->inputValues['equipamento'] ?? null;
// $tecnico_filtro = $this->inputValues['tecnico'] ?? null;
// $os_filtro = $this->inputValues['os'] ?? null;
$checklist_filtro = $this->inputValues['checklist'] ?? null;

$checklists_prospeccao = Dao::Table('apontamento_resposta_prospeccao')
	->select(['SEQ_DB',
			  'FUNCIONARIO_SEQ_DB',
			  'INI_DH',
              'SEQ_DB_DEVICE_CHECKLIST'
	])
	->whereBetween('INI_DH',$data_filtro,$data_atual)
	->whereIn('SEQ_DB_DEVICE_CHECKLIST',$checklist_filtro)
    ->groupBy('SEQ_DB_DEVICE_CHECKLIST')
    ->orderBy('INI_DH','DESC')
	->get();
$funcionario_fk = array_column($checklists_prospeccao,'FUNCIONARIO_SEQ_DB','FUNCIONARIO_SEQ_DB');

$funcionario_array = Dao::Table('FUNCIONARIO')->select(['SEQ_DB','CRACHA','NOME'])->whereIn('SEQ_DB',$funcionario_fk)->get();
$funcionario_list = array_column($funcionario_array,null,'SEQ_DB');

$dadosGridChecklist = [];
$funcionario = null;
$data = null;
$i=0;
foreach($checklists_prospeccao as $cp){
	$funcionario_fk = $funcionario_list[$cp['FUNCIONARIO_SEQ_DB']] ?? null;
	if(!empty($funcionario_fk)){
		$funcionario = $funcionario_fk['CRACHA']." :: ".$funcionario_fk['NOME'];
		$data = $cp['INI_DH'];
		
		$dadosGridChecklist[$i]['SEQ_CHECKLIST'] = $cp['SEQ_DB_DEVICE_CHECKLIST'];
		$dadosGridChecklist[$i]['NOME_TEC'] = $funcionario;
		$dadosGridChecklist[$i]['DH'] = $data;
		$i++;
	}
}
$this->queryData['data']['DATA'] = $dadosGridChecklist;

Entry point: Relatório Checklist

  • Topo Topo do relatório

  • Conteúdo Conteúdo do relatório

  • Rodapé Rodapé

Exibição do relatório de checklist.

Rota: /checklist/<SEQ_CHECKLIST>
Ex: https://smartos-interno.h.simova.cloud/checklist/13

Entrada

$this->inputValues['checklist']; /* seq chamado na url */

Saida::Array

[
	"SEQ_CHECKLIST" => 10, /* seq checklist */
	"ORDEM_REPARACAO" => "OS2154ww1237",
	"DH" => "2023-07-12 10:55:29",
	"NOME_TEC" => "Chucky Norris",
	"COTACAO" => "0,00",
	"NOTE_1" => "3 desplazamiento",
	"NOTE_2" => "Andrés Camacho",
	"NOTE_3" => "8h labor",
	"NOME" => "Ingenio La Cabaña",
	"VEICULO" => "John Deere 6110J 2015",
	"PLACA" => "3182903",
	"VIN" => "1BM6110JTHA009297",
	"SERVICO_URGENTE" => [ /* array com os itens urgentes*/
		[
    		"SEQ_CHECKLIST_ITEM" => 1, /* seq do detalhe */
        	"TITULO_ITEM" => "Título do item 1", /* titulo do detalhe */
            "COMMENTS" => [ /* array dos comentarios, separado por virgula se mais de 1*/
            	"Aqui exibe o comentário 1"
            ],
            "QTD_FOTOS" => 2 /* quantidade de fotos que o detalhe possui*/
        ]
    ],
	"SERVICO_PRONTO" => [ /* array com os itens prontos*/
    	[
        	"SEQ_CHECKLIST_ITEM" => 5,
         	"TITULO_ITEM" => "Título do item",
         	"COMMENTS" => [
         	"Tem que ver isso aí", "Comentário 2"
                    ],
        	"QTD_FOTOS" => 2
      	]
  	],
  	"SERVICO_OK" => [ /* array com os itens ok*/
      	[
         	"SEQ_CHECKLIST_ITEM" => 8,
          	"TITULO_ITEM" => "Título do item",
           	"COMMENTS" => [
              	"Tem que ver isso aí", "Comentário 2"
           	],
           	"QTD_FOTOS" => 2
      	]
    ],
    "PHOTOS" => [ /* array com todas as fotos do checklist ok*/
      	[
           	"SEQ_CHECKLIST_ITEM" => 5, /* checklist do detalhe*/
           	"TYPE" => 'urgent', /* tipo do detalhe */
           	"TITLE" => "Títulos aqui", /* titulo do detalhe */
          	"URL" => "assets/img/checklist/teste_img.jpg" /*url da imagem*/
      	]
   	]

]

Entrypoint (Exemplo objeto)

FILE_OR_DOMAIN: CHECKLIST
ACTION: checklist

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

//Filtros
$checklist_filtro = $this->inputValues['checklist'] ?? null;

//Consulta Principal
$checklists_prospeccao = Dao::Table('apontamento_resposta_prospeccao')
	->select(['SEQ_DB',
			  'FUNCIONARIO_SEQ_DB',
			  'INI_DH',
			  'TIPO_EQUIPAMENTO_SEQ_DB',
			  'GRUPO_QUESTAO_SEQ_DB',
			  'QUESTAO_SEQ_DB',
			  'TIPO_RESPOSTA_SEQ_DB',
			  'OS_SEQ_DB',
			  'CLIENTE_SEQ_DB',
			  'CHASSI',
			  'SEQ_DB_DEVICE',
              'SEQ_DB_DEVICE_CHECKLIST',
			  'SEQ_DB_DEVICE_MASTER_SEQ_DB',
			  'OBSERVACAO'
	])
	->whereIn('SEQ_DB_DEVICE_CHECKLIST',$checklist_filtro)
    ->orderBy('INI_DH','DESC')
	->get();
//FKs resultantes da consulta principal
$funcionario_fk = array_column($checklists_prospeccao,'FUNCIONARIO_SEQ_DB','FUNCIONARIO_SEQ_DB');
$tipo_equipamento_fk = array_column($checklists_prospeccao,'TIPO_EQUIPAMENTO_SEQ_DB','TIPO_EQUIPAMENTO_SEQ_DB');
$grupo_questao_fk = array_column($checklists_prospeccao,'GRUPO_QUESTAO_SEQ_DB','GRUPO_QUESTAO_SEQ_DB');
$questao_fk = array_column($checklists_prospeccao,'QUESTAO_SEQ_DB','QUESTAO_SEQ_DB');
$tipo_resposta_fk = array_column($checklists_prospeccao,'TIPO_RESPOSTA_SEQ_DB','TIPO_RESPOSTA_SEQ_DB');
$os_fk = array_column($checklists_prospeccao,'OS_SEQ_DB','OS_SEQ_DB');
$cliente_fk = array_column($checklists_prospeccao,'CLIENTE_SEQ_DB','CLIENTE_SEQ_DB');
$boletim_fk = array_column($checklists_prospeccao,'SEQ_DB_DEVICE_MASTER_SEQ_DB','SEQ_DB_DEVICE_MASTER_SEQ_DB');
$prospeccao_fk = array_column($checklists_prospeccao,'SEQ_DB_DEVICE','SEQ_DB_DEVICE');
$checklist_fk = array_column($checklists_prospeccao,'SEQ_DB_DEVICE_CHECKLIST','SEQ_DB_DEVICE_CHECKLIST');

//Consultas auxiliares
$apontamento_deslocamento_array = Dao::Table('APONTAMENTO_DESLOCAMENTO')->select(['SEQ_DB','OS_SEQ_DB','INI_DH','TIPO_DESLOCAMENTO','VEICULO_SEQ_DB'])->whereIsNotNull('OS_SEQ_DB')->whereRaw('TIPO_DESLOCAMENTO = 2')->whereIn('OS_SEQ_DB',$os_fk)->whereIn('SEQ_DB_DEVICE_MASTER_SEQ_DB',$boletim_fk)->get();
$veiculo_fk = array_column($apontamento_deslocamento_array,'VEICULO_SEQ_DB','VEICULO_SEQ_DB');
$apontamento_deslocamento_list = [];
foreach($apontamento_deslocamento_array as $apt){
	if(!empty($apontamento_deslocamento_list[$apt['OS_SEQ_DB']]['FIM_DESLOCAMENTO'])){
		$apontamento_deslocamento_list[$apt['OS_SEQ_DB']]['FIM_DESLOCAMENTO'] = $apontamento_deslocamento_list[$apt['OS_SEQ_DB']]['FIM_DESLOCAMENTO'] + 1;	
	}else{
		$apontamento_deslocamento_list[$apt['OS_SEQ_DB']]['FIM_DESLOCAMENTO'] = 1;
	}
	$apontamento_deslocamento_list[$apt['OS_SEQ_DB']]['VEICULO_SEQ_DB'] = $apt['VEICULO_SEQ_DB'];
}

$apontamento_array = Dao::Table('APONTAMENTO')->select(['SEQ_DB_DEVICE_MASTER_SEQ_DB','INI_FIM_DIFF_SEC'])->whereIn('SEQ_DB_DEVICE_MASTER_SEQ_DB',$boletim_fk)->get();
$apontamento_list = [];
foreach($apontamento_array as $apt){
	if(!empty($apontamento_list[$apt['SEQ_DB_DEVICE_MASTER_SEQ_DB']]['TEMPO_TRABALHADO'])){
		$apontamento_list[$apt['SEQ_DB_DEVICE_MASTER_SEQ_DB']]['TEMPO_TRABALHADO'] = $apontamento_list[$apt['SEQ_DB_DEVICE_MASTER_SEQ_DB']]['TEMPO_TRABALHADO'] + $apt['INI_FIM_DIFF_SEC'];	
	}else{
		$apontamento_list[$apt['SEQ_DB_DEVICE_MASTER_SEQ_DB']]['TEMPO_TRABALHADO'] = $apt['INI_FIM_DIFF_SEC'];	
	}
}

$apontamento_fotos_array = Dao::Table('APONTAMENTO_FOTO_RESPOSTA_PROSPECCAO')->select(['SEQ_DB_DEVICE_RESPOSTA_CHECKLIST','FOTO_THUMBNAIL'])->whereIn('SEQ_DB_DEVICE_RESPOSTA_CHECKLIST',$prospeccao_fk)->whereIn('SEQ_DB_DEVICE_CHECKLIST',$checklist_fk)->get();
$apontamento_fotos_list = [];
foreach($apontamento_fotos_array as $apt){
	if(!empty($apontamento_fotos_list[$apt['SEQ_DB_DEVICE_RESPOSTA_CHECKLIST']]['QTD_FOTOS'])){
		$apontamento_fotos_list[$apt['SEQ_DB_DEVICE_RESPOSTA_CHECKLIST']]['QTD_FOTOS'] = $apontamento_fotos_list[$apt['SEQ_DB_DEVICE_RESPOSTA_CHECKLIST']]['QTD_FOTOS'] + 1;
	}else{
		$apontamento_fotos_list[$apt['SEQ_DB_DEVICE_RESPOSTA_CHECKLIST']]['QTD_FOTOS'] = 1;
	}
	$apontamento_fotos_list[$apt['SEQ_DB_DEVICE_RESPOSTA_CHECKLIST']]['FOTOS'][] = $apt['FOTO_THUMBNAIL'];
}

$funcionario_array = Dao::Table('FUNCIONARIO')->select(['SEQ_DB','CRACHA','NOME'])->whereIn('SEQ_DB',$funcionario_fk)->get();
$funcionario_list = array_column($funcionario_array,null,'SEQ_DB');

$tipo_equipamento_array = Dao::Table('TIPO_EQUIPAMENTO')->select(['SEQ_DB','CODIGO','DESCRICAO'])->whereIn('SEQ_DB',$tipo_equipamento_fk)->get();
$tipo_equipamento_list = array_column($tipo_equipamento_array,null,'SEQ_DB');

$grupo_questao_array = Dao::Table('GRUPO_QUESTAO')->select(['SEQ_DB','DESCRICAO'])->whereIn('SEQ_DB',$grupo_questao_fk)->get();
$grupo_questao_list = array_column($grupo_questao_array,'DESCRICAO','SEQ_DB');

$questao_array = Dao::Table('QUESTAO')->select(['SEQ_DB','DESCRICAO'])->whereIn('SEQ_DB',$questao_fk)->get();
$questao_list = array_column($questao_array,'DESCRICAO','SEQ_DB');

$tipo_resposta_array = Dao::Table('TIPO_RESPOSTA')->select(['SEQ_DB','CLASSIFICACAO_RESPOSTA','DESCRICAO'])->whereIn('SEQ_DB',$tipo_resposta_fk)->get();
$tipo_resposta_list = array_column($tipo_resposta_array,null,'SEQ_DB');

$os_array = Dao::Table('OS')->select(['SEQ_DB','CODIGO'])->whereIn('SEQ_DB',$os_fk)->get();
$os_list = array_column($os_array,'CODIGO','SEQ_DB');

$veiculo_array = Dao::Table('VEICULO')->select(['SEQ_DB','PLACA','DESCRICAO'])->whereIn('SEQ_DB',$veiculo_fk)->get();
$veiculo_list = array_column($veiculo_array,null,'SEQ_DB');

$cliente_array = Dao::Table('CLIENTE')->select(['SEQ_DB','CODIGO','DESCRICAO'])->whereIn('SEQ_DB',$cliente_fk)->get();
$cliente_list = array_column($cliente_array,null,'SEQ_DB');

//Variáveis para montagem dos dados
$dados = [];
$data_hora = null;
$deslocamento = null;
$veiculo_descricao = null;
$veiculo_placa = null;
$os = null;
$cliente = null;
$grupo_questao = null;
$questao = null;
$tipo_resposta = null;
$observacao = null;
$funcionario = null;
$chassi = null;
$horas_trabalhadas = null;
$qtd_deslocamento = null;
$horas_trabalhadas = null;
$qtd_fotos = null;
$fotos = null;
$a=0;
$b=0;
$c=0;
$i=0;
$f=0;
//Montagem do array de dados
foreach($checklists_prospeccao as $check){
	//Preenchimento das variáveis
	$os = $os_list[$check['OS_SEQ_DB']] ?? null;
	$cliente = $cliente_list[$check['CLIENTE_SEQ_DB']]['CODIGO']."::".$cliente_list[$check['CLIENTE_SEQ_DB']]['DESCRICAO'] ?? null;
	$data_hora = date("d/m/Y H:i:s", strtotime($check['INI_DH']));
	$funcionario = $funcionario_list[$check['FUNCIONARIO_SEQ_DB']]['CRACHA']."::".$funcionario_list[$check['FUNCIONARIO_SEQ_DB']]['NOME'] ?? null;
	$deslocamento = $apontamento_deslocamento_list[$check['OS_SEQ_DB']] ?? null;
	if(!empty($deslocamento)){
		$qtd_deslocamento = $deslocamento['FIM_DESLOCAMENTO'];
		$veiculo_descricao = $veiculo_list[$deslocamento['VEICULO_SEQ_DB']]['DESCRICAO'] ?? null;
		$veiculo_placa = $veiculo_list[$deslocamento['VEICULO_SEQ_DB']]['PLACA'] ?? null;
	}
	$horas_trabalhadas_fk = $apontamento_list[$check['SEQ_DB_DEVICE_MASTER_SEQ_DB']]['TEMPO_TRABALHADO'] ?? null;
	if(!empty($horas_trabalhadas_fk)){
		$horas_trabalhadas = gmdate('H:i:s',$horas_trabalhadas_fk);
	}
	$chassi = $check['CHASSI'] ?? null;
	$grupo_questao = $grupo_questao_list[$check['GRUPO_QUESTAO_SEQ_DB']] ?? null;
	$questao = $questao_list[$check['QUESTAO_SEQ_DB']] ?? null;
	$tipo_resposta = $tipo_resposta_list[$check['TIPO_RESPOSTA_SEQ_DB']] ?? null;
	$observacao = $check['OBSERVACAO'] ?? null;
	$fotos = $apontamento_fotos_list[$check['SEQ_DB_DEVICE']] ?? null;
	$qtd_fotos = $fotos['QTD_FOTOS'] ?? 0;
	
	$dados['SEQ_CHECKLIST'] = $check['SEQ_DB_DEVICE_CHECKLIST'];
	$dados['ORDEM_REPARACAO'] = $os;
	$dados['DH'] = $data_hora;
	$dados['NOME_TEC'] = $funcionario;
	//	$dados['COTACAO'] = "0,00";
	$dados['NOTE_1'] = $qtd_deslocamento;
	$dados['NOTE_2'] = $cliente;
	$dados['NOTE_3'] = $horas_trabalhadas;
	$dados['NOME'] = $funcionario;
	$dados['VEICULO'] = $veiculo_descricao;
	$dados['PLACA'] = $veiculo_placa;
	$dados['VIN'] = $chassi;
	
	if($tipo_resposta['CLASSIFICACAO_RESPOSTA'] == 2){
		$dados['SERVICO_URGENTE'][$a]['SEQ_CHECKLIST_ITEM'] = $check['SEQ_DB'];
		$dados['SERVICO_URGENTE'][$a]['TITULO_ITEM'] = $questao;
		$dados['SERVICO_URGENTE'][$a]['COMMENTS'][0] = $observacao;
		$dados['SERVICO_URGENTE'][$a]['QTD_FOTOS'] = $qtd_fotos;
		
		if(!empty($fotos)){
			foreach($fotos['FOTOS'] as $foto){
				$dados['PHOTOS'][$f]['SEQ_CHECKLIST_ITEM'] = $check['SEQ_DB'];
				$dados['PHOTOS'][$f]['TYPE'] = 'urgent';
				$dados['PHOTOS'][$f]['TITLE'] = $questao;
				$dados['PHOTOS'][$f]['URL'] = $foto;	
				$f++;
			}
		}

		$a++;
	}else if($tipo_resposta['CLASSIFICACAO_RESPOSTA'] == 1){
		$dados['SERVICO_PRONTO'][$b]['SEQ_CHECKLIST_ITEM'] = $check['SEQ_DB'];
		$dados['SERVICO_PRONTO'][$b]['TITULO_ITEM'] = $questao;
		$dados['SERVICO_PRONTO'][$b]['COMMENTS'][0] = $observacao;
		$dados['SERVICO_PRONTO'][$b]['QTD_FOTOS'] = $qtd_fotos;
		
		if(!empty($fotos)){
			foreach($fotos['FOTOS'] as $foto){
				$dados['PHOTOS'][$f]['SEQ_CHECKLIST_ITEM'] = $check['SEQ_DB'];
				$dados['PHOTOS'][$f]['TYPE'] = 'ready';
				$dados['PHOTOS'][$f]['TITLE'] = $questao;
				$dados['PHOTOS'][$f]['URL'] = $foto;	
				$f++;
			}
		}

		$b++;
	}else if($tipo_resposta['CLASSIFICACAO_RESPOSTA'] == 0){
		$dados['SERVICO_OK'][$c]['SEQ_CHECKLIST_ITEM'] = $check['SEQ_DB'];
		$dados['SERVICO_OK'][$c]['TITULO_ITEM'] = $questao;
		$dados['SERVICO_OK'][$c]['COMMENTS'][0] = $observacao;
		$dados['SERVICO_OK'][$c]['QTD_FOTOS'] = $qtd_fotos;
		
		if(!empty($fotos)){
			foreach($fotos['FOTOS'] as $foto){
				$dados['PHOTOS'][$f]['SEQ_CHECKLIST_ITEM'] = $check['SEQ_DB'];
				$dados['PHOTOS'][$f]['TYPE'] = 'ok';
				$dados['PHOTOS'][$f]['TITLE'] = $questao;
				$dados['PHOTOS'][$f]['URL'] = $foto;	
				$f++;
			}
		}
		$c++;
	}
}

$this->queryData['data'] = $dados;

Entry point: Tela detalhes do checklist

Tela de detalhes

Exibe as imagens e detalhes do item.

Rota: checklist/<SEQ_CHECKLIST>/detail/<SEQ_DETALHE>
Ex: https://smartos-interno.h.simova.cloud/checklist/10/details/5

Entrada

$this->inputValues['checklist']; /*seq do checklist vindo da url*/
$this->inputValues['detail']; /*seq do detalhe vindo da url*/

Saida::Array

[
    "SEQ_CHECKLIST" => 10,
    "SEQ_CHECKLIST_ITEM" => 23,
    "TYPE" => 'ok', /* urgent, ready, ou ok*/
    "TITLE" => "Luces de servicio delanteras (altas y bajas)",
    "COMMENTS" => [
        "Tem que ver isso aí", "Comentário 2"
    ],
    "PHOTOS" => [
        [
            "URL" => "assets/img/checklist/teste_img.jpg",
            "PHOTO_TITLE" => "Vehículo: John Deere 6110j 2015",
            "PHOTO_SUBTITLE" => "Hora de Carga Certificada: 27/01/2023 7:46 p.m."
        ],
        [
            "URL" => "assets/img/checklist/teste_img1.jpg",
            "PHOTO_TITLE" => "Vehículo: John Wick 2015",
            "PHOTO_SUBTITLE" => "Hora de Carga Certificada: 20/10/2023 7:46 p.m."
        ]
    ]
];

Entrypoint (Exemplo - kurosu.h.simova.cloud)

FILE_OR_DOMAIN: CHECKLIST
ACTION: checklistDetail

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

//Filtros
$checklist_filtro = $this->inputValues['checklist'] ?? null;
$details_filtro = $this->inputValues['detail'] ?? null;

//Consulta Principal
$checklists_prospeccao = Dao::Table('apontamento_resposta_prospeccao')
	->select(['SEQ_DB',
			  'FUNCIONARIO_SEQ_DB',
			  'INI_DH',
			  'TIPO_EQUIPAMENTO_SEQ_DB',
			  'GRUPO_QUESTAO_SEQ_DB',
			  'QUESTAO_SEQ_DB',
			  'TIPO_RESPOSTA_SEQ_DB',
			  'OS_SEQ_DB',
			  'CLIENTE_SEQ_DB',
			  'CHASSI',
			  'SEQ_DB_DEVICE',
              'SEQ_DB_DEVICE_CHECKLIST',
			  'SEQ_DB_DEVICE_MASTER_SEQ_DB',
			  'OBSERVACAO'
	])
	->whereIn('SEQ_DB_DEVICE_CHECKLIST',$checklist_filtro)
	->whereIn('SEQ_DB',$details_filtro)
    ->orderBy('INI_DH','DESC')
	->get();
//FKs resultantes da consulta principal
$grupo_questao_fk = array_column($checklists_prospeccao,'GRUPO_QUESTAO_SEQ_DB','GRUPO_QUESTAO_SEQ_DB');
$questao_fk = array_column($checklists_prospeccao,'QUESTAO_SEQ_DB','QUESTAO_SEQ_DB');
$tipo_resposta_fk = array_column($checklists_prospeccao,'TIPO_RESPOSTA_SEQ_DB','TIPO_RESPOSTA_SEQ_DB');
$prospeccao_fk = array_column($checklists_prospeccao,'SEQ_DB_DEVICE','SEQ_DB_DEVICE');
$checklist_fk = array_column($checklists_prospeccao,'SEQ_DB_DEVICE_CHECKLIST','SEQ_DB_DEVICE_CHECKLIST');

//Consultas auxiliares
$apontamento_fotos_array = Dao::Table('APONTAMENTO_FOTO_RESPOSTA_PROSPECCAO')->select(['SEQ_DB_DEVICE_RESPOSTA_CHECKLIST','FOTO_THUMBNAIL'])->whereIn('SEQ_DB_DEVICE_RESPOSTA_CHECKLIST',$prospeccao_fk)->whereIn('SEQ_DB_DEVICE_CHECKLIST',$checklist_fk)->get();
$apontamento_fotos_list = [];
foreach($apontamento_fotos_array as $apt){
	if(!empty($apontamento_fotos_list[$apt['SEQ_DB_DEVICE_RESPOSTA_CHECKLIST']]['QTD_FOTOS'])){
		$apontamento_fotos_list[$apt['SEQ_DB_DEVICE_RESPOSTA_CHECKLIST']]['QTD_FOTOS'] = $apontamento_fotos_list[$apt['SEQ_DB_DEVICE_RESPOSTA_CHECKLIST']]['QTD_FOTOS'] + 1;
	}else{
		$apontamento_fotos_list[$apt['SEQ_DB_DEVICE_RESPOSTA_CHECKLIST']]['QTD_FOTOS'] = 1;
	}
	$apontamento_fotos_list[$apt['SEQ_DB_DEVICE_RESPOSTA_CHECKLIST']]['FOTOS'][] = $apt['FOTO_THUMBNAIL'];
}

$grupo_questao_array = Dao::Table('GRUPO_QUESTAO')->select(['SEQ_DB','DESCRICAO'])->whereIn('SEQ_DB',$grupo_questao_fk)->get();
$grupo_questao_list = array_column($grupo_questao_array,'DESCRICAO','SEQ_DB');

$questao_array = Dao::Table('QUESTAO')->select(['SEQ_DB','DESCRICAO'])->whereIn('SEQ_DB',$questao_fk)->get();
$questao_list = array_column($questao_array,'DESCRICAO','SEQ_DB');

$tipo_resposta_array = Dao::Table('TIPO_RESPOSTA')->select(['SEQ_DB','CLASSIFICACAO_RESPOSTA','DESCRICAO'])->whereIn('SEQ_DB',$tipo_resposta_fk)->get();
$tipo_resposta_list = array_column($tipo_resposta_array,null,'SEQ_DB');


//Variáveis para montagem dos dados
$dados = [];
$grupo_questao = null;
$questao = null;
$tipo_resposta = null;
$observacao = null;
$fotos = null;
$a=0;
//Montagem do array de dados
foreach($checklists_prospeccao as $check){
	//Preenchimento das variáveis
	$grupo_questao = $grupo_questao_list[$check['GRUPO_QUESTAO_SEQ_DB']] ?? null;
	$questao = $questao_list[$check['QUESTAO_SEQ_DB']] ?? null;
	$tipo_resposta = $tipo_resposta_list[$check['TIPO_RESPOSTA_SEQ_DB']] ?? null;
	$observacao = $check['OBSERVACAO'] ?? null;
	$fotos = $apontamento_fotos_list[$check['SEQ_DB_DEVICE']] ?? null;
	
	$dados['SEQ_CHECKLIST'] = $check['SEQ_DB_DEVICE_CHECKLIST'];
	$dados['SEQ_CHECKLIST_ITEM'] = $check['SEQ_DB'];
	if($tipo_resposta['CLASSIFICACAO_RESPOSTA'] == 2){
		$dados['TYPE'] = 'urgent';
	}else if($tipo_resposta['CLASSIFICACAO_RESPOSTA'] == 1){
		$dados['TYPE'] = 'ready';
	}else if($tipo_resposta['CLASSIFICACAO_RESPOSTA'] == 0){
		$dados['TYPE'] = 'ok';
	}
	$dados['TITLE'] = $questao;
	$dados['COMMENTS'][0] = $observacao;
	if(!empty($fotos)){
		foreach($fotos['FOTOS'] as $foto){
			$dados['PHOTOS'][$a]['URL'] = $foto;
			$dados['PHOTOS'][$a]['PHOTO_TITLE'] = $tipo_resposta['DESCRICAO'];
			$dados['PHOTOS'][$a]['PHOTO_SUBTITLE'] = '';
			$a++;
		}
	}
}

$this->queryData['data'] = $dados;

Novo Detalhes

Para criar um novo detalhes é necessário configurar o botão na tabea de nfs_core_ds_field_action (acesse aqui se não sabe como criar) e depois vincular a duas tabelas, se deseja ser por grupo

nfs_acl_grupo_permissao_field_action

ou usuário

nfs_acl_usuario_permissao_field_action

Quando criar esse item não há tabela associada, mas um NAME e no caso é DETAILS.

Campos do Novo Detalhes

Para um campo aparecer no novo detalhes o mesmo precisar está liberado para exibição na tela de detalhes e ter permissão para exibir/alterar, isso é encontrado na nfs_core_ds_tabela_campo com GRID igual a 1.

Exemplo:

Field Action - Botão para Criar um novo Boletim na Tela de Detalhes

Resultado:

novo_boletim_button.png

Também é possível criar um acesso para um relatório, basta criar mais um item com name DETAILS.

É possível criar um link passando um valor que esteja sendo disponibilizado no DISPLAY_FIELDS para ser parâmetro em um relatório, por enquanto esse recurso só está disponível para as additional tables.

{
  "TABLES": [
    {
      "NAME": "APONTAMENTO",
      "DISPLAY_FIELDS": ["NOME_COLUNA"],
      "LINKS": {
        "NOME_COLUNA": "/reports/assistencia_tecnica?form[os_fk][]=CODIGO_OS"
      }
    }
  ]
}

Como é possível observar tem a propriedade LINKS que fica dentro de APONTAMENTO, e o nome da coluna é o mesmo que é exibida no display fields, por enquanto somente está disponível para colunas que são exibidas na tela.

Exemplo Completo:

{
  "TABLES": [
    {
      "NAME": "APONTAMENTO",
      "FILTER": "",
      "ORDER": "INI_DH ASC",
      "DISPLAY_FIELDS": [
        "FIM_DH",
        "INI_FIM_DIFF_STR",
        "AUXILIAR_FK",
        "TIPO_APONTAMENTO",
        "SERVICO_FK",
        "PARADA_FK",
        "CODIGO_OS",
        "CLIENTE_FK"
      ],
      "EDITABLE_FIELDS": ["INI_DH"],
      "LINKS": {
        "CODIGO_OS": "/reports/assistencia_tecnica?form[os_fk][]=CODIGO_OS"
      }
    },
    {
      "NAME": "APONTAMENTO_DESLOCAMENTO",
      "FILTER": "",
      "ORDER": "INI_DH ASC",
      "DISPLAY_FIELDS": [
        "FIM_DH",
        "TEMPO_DESLOCAMENTO",
        "CODIGO_OS",
        "TIPO_DESLOCAMENTO",
        "VEICULO_FK",
        "KM_INICIAL",
        "KM_FINAL",
        "DIFERENCA_KM"
      ],
      "EDITABLE_FIELDS": ["KM_INICIAL", "KM_FINAL"]
    },
    {
      "NAME": "APONTAMENTO_STATUS_OS",
      "FILTER": "",
      "ORDER": "INI_DH ASC",
      "DISPLAY_FIELDS": ["CODIGO_OS", "TIPO_APONTAR", "MOTIVO_PAUSA_FK"],
      "LINKS": {
        "CODIGO_OS": "/reports/assistencia_tecnica?form[os][]=CODIGO_OS"
      }
    },

    {
      "NAME": "APONTAMENTO_DESPESA",
      "FILTER": "",
      "ORDER": "INI_DH ASC",
      "DISPLAY_FIELDS": ["CODIGO_OS", "DESPESA_FK", "DESCRICAO_DESPESA"]
    },

    {
      "NAME": "APONTAMENTO_FOTO",
      "FILTER": "",
      "ORDER": "INI_DH ASC",
      "DISPLAY_FIELDS": ["CODIGO_OS", "FOTO", "OBS"]
    }
  ]
}

Configuração de Cores

A configuração das cores foi criada para facilitar a visualização e identificação de boletins integrados e fechados que podem ser configurados no campo SECONDARY_TABLE_OPTIONS da tabela nfs_qig_details e apontamentos com valores especificos que podem ser configurados no campo ADDITIONAL_TABLES da tabela nfs_qig_details.

OBS: a configuração do JSON COLORS dos campos SECONDARY_TABLE_OPTIONS e ADDITIONAL_TABLES é diferente.

Configurações disponíveis para o array COLORS

Campos configuráveis do JSON INTEGRATION:

FIELD -> Nome do campo para integração
GRID -> Possui o valor **0** ou **1** para exibir ou não na Tabela;
VALUE -> Valor considerado como Integrado
LABEL -> Texto exibido na legenda
COLOR -> Cor do campo integrado
DATE -> JSON (mostrado abaixo) para configurar a exibição da data da integração
DESCRIPTION -> JSON (mostrado abaixo) para configurar a exibição da descrição da integração

Campos configuráveis do JSON **DATE**:
FIELD -> Nome do campo que possui o valor da data da integração
GRID -> Possui o valor **0** ou **1** para exibir ou não na Tabela;

Campos configuráveis do JSON **DESCRIPTION**:
FIELD -> Nome do campo que possui a descrição da integração
GRID -> Possui o valor **0** ou **1** para exibir ou não na Tabela;

Configuração da SECONDARY_TABLE_OPTIONS

O campo a ser utilizado como exmplo no array INTEGRATION é o PROC_ST, pode ser outro campo qualquer, assim vai ser setada a cor e você pode adicionar uma legenda. O array INTEGRATION tem esse nome porque os maiores casos será para adicionar cor para quando o boletim for integrado, porém pode ser utilizado para outras validações, basta adicionar no LABEL a legenda do que se trata a validação.

Dentro do array INTEGRATION ainda temos outros dois arrays que são:

  • DESCRIPTION: Em caso de validação de INTEGRAÇÃO pode ser adicionada a Descrição da Validação.
  • DATE: Em caso de INTEGRAÇÃO pode ser adicionda a data de integração do Boletim.

O array CLOSED serve apenas para adicionar cor quando o boletim for fechado, ou seja, quando ele tiver uma data FIM_DH. Neste caso possui uma validação verificando se ele possui FIM_DH e somente se tiver se4rá setada a cor.

A configuração da SECONDARY_TABLE_OPTIONS insere a cor no cabeçalho do boletim, como no exemplo do próximo tópico onde pode-se observar a cor lanja mais forte {.is-danger}

Exemplo de configuração completa:

{
  "TABLE": "BOLETIM",
  "FILTER": "FIM_DH IS NULL",
  "FILTER_INTERVAL_NUMBER": 1,
  "ORDER": "INI_DH DESC",
  "DISPLAY_FIELDS": [
    "INI_DH",
    "FIM_DH",
    "FUNCIONARIO_FK",
    "PROC_ST",
    "PROC_DH",
    "PROC_DESC"
  ],
  "EDITABLE_FIELDS": ["INI_DH", "FIM_DH"],
  "COLORS": {
    "INTEGRATION": {
      "FIELD": "PROC_ST",
      "GRID": "1",
      "VALUE": "1:Pronto para integrar",
      "LABEL": "integrado",
      "COLOR": "#D2691E",
      "DATE": {
        "FIELD": "PROC_DH",
        "GRID": "0"
      },
      "DESCRIPTION": {
        "FIELD": "PROC_DESC",
        "GRID": "0"
      }
    },
    "CLOSED": {
      "FIELD": "FIM_DH",
      "VALUE": "",
      "LABEL": "fechado",
      "COLOR": "#808080"
    }
  }
}

Configuração da ADDITIONAL_TABLES

A configuração no ADDITIONAL_TABLES insere cores nos campos das tabelas adicionais, como no exemplo do próximo tópico onde pode-se observar a cor lanja mais claro {.is-danger}

Campos configuráveis do JSON COLORS:

FIELD -> Nome do Campo a ser configurado,
VALUE -> Valor considerado para alterar cor,
LABEL -> Texto exibido na legenda,
COLOR -> Cor do campo que possui o valor do campo VALUE.

Exemplo de configuração completa:

{
  "TABLES": [
    {
      "NAME": "APONTAMENTO",
      "FILTER": "",
      "ORDER": "INI_DH ASC",
      "DISPLAY_FIELDS": [
        "FIM_DH",
        "INI_FIM_DIFF_STR",
        "AUXILIAR_FK",
        "TIPO_APONTAMENTO",
        "SERVICO_FK",
        "PARADA_FK",
        "CODIGO_OS",
        "CLIENTE_FK"
      ],
      "EDITABLE_FIELDS": ["INI_DH"],
      "COLORS": {
        "FIELD": "TIPO_APONTAMENTO",
        "VALUE": "2:Parada",
        "LABEL": "parada",
        "COLOR": "#F4A460"
      }
    },
    {
      "NAME": "APONTAMENTO_DESLOCAMENTO",
      "FILTER": "",
      "ORDER": "INI_DH ASC",
      "DISPLAY_FIELDS": [
        "FIM_DH",
        "TEMPO_DESLOCAMENTO",
        "CODIGO_OS",
        "TIPO_DESLOCAMENTO",
        "VEICULO_FK",
        "KM_INICIAL",
        "KM_FINAL",
        "DIFERENCA_KM"
      ],
      "EDITABLE_FIELDS": ["KM_INICIAL", "KM_FINAL"],
      "COLORS": {
        "FIELD": "TIPO_DESLOCAMENTO",
        "VALUE": "2:Fim Deslocamento",
        "LABEL": "fim deslocamento",
        "COLOR": "#7FFFD4"
      }
    },
    {
      "NAME": "APONTAMENTO_STATUS_OS",
      "FILTER": "",
      "ORDER": "INI_DH ASC",
      "DISPLAY_FIELDS": ["CODIGO_OS", "TIPO_APONTAR", "MOTIVO_PAUSA_FK"],
      "COLORS": {
        "FIELD": "TIPO_APONTAR",
        "VALUE": "4:Pausada",
        "LABEL": "OS pausada",
        "COLOR": "#40E0D0"
      }
    },

    {
      "NAME": "APONTAMENTO_DESPESA",
      "FILTER": "",
      "ORDER": "INI_DH ASC",
      "DISPLAY_FIELDS": ["CODIGO_OS", "DESPESA_FK", "DESCRICAO_DESPESA"]
    },

    {
      "NAME": "APONTAMENTO_FOTO",
      "FILTER": "",
      "ORDER": "INI_DH ASC",
      "DISPLAY_FIELDS": ["CODIGO_OS", "FOTO", "OBS"]
    }
  ]
}

Os campos declarados na configuração devem ser declarados também no DISPLAY_FIELDS para que ele possa ser trazido no select de campos, se não for declarado a condição não funcionará. Se não quiser que ele seja exibido na tabela basta inserir no array COLORS a opção "GRID": "0". A opção "GRID": "1" é opcional, pois ao inserir no DISPLAY_FIELDS o campo já será exibido na tabela automaticamente. {.is-danger}

Após adicionar essas configurações no banco de dados basta seguir os passos abaixo para verificar a funcionalidade:

1- Acessar a tela de Detalhes do Boletim 2- Clicar no link Nova exibição compacta

details_link.png

3- Verificar se as cores onde a condição corresponde com a configuração, como no exemplo em anexo. 4- Verificar se não há quebra de layout na nova tela de detalhes (acessada através do passo 2).

OBS: As configurações feitas no campo SECONDARY_TABLE_OPTIONS são para boletim integrado ou fechado, e as configurações no campo ADDITIONAL_TABLES é para alterar as cores do apontamento.

Ao configurar as cores deve-se aparecer na nova tela de detalhes, no topo da tela uma legenda com as cores e os label's configurados como no imagem de exemplo abaixo:

details_colors.png

OBS: a tela antiga de detalhes não muda com as configurações, apenas a nova tela acessada pelo passo 2.

Modo compacto e modo normal

Como é possível obsevar no painel existem dois modos de visualização:

  • normal: Modo normal ou antigo, que é aberto por padrão se não for configurado nada.

  • compact: Modo compacto ou novo, onde é possível realizar configurações e exibir mais informações.

Por padrão sempre vai abrir no modo compacto, mas caso haja a necessidade abrir no direto no modo compacto é possível usar o parâmetro DETAILS_MODE com o valor compact.

sql script: INSERT INTO nfs_core_par_parametros (EMPRESA, FILIAL, LOCAL, NOME, CONTEUDO, TIPO) VALUES(9999, 9999, 9999, 'DETAILS_MODE', 'compact', 1);

Caso já exista o parâmetro, é só mudar de compact para normal e vai abrir no modo normal/antigo.

Painel MO

*Recurso disponivel em homol - fase de testes

Sobre

Painel possibilita visualizar informações dos boletins abertos e seus apontamentos.

Requisitos e configurações

O Painel MO necessita de uma tabela auxiliar que vai ajudar na questão do desempenho, ao invés de buscarmos dentro da tabela mo_boletim onde teoricamente teríamos uma consulta mais lenta por se tratar de uma tabela bem populada vamos buscar os boletins abertos nessa tabela auxiliar que terá apenas o registro do último boletim aberto de cada encarregado.

Essa tabela auxiliar funciona da seguinte forma, a cada boletim aberto por um encarregado essa tabela é atualizada para termos sempre os dados atualizados dos ultimos boletins.

Abaixo temos o SQL para criação da tabela e seus campos via tabela nfs_core_ds_tabela e nfs_core_ds_tabela_campo:

INSERT INTO nfs_core_ds_tabela
(NOME, TABELA_REAL, DESCRICAO, UNQ, DISPLAY, TIPO, MOBILE_TABLE, MOBILE_MESSAGE, MOBILE_MESSAGE_FK, FILTRO_USER, MAX_ROWS_WO_FILTER, `OPTIONS`, ORDER_BY, INDICATORS)
VALUES('MO_BOLETIM_ENCARREGADO', 'APP_MO_BOLETIM_ENCARREGADO', 'Último boletim', NULL, 'SEQ_DB', 1, 0, 0, NULL, 0, 1000, NULL, NULL, NULL);
INSERT INTO nfs_core_ds_tabela_campo
(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('MO_BOLETIM_ENCARREGADO', 'BOLETIM', NULL, 0, 1, 1, 1, 'Seq Boletim', 'Seq Boletim', 'FK', 1, NULL, NULL, NULL, 'IU', '0', NULL, 'MO_BOLETIM', NULL, NULL, NULL, NULL, NULL, NULL, NULL);

INSERT INTO nfs_core_ds_tabela_campo
(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('MO_BOLETIM_ENCARREGADO', 'ENCARREGADO', NULL, 0, 1, 1, 1, 'Seq Encarregado', 'Seq Encarregado', 'FK', 1, NULL, NULL, NULL, 'IU', '0', NULL, 'EFETIVO_FUNCIONARIO', NULL, NULL, NULL, NULL, NULL, NULL, NULL);

Configs

Parametros nfs_core_par_parametros

INSERT INTO nfs_homol_civilmaster_mo_construmobil.nfs_core_par_parametros
(EMPRESA, FILIAL, `LOCAL`, NOME, CONTEUDO, TIPO)
VALUES(9999, 9999, 9999, 'PAINEL_MO', '{
	"ENABLE": true,
	"BOLETIM_TABLE": "MO_BOLETIM",
	"BOLETIM_ENTITY_KEY": "EFETIVO_FUNCIONARIO_SEQ_DB",
	"STATUS_TABLE": {
		"ALIAS": "mo_boletim_encarregado",
		"NAME": "app_mo_boletim_encarregado",
		"BOLETIM_FIELD": "BOLETIM_SEQ_DB",
		"ENTITY_FIELD": "ENCARREGADO_SEQ_DB",
		"ENTITY_KEY": "EFETIVO_FUNCIONARIO_SEQ_DB"
	}
}', 1);

Tabela de configuração dos paineis MO

Na tabela nfs_mo_panels poderão ser criados multiplos painéis, temos alguns parametros iniciais que precisam ser configurados para o funcionamento.

MAIN_TABLE e SECONDARY_TABLE

Usados para identificar as tabelas principais do painel. Como exemplo estamos usando a estrutura da civilmaster-mo onde MAIN_TABLE é app_efetivo_funcionario e SECONDARY_TABLE é app_mo_boletim.

RELATIONSHIPS

Guarda um json com as tabelas utilizadas categorizadas e seus respectivos campos que são utilizados no recurso.

  • Exemplo:
{
 	"TABLE_MAIN": {
  		"NAME": "app_efetivo_funcionario",
		"FIELDS": {
			"NOME": "NOME",
			"FUNCAO": "EFETIVO_FUNCAO_SEQ_DB",
			"FLAG": "FLAG_ENCARREGADO",
			"QDISPLAY": "QIG_DISPLAY",
			"CRACHA": "CRACHA",
			"ENCARREGADO": "ENCARREGADO_SEQ_DB"
		}
    },
 	"TABLE_SECONDARY": {
  		"NAME": "app_mo_boletim",
		"FIELDS": {
			"ENTITY": "EFETIVO_FUNCIONARIO_SEQ_DB"
		}
    },
	"TABLE_FUNCAO": {
		"NAME": "app_efetivo_funcao",
      	"FIELDS": {
       		"DESC": "DESCRICAO"
        }
	},
	"TABLE_MENSAGEM": {
		"NAME": "app_apontamento_mo_mensagem",
		"FIELDS": {
			"DESC": "DESCRICAO",
			"ENTITY_SEQ": "EFETIVO_FUNCIONARIO_SEQ_DB",
			"DEVICEMASTER": "SEQ_DB_DEVICE_MASTER_SEQ_DB",
			"INS_DH": "INS_DH"
		}
	},
	"TABLE_APT": {
		"NAME": "app_mo_apt",
		"FIELDS": {
			"ENTITY": "EFETIVO_FUNCIONARIO_SEQ_DB",
			"TIPO": "TIPO_APONTAMENTO",
			"OPERACAO": "OPER_SEQ_DB",
			"OPERADOR": "OPERADOR_SEQ_DB",
			"DEVICEMASTER": "SEQ_DB_DEVICE_MASTER_SEQ_DB",
			"LAST_UPD": "UPD_DH"
		}
	},
	"TABLE_OPERACAO": {
		"NAME": "app_oper",
		"FIELDS": {
			"DESC": "DESCRICAO",
			"SUBATIVIDADE": "SUBATIVIDADE_PRODUCAO_SEQ_DB"
		}
	},
	"TABLE_SUBATIVIDADE": {
		"NAME": "app_subatividade",
		"FIELDS": {
			"DESC": "DESCRICAO"
		}
	}
}

STATUS_TABLE

Guarda um json com a configuração da tabela auxiliar, e seus campos.

  • Exemplo:
{
	"NAME": "app_mo_boletim_encarregado",
	"BOLETIM_FIELD": "BOLETIM_SEQ_DB",
	"ENTITY_FIELD": "ENCARREGADO_SEQ_DB"
}

CONFIG

Configurações gerais.

  • Exemplo:
{
	"ENCARREGADO_ORDEM": "CRACHA"
}

INDICATORS e INDICATORS_OPTIONS

É possível adicionar o recurso de indicadores padrão do NFS também no Painel MO, o funcionamento é o mesmo já utilizado em outras partes do sistema.

REFRESH_INTERVAL

Controla o intervalo que o painel é atualizado, valor em segundos.

SQL da configuração completa do painel

INSERT INTO nfs_mo_panels
(SEQ_DB, EMPRESA, FILIAL, `LOCAL`, NAME, DISPLAY_NAME, SHORT_NAME, MAIN_TABLE, RELATIONSHIPS, SECONDARY_TABLE, REFRESH_INTERVAL, SCROLL_TIME, INDICATORS, INDICATORS_OPTIONS, INS_DH, UPD_DH, CONFIG, STATUS_TABLE)
VALUES(1, 1, 1, 1, NULL, NULL, NULL, 'app_efetivo_funcionario', '{
 	"TABLE_MAIN": {
  		"NAME": "app_efetivo_funcionario",
		"FIELDS": {
			"NOME": "NOME",
			"FUNCAO": "EFETIVO_FUNCAO_SEQ_DB",
			"FLAG": "FLAG_ENCARREGADO",
			"QDISPLAY": "QIG_DISPLAY",
			"CRACHA": "CRACHA",
			"ENCARREGADO": "ENCARREGADO_SEQ_DB"
		}
    },
 	"TABLE_SECONDARY": {
  		"NAME": "app_mo_boletim",
		"FIELDS": {
			"ENTITY": "EFETIVO_FUNCIONARIO_SEQ_DB"
		}
    },
	"TABLE_FUNCAO": {
		"NAME": "app_efetivo_funcao",
      	"FIELDS": {
       		"DESC": "DESCRICAO"
        }
	},
	"TABLE_MENSAGEM": {
		"NAME": "app_apontamento_mo_mensagem",
		"FIELDS": {
			"DESC": "DESCRICAO",
			"ENTITY_SEQ": "EFETIVO_FUNCIONARIO_SEQ_DB",
			"DEVICEMASTER": "SEQ_DB_DEVICE_MASTER_SEQ_DB",
			"INS_DH": "INS_DH"
		}
	},
	"TABLE_APT": {
		"NAME": "app_mo_apt",
		"FIELDS": {
			"ENTITY": "EFETIVO_FUNCIONARIO_SEQ_DB",
			"TIPO": "TIPO_APONTAMENTO",
			"OPERACAO": "OPER_SEQ_DB",
			"OPERADOR": "OPERADOR_SEQ_DB",
			"DEVICEMASTER": "SEQ_DB_DEVICE_MASTER_SEQ_DB",
			"LAST_UPD": "UPD_DH"
		}
	},
	"TABLE_OPERACAO": {
		"NAME": "app_oper",
		"FIELDS": {
			"DESC": "DESCRICAO",
			"SUBATIVIDADE": "SUBATIVIDADE_PRODUCAO_SEQ_DB"
		}
	},
	"TABLE_SUBATIVIDADE": {
		"NAME": "app_subatividade",
		"FIELDS": {
			"DESC": "DESCRICAO"
		}
	}
}', 'app_mo_boletim', 10, 5, '{
	"TOTALSERVICO": {
		"label": "Serviço",
		"color": "green",
		"icon": "fa fa-users",
		"display": "[result]",
		"decimalPlace": "0",
		"queryBuilder": {}
	},
	"TOTALPERDA": {
		"label": "Perda",
		"color": "red",
		"icon": "fa fa-window-close-o",
		"display": "[result]",
		"decimalPlace": "0",
		"queryBuilder": {}
	},
	"SEMAPONTAMENTO": {
		"label": "Sem Apontamento",
		"color": "blue",
		"icon": "fa fa-clock-o",
		"display": "[result]",
		"decimalPlace": "0",
		"queryBuilder": {}
	},
	"TOTALGERAL": {
		"label": "Total",
		"color": "yellow",
		"icon": "fa fa-calendar-times-o",
		"display": "[result]",
		"decimalPlace": "0",
		"queryBuilder": {}
	}
}', NULL, '2024-06-28 09:09:32', '2024-07-05 16:16:39', '{
	"ENCARREGADO_ORDEM": "CRACHA"
}', '{
	"NAME": "app_mo_boletim_encarregado",
	"BOLETIM_FIELD": "BOLETIM_SEQ_DB",
	"ENTITY_FIELD": "ENCARREGADO_SEQ_DB"
}');

Visão geral

O recurso possui filtros, lista de encarregados e um painel onde são exibidos em formato de cards os dados por encarregado.

Tela do Painel MO

Filtros

Inicialmente o recurso possui 3 filtros, por encarregados, por atividades, e se exibe atividades improdutivas ou não.

Filtros

Indicadores

O componente de indicadores é o mesmo que temos em outras partes do sistema, para configurá-lo, basta seguir os passos de configuração padrão no campo INDICATORS da tabela nfs_core_ds_tabela para a tabela EFETIVO_FUNCIONARIO.

A documentação dos indicadores pode ser acessada clicando aqui.

Exemplo inicial:

{
	"TOTALSERVICO": {
		"label": "Serviço",
		"color": "green",
		"icon": "fa fa-users",
		"display": "[result]",
		"decimalPlace": "0",
		"queryBuilder": {}
	},
	"TOTALPERDA": {
		"label": "Perda",
		"color": "red",
		"icon": "fa fa-window-close-o",
		"display": "[result]",
		"decimalPlace": "0",
		"queryBuilder": {}
	},
	"SEMAPONTAMENTO": {
		"label": "Sem Apontamento",
		"color": "blue",
		"icon": "fa fa-clock-o",
		"display": "[result]",
		"decimalPlace": "0",
		"queryBuilder": {}
	},
	"TOTALGERAL": {
		"label": "Total",
		"color": "yellow",
		"icon": "fa fa-calendar-times-o",
		"display": "[result]",
		"decimalPlace": "0",
		"queryBuilder": {}
	}
}

Lista de encarregados

A lista de encarregados irá mostrar todos os encarregados (EFETIVO_FUNCIONARIO com o campo QIG_DISPLAY == 1), encarregados com o ícone colorido estão com boletim aberto. O click no encarregado abre um pop-up de detalhes.

Lista encarregados

Painel - Cards encarregados

No painel são exibidos para cada encarregado um card contendo informações relacionadas ao boletim aberto.

Painel cards

Cards

Card

Dentro de cada card teremos algumas informações e diferentes ações, vamos ver cada uma delas abaixo:

(1) Encarregado: Abre a tela de detalhes do encarregado.

Detalhes do encarregado

(2) Contagem de atividades: Abre a tela de detalhes por status, "em serviço", "perda", e "sem apontamento" exibindo todos técnicos agrupados pela atividadde.

Detalhes de atividades por tipo

(3) Detalhes da atividade: Abre a tela de detalhes de uma atividade.

Detalhes da atividade

(4) Exibe a quantidade de tecnicos na equipe: Considera os técnicos relacionados ao encarregado e técnicos apontados pelo encarregado.

(5) Botões auxiliares:

  • Mensagens: Exibe as mensagens do boletim. Detalhes das mensagens

  • Mapa: Abre o mapa (trajeto).

  • Comunicação: Mostra os detalhes da última comunicação. Última comunicação

Implementando recurso

Ao liberar o recurso precisamos entender que a tabela de status será populada conforme os boletins são abertos. Caso seja necessário trazer para a tabela de status os boletins que já foram abertos, pode ser utilizado o script abaixo para trazer esses retroativos.

SET @id = (SELECT IFNULL(MAX(ID), 0) FROM app_mo_boletim_encarregado);

INSERT INTO app_mo_boletim_encarregado (EMPRESA, FILIAL, LOCAL, INS_USUARIO_SEQ_DB, BOLETIM_SEQ_DB, ENCARREGADO_SEQ_DB, ID)
SELECT 
    latest_boletim.EMPRESA,
    latest_boletim.FILIAL,
    latest_boletim.LOCAL,
    latest_boletim.INS_USUARIO_SEQ_DB,
    latest_boletim.SEQ_DB,
    latest_boletim.EFETIVO_FUNCIONARIO_SEQ_DB,
    (@id := @id + 1)
FROM 
    (SELECT 
        b.EMPRESA,
        b.FILIAL,
        b.LOCAL,
        b.INS_USUARIO_SEQ_DB,
        b.SEQ_DB,
        b.EFETIVO_FUNCIONARIO_SEQ_DB,
        b.INI_DH,
        ROW_NUMBER() OVER (PARTITION BY b.EFETIVO_FUNCIONARIO_SEQ_DB ORDER BY b.INI_DH DESC) as rn
    FROM 
        app_mo_boletim b
    WHERE 
        b.FIM_DH IS NULL
        AND b.EFETIVO_FUNCIONARIO_SEQ_DB IS NOT NULL) as latest_boletim
LEFT JOIN
    app_mo_boletim_encarregado e
ON 
    latest_boletim.EFETIVO_FUNCIONARIO_SEQ_DB = e.ENCARREGADO_SEQ_DB
INNER JOIN
    app_efetivo_funcionario f
ON
    latest_boletim.EFETIVO_FUNCIONARIO_SEQ_DB = f.SEQ_DB
WHERE 
    latest_boletim.rn = 1
    AND e.ENCARREGADO_SEQ_DB IS NULL;

Criação da rota para o Painel de Abastecimento

Para podermos acessar o painel de abastecimento é necessário criarmos a rota para ele, isto pode ser feito utilizando o sql abaixo:

INSERT INTO nfs_acl_route_custom(DISPLAY_NAME, NAME, `TYPE`, ROUTE, INS_DH, ENABLED, DELETED)
VALUES('Supply / Abastecimento', 'supply', 'DASH', '/custom/supply', '2019-10-21 09:22:10', 1, 0);

Ver como criar rotas customizadas

Criando a tabela de configuração

A tabela de configuração é onde poderemos configurar o nosso painel, para isso deveremos criar a estrutura da mesma, segue abaixo o modelo DDL:

CREATE TABLE `nfs_supply` (
  `SEQ_DB` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `EMPRESA` int(11) DEFAULT NULL,
  `FILIAL` int(11) DEFAULT NULL,
  `LOCAL` int(11) DEFAULT NULL,
  `TITLE` varchar(100) DEFAULT NULL,
  `FILTERS` text,
  `OPTIONS` text,
  `ENABLED` int(1) DEFAULT '1',
  UNIQUE KEY `SEQ_DB_UNIQUE` (`SEQ_DB`)
) DEFAULT CHARSET=utf8;

Exemplo de registro:

INSERT INTO nfs_supply(SEQ_DB, EMPRESA, FILIAL, `LOCAL`, TITLE, FILTERS, `OPTIONS`, ENABLED)
VALUES(1, 9999, 9999, 9999, 'Supply / Abastecimento', '', '{
  "ENTIDADES": {
    "POSTO": {
      "title": "Postos Fixos",
      "classe": "col-md-6",
      "main": {
        "select": [
          "SEQ_DB AS SEQ_DB",
          "CONCAT(CODIGO, '' :: '', DESCRICAO) AS DESCRICAO"
        ],
        "from": "POSTO",
        "where": [
          "SEQ_DB in (:seqDb)"
        ],
        "order_by": [
            ["SEQ_DB", "ASC"]
        ]
      },
      "tanque": {
        "select": [
          "CONCAT(C.CODIGO, '' :: '', C.DESCRICAO) AS COMBUSTIVEL",
          "T.SEQ_DB AS SEQ_DB",
          "CONCAT(T.CODIGO, '' :: '', T.DESCRICAO) AS DESCRICAO",
          "T.CAPACIDADE_CARGA AS CAPACIDADE_CARGA",
          "T.ESTOQUE_ALERTA AS ESTOQUE_ALERTA",
          "T.SALDO AS SALDO"
        ],
        "from": "POSTO_TANQUE T",
        "inner_join": [
          [
            "T",
            "COMBUSTIVEL",
            "C",
            "C.SEQ_DB = T.COMBUSTIVEL_SEQ_DB"
          ]
        ],
        "where": [
          "T.POSTO_SEQ_DB = :seqDb"
        ]
      },
      "bomba": {
        "select": [
          "SEQ_DB AS SEQ_DB",
          "CONCAT(CODIGO, '' :: '', DESCRICAO) AS DESCRICAO"
        ],
        "from": "POSTO_BOMBA",
        "where": [
          "POSTO_TANQUE_SEQ_DB = :seqDb"
        ]
      },
      "movimento": {
        "select": [
          "QUANTIDADE AS QUANTIDADE",
          "TIPO_MOVIMENTO AS TIPO_MOVIMENTO"
        ],
        "from": "MOVIMENTO_ESTOQUE",
        "where": [
          "POSTO_BOMBA_SEQ_DB = :seqDb"
        ]
      }
    }
  }
}', 1);

Configuração das Tabelas no DS

Será necessário inserir alguns registros nas tabelas CORE_DS_TABELA e CORE_DS_TABELA_CAMPO para que possamos utilizar esse painel, esse registros são:

1. APP_POSTO

-- TABELA
INSERT INTO nfs_core_ds_tabela(NOME, TABELA_REAL, DESCRICAO, UNQ, DISPLAY, TIPO, MOBILE_TABLE, MOBILE_MESSAGE, MOBILE_MESSAGE_FK, FILTRO_USER, MAX_ROWS_WO_FILTER, `OPTIONS`, ORDER_BY, INDICATORS)
VALUES('POSTO', 'APP_POSTO', 'Posto de Abastecimento', NULL, 'CODIGO,DESCRICAO', 1, 0, 0, NULL, 0, 100, NULL, NULL, NULL);

-- CAMPOS
INSERT INTO nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('POSTO', 'CODIGO', 2, 0, 1, 1, 1, 'Código', 'Código', 'TXT', 1, 50, NULL, NULL, 'IU', '0', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);

INSERT INTO nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('POSTO', 'DESCRICAO', 3, 0, 1, 1, 1, 'Descrição', 'Descrição', 'TXT', 1, 250, NULL, NULL, 'IU', '0', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);

2. COMBUSTIVEL

-- TABELA
INSERT INTO nfs_core_ds_tabela (NOME, TABELA_REAL, DESCRICAO, UNQ, DISPLAY, TIPO, MOBILE_TABLE, MOBILE_MESSAGE, MOBILE_MESSAGE_FK, FILTRO_USER, MAX_ROWS_WO_FILTER, `OPTIONS`, ORDER_BY, INDICATORS) VALUES('COMBUSTIVEL', 'APP_COMBUSTIVEL', 'Combustível', NULL, 'CODIGO,DESCRICAO', 1, 0, 0, NULL, 0, 100, NULL, NULL, NULL);

-- CAMPOS
INSERT INTO nfs_core_ds_tabela_campo (TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES) VALUES('COMBUSTIVEL', 'CODIGO', 2, 0, 1, 1, 1, 'Código', 'Código', 'TXT', 1, 50, NULL, NULL, 'IU', '0', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);

INSERT INTO nfs_core_ds_tabela_campo (TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES) VALUES('COMBUSTIVEL', 'DESCRICAO', 3, 0, 1, 1, 1, 'Descrição', 'Descrição', 'TXT', 1, 250, NULL, NULL, 'IU', '0', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);

3. POSTO_TANQUE

-- TABELA
INSERT INTO nfs_core_ds_tabela(NOME, TABELA_REAL, DESCRICAO, UNQ, DISPLAY, TIPO, MOBILE_TABLE, MOBILE_MESSAGE, MOBILE_MESSAGE_FK, FILTRO_USER, MAX_ROWS_WO_FILTER, `OPTIONS`, ORDER_BY, INDICATORS)
VALUES('POSTO_TANQUE', 'APP_POSTO_TANQUE', 'Tanque de Abastecimento', NULL, 'CODIGO,DESCRICAO', 1, 0, 0, NULL, 0, 100, NULL, NULL, NULL);

-- CAMPOS
INSERT INTO nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('POSTO_TANQUE', 'CAPACIDADE_CARGA', 5, 0, 1, 1, 1, 'Capacidade de Carga', 'Capacidade de Carga', 'DECIMAL', 1, 10, 2, '9[9][9][9][9][9][9][9].99', 'IU', '0', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);

INSERT INTO nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('POSTO_TANQUE', 'CODIGO', 2, 0, 1, 1, 1, 'Código', 'Código', 'TXT', 1, 50, NULL, NULL, 'IU', '0', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);

INSERT INTO nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('POSTO_TANQUE', 'COMBUSTIVEL', 4, 0, 1, 1, 1, 'Combustível', 'Combustível', 'FK', 1, NULL, NULL, NULL, 'IU', '0', NULL, 'COMBUSTIVEL', NULL, NULL, NULL, NULL, NULL, NULL, NULL);

INSERT INTO nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('POSTO_TANQUE', 'DESCRICAO', 3, 0, 1, 1, 1, 'Descrição', 'Descrição', 'TXT', 1, 250, NULL, NULL, 'IU', '0', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);

INSERT INTO nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('POSTO_TANQUE', 'ESTOQUE_ALERTA', 6, 0, 1, 1, 1, 'Estoque Mínimo', 'Estoque Mínimo', 'DECIMAL', 1, 10, 2, '9[9][9][9][9][9][9][9].99', 'IU', '0', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);

INSERT INTO nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('POSTO_TANQUE', 'POSTO', 1, 0, 1, 1, 1, 'Posto', 'Posto', 'FK', 1, NULL, NULL, NULL, 'IU', '0', NULL, 'POSTO', NULL, NULL, NULL, NULL, NULL, NULL, NULL);

INSERT INTO nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('POSTO_TANQUE', 'SALDO', 7, 0, 1, 1, 1, 'Quantidade Disponível', 'Quantidade Disponível', 'DECIMAL', NULL, 10, 2, '9[9][9][9][9][9][9][9].99', 'IU', '1', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);

4. CENTRO_CUSTO

-- TABELA
INSERT INTO nfs_core_ds_tabela (NOME, TABELA_REAL, DESCRICAO, UNQ, DISPLAY, TIPO, MOBILE_TABLE, MOBILE_MESSAGE, MOBILE_MESSAGE_FK, FILTRO_USER, MAX_ROWS_WO_FILTER, `OPTIONS`, ORDER_BY, INDICATORS) VALUES('CENTRO_CUSTO', 'APP_CENTRO_CUSTO', 'Centro de Custo', NULL, 'CODIGO,DESCRICAO', 1, 0, 0, NULL, 0, 100, NULL, NULL, NULL);

-- CAMPOS
INSERT INTO nfs_core_ds_tabela_campo (TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES) VALUES('CENTRO_CUSTO', 'CODIGO', 2, 0, 1, 1, 1, 'Código', 'Código', 'TXT', 1, 50, NULL, NULL, 'IU', '0', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);

INSERT INTO nfs_core_ds_tabela_campo (TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES) VALUES('CENTRO_CUSTO', 'DESCRICAO', 3, 0, 1, 1, 1, 'Descrição', 'Descrição', 'TXT', 1, 250, NULL, NULL, 'IU', '0', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);

5. POSTO_BOMBA

-- TABELA
INSERT INTO nfs_core_ds_tabela(NOME, TABELA_REAL, DESCRICAO, UNQ, DISPLAY, TIPO, MOBILE_TABLE, MOBILE_MESSAGE, MOBILE_MESSAGE_FK, FILTRO_USER, MAX_ROWS_WO_FILTER, `OPTIONS`, ORDER_BY, INDICATORS)
VALUES('POSTO_BOMBA', 'APP_POSTO_BOMBA', 'Bomba de Abastecimento', NULL, 'CODIGO,DESCRICAO', 1, 0, 0, NULL, 0, 100, NULL, NULL, NULL);

-- CAMPOS
INSERT INTO nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('POSTO_BOMBA', 'CENTRO_CUSTO', 4, 0, 1, 1, 1, 'Centro de Custo', 'Centro de Custo', 'FK', 1, NULL, NULL, NULL, 'IU', '0', NULL, 'CENTRO_CUSTO', NULL, NULL, NULL, NULL, NULL, NULL, NULL);

INSERT INTO nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('POSTO_BOMBA', 'CODIGO', 2, 0, 1, 1, 1, 'Código', 'Código', 'TXT', 1, 50, NULL, NULL, 'IU', '0', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);

INSERT INTO nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('POSTO_BOMBA', 'DESCRICAO', 3, 0, 1, 1, 1, 'Descrição', 'Descrição', 'TXT', 1, 250, NULL, NULL, 'IU', '0', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);

INSERT INTO nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('POSTO_BOMBA', 'POSTO_TANQUE', 1, 0, 1, 1, 1, 'Tanque', 'Tanque', 'FK', 1, NULL, NULL, NULL, 'IU', '0', NULL, 'POSTO_TANQUE', NULL, NULL, NULL, NULL, NULL, NULL, NULL);

6. MOVIMENTO_ESTOQUE

-- TABELA
INSERT INTO nfs_core_ds_tabela(NOME, TABELA_REAL, DESCRICAO, UNQ, DISPLAY, TIPO, MOBILE_TABLE, MOBILE_MESSAGE, MOBILE_MESSAGE_FK, FILTRO_USER, MAX_ROWS_WO_FILTER, `OPTIONS`, ORDER_BY, INDICATORS)
VALUES('MOVIMENTO_ESTOQUE', 'APP_MOVIMENTO_ESTOQUE', 'Movimentação de Estoque', NULL, 'ID', 3, 0, 0, NULL, 0, 100, NULL, NULL, NULL);

-- CAMPOS
INSERT INTO nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('MOVIMENTO_ESTOQUE', 'COMBUSTIVEL', 7, 0, 1, 1, 1, 'Combustível', 'Combustível', 'FK', NULL, NULL, NULL, NULL, 'IU', '1', NULL, 'COMBUSTIVEL', NULL, NULL, NULL, NULL, NULL, NULL, NULL);

INSERT INTO nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('MOVIMENTO_ESTOQUE', 'INI_DH', 1, 0, 1, 1, 1, 'Data Movimentação', 'Data Movimentação', 'DHS', NULL, NULL, NULL, NULL, 'IU', '1', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);

INSERT INTO nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('MOVIMENTO_ESTOQUE', 'POSTO', 3, 0, 1, 1, 1, 'Posto', 'Posto', 'FK', NULL, NULL, NULL, NULL, 'IU', '1', NULL, 'POSTO', NULL, NULL, NULL, NULL, NULL, NULL, NULL);

INSERT INTO nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('MOVIMENTO_ESTOQUE', 'POSTO_BOMBA', 5, 0, 1, 1, 1, 'Bomba', 'Bomba', 'FK', NULL, NULL, NULL, NULL, 'IU', '1', NULL, 'POSTO_BOMBA', NULL, NULL, NULL, NULL, NULL, NULL, NULL);

INSERT INTO nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('MOVIMENTO_ESTOQUE', 'POSTO_TANQUE', 4, 0, 1, 1, 1, 'Tanque', 'Tanque', 'FK', NULL, NULL, NULL, NULL, 'IU', '1', NULL, 'POSTO_TANQUE', NULL, NULL, NULL, NULL, NULL, NULL, NULL);

INSERT INTO nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('MOVIMENTO_ESTOQUE', 'QUANTIDADE', 6, 0, 1, 1, 1, 'Quantidade', 'Quantidade', 'DECIMAL', 0, 10, 2, '9[9][9][9][9][9][9].99', 'IU', '1', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);

INSERT INTO nfs_core_ds_tabela_campo(TABELA_NOME, NOME, SEQ, SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA, TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE, DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT, VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES)
VALUES('MOVIMENTO_ESTOQUE', 'TIPO_MOVIMENTO', 2, 0, 1, 1, 1, 'Evento', 'Evento', 'LOV1', 0, NULL, NULL, NULL, 'IU', '1', NULL, NULL, NULL, '1:Abastecimento;2:Recebimento;3:Transferência Saída;4:Transferência Entrada;5:Outros Fins;6:Transferência Comboio', NULL, NULL, NULL, NULL, NULL);

Populando as tabelas

Após configurarmos o CORE_DS_TABELA/TABELA_CAMPO, precisamos acessar o admin console e executar o create DS/DDL, feito isso, basta popular as tabelas criadas com alguns registros caso queira ver o resultado:

INSERT INTO app_centro_custo (`SEQ_DB`, `EMPRESA`, `FILIAL`, `LOCAL`, `INS_DH`, `INS_USUARIO_SEQ_DB`, `UPD_DH`, `UPD_USUARIO_SEQ_DB`, `SOURCE`, `ATIVO`, `DELETED`, `RO`, `ID`, `CODIGO`, `DESCRICAO`) VALUES(1, 9999, 9999, 9999, '2019-10-10 17:33:57', 1, '2019-10-10 17:33:57', NULL, NULL, 1, 0, 0, 1, 'CC01', 'Centro de Custo 01');
INSERT INTO app_combustivel (`SEQ_DB`, `EMPRESA`, `FILIAL`, `LOCAL`, `INS_DH`, `INS_USUARIO_SEQ_DB`, `UPD_DH`, `UPD_USUARIO_SEQ_DB`, `SOURCE`, `ATIVO`, `DELETED`, `RO`, `ID`, `CODIGO`, `DESCRICAO`) VALUES(1, 9999, 9999, 9999, '2019-10-10 17:28:39', 1, '2019-10-10 17:28:39', NULL, NULL, 1, 0, 0, 1, 'G', 'Gasolina');
INSERT INTO app_combustivel (`SEQ_DB`, `EMPRESA`, `FILIAL`, `LOCAL`, `INS_DH`, `INS_USUARIO_SEQ_DB`, `UPD_DH`, `UPD_USUARIO_SEQ_DB`, `SOURCE`, `ATIVO`, `DELETED`, `RO`, `ID`, `CODIGO`, `DESCRICAO`) VALUES(2, 9999, 9999, 9999, '2019-10-10 17:28:52', 1, '2019-10-10 17:28:52', NULL, NULL, 1, 0, 0, 2, 'E', 'Etanol');
INSERT INTO app_combustivel (`SEQ_DB`, `EMPRESA`, `FILIAL`, `LOCAL`, `INS_DH`, `INS_USUARIO_SEQ_DB`, `UPD_DH`, `UPD_USUARIO_SEQ_DB`, `SOURCE`, `ATIVO`, `DELETED`, `RO`, `ID`, `CODIGO`, `DESCRICAO`) VALUES(3, 9999, 9999, 9999, '2019-10-10 17:29:02', 1, '2019-10-10 17:29:02', NULL, NULL, 1, 0, 0, 3, 'D', 'Diesel');
INSERT INTO app_posto (`SEQ_DB`, `EMPRESA`, `FILIAL`, `LOCAL`, `INS_DH`, `INS_USUARIO_SEQ_DB`, `UPD_DH`, `UPD_USUARIO_SEQ_DB`, `SOURCE`, `ATIVO`, `DELETED`, `RO`, `ID`, `CODIGO`, `DESCRICAO`) VALUES(1, 9999, 9999, 9999, '2019-10-10 17:27:22', 1, '2019-10-14 16:42:39', 1, NULL, 1, 0, 0, 1, 'YPIRANGA', 'Posto Ypiranga');
INSERT INTO app_posto (`SEQ_DB`, `EMPRESA`, `FILIAL`, `LOCAL`, `INS_DH`, `INS_USUARIO_SEQ_DB`, `UPD_DH`, `UPD_USUARIO_SEQ_DB`, `SOURCE`, `ATIVO`, `DELETED`, `RO`, `ID`, `CODIGO`, `DESCRICAO`) VALUES(2, 9999, 9999, 9999, '2019-10-14 16:42:06', 1, '2019-10-14 16:42:06', NULL, NULL, 1, 0, 0, 2, 'BR', 'Posto BR');
INSERT INTO app_posto_tanque (`SEQ_DB`, `EMPRESA`, `FILIAL`, `LOCAL`, `INS_DH`, `INS_USUARIO_SEQ_DB`, `UPD_DH`, `UPD_USUARIO_SEQ_DB`, `SOURCE`, `ATIVO`, `DELETED`, `RO`, `ID`, `POSTO_SEQ_DB`, `CODIGO`, `DESCRICAO`, `COMBUSTIVEL_SEQ_DB`, `CAPACIDADE_CARGA`, `ESTOQUE_ALERTA`, `SALDO`) VALUES(1, 9999, 9999, 9999, '2019-10-10 17:29:41', 1, '2019-10-14 16:54:13', 1, NULL, 1, 0, 0, 1, 1, 'G01', 'Tanque Gasolina', 1, 5000, 500, 2500);
INSERT INTO app_posto_tanque (`SEQ_DB`, `EMPRESA`, `FILIAL`, `LOCAL`, `INS_DH`, `INS_USUARIO_SEQ_DB`, `UPD_DH`, `UPD_USUARIO_SEQ_DB`, `SOURCE`, `ATIVO`, `DELETED`, `RO`, `ID`, `POSTO_SEQ_DB`, `CODIGO`, `DESCRICAO`, `COMBUSTIVEL_SEQ_DB`, `CAPACIDADE_CARGA`, `ESTOQUE_ALERTA`, `SALDO`) VALUES(2, 9999, 9999, 9999, '2019-10-10 17:30:32', 1, '2019-10-14 16:54:13', 1, NULL, 1, 0, 0, 2, 1, 'E01', 'Tanque Etanol', 2, 10000, 1000, 750);
INSERT INTO app_posto_tanque (`SEQ_DB`, `EMPRESA`, `FILIAL`, `LOCAL`, `INS_DH`, `INS_USUARIO_SEQ_DB`, `UPD_DH`, `UPD_USUARIO_SEQ_DB`, `SOURCE`, `ATIVO`, `DELETED`, `RO`, `ID`, `POSTO_SEQ_DB`, `CODIGO`, `DESCRICAO`, `COMBUSTIVEL_SEQ_DB`, `CAPACIDADE_CARGA`, `ESTOQUE_ALERTA`, `SALDO`) VALUES(3, 9999, 9999, 9999, '2019-10-10 17:31:59', 1, '2019-10-14 16:54:13', 1, NULL, 1, 0, 0, 3, 1, 'D01', 'Tanque Diesel', 3, 5000, 500, 1800);
INSERT INTO app_posto_tanque (`SEQ_DB`, `EMPRESA`, `FILIAL`, `LOCAL`, `INS_DH`, `INS_USUARIO_SEQ_DB`, `UPD_DH`, `UPD_USUARIO_SEQ_DB`, `SOURCE`, `ATIVO`, `DELETED`, `RO`, `ID`, `POSTO_SEQ_DB`, `CODIGO`, `DESCRICAO`, `COMBUSTIVEL_SEQ_DB`, `CAPACIDADE_CARGA`, `ESTOQUE_ALERTA`, `SALDO`) VALUES(4, 9999, 9999, 9999, '2019-10-14 16:45:02', 1, '2019-10-14 16:54:13', NULL, NULL, 1, 0, 0, 4, 2, 'G01', 'Tanque Gasolina', 1, 25000, 5000, 17500);
INSERT INTO app_posto_bomba (`SEQ_DB`, `EMPRESA`, `FILIAL`, `LOCAL`, `INS_DH`, `INS_USUARIO_SEQ_DB`, `UPD_DH`, `UPD_USUARIO_SEQ_DB`, `SOURCE`, `ATIVO`, `DELETED`, `RO`, `ID`, `POSTO_TANQUE_SEQ_DB`, `CODIGO`, `DESCRICAO`, `CENTRO_CUSTO_SEQ_DB`) VALUES(1, 9999, 9999, 9999, '2019-10-10 17:34:12', 1, '2019-10-10 17:35:34', 1, NULL, 1, 0, 0, 1, 1, 'B-G01', 'Bomba de Gasolina 01', 1);
INSERT INTO app_posto_bomba (`SEQ_DB`, `EMPRESA`, `FILIAL`, `LOCAL`, `INS_DH`, `INS_USUARIO_SEQ_DB`, `UPD_DH`, `UPD_USUARIO_SEQ_DB`, `SOURCE`, `ATIVO`, `DELETED`, `RO`, `ID`, `POSTO_TANQUE_SEQ_DB`, `CODIGO`, `DESCRICAO`, `CENTRO_CUSTO_SEQ_DB`) VALUES(2, 9999, 9999, 9999, '2019-10-10 17:34:44', 1, '2019-10-10 17:34:44', NULL, NULL, 1, 0, 0, 2, 1, 'B-G02', 'Bomba de Gasolina 02', 1);
INSERT INTO app_posto_bomba (`SEQ_DB`, `EMPRESA`, `FILIAL`, `LOCAL`, `INS_DH`, `INS_USUARIO_SEQ_DB`, `UPD_DH`, `UPD_USUARIO_SEQ_DB`, `SOURCE`, `ATIVO`, `DELETED`, `RO`, `ID`, `POSTO_TANQUE_SEQ_DB`, `CODIGO`, `DESCRICAO`, `CENTRO_CUSTO_SEQ_DB`) VALUES(3, 9999, 9999, 9999, '2019-10-10 17:35:16', 1, '2019-10-10 17:35:16', NULL, NULL, 1, 0, 0, 3, 2, 'B-E01', 'Bomba de Etanol 01', 1);
INSERT INTO app_posto_bomba (`SEQ_DB`, `EMPRESA`, `FILIAL`, `LOCAL`, `INS_DH`, `INS_USUARIO_SEQ_DB`, `UPD_DH`, `UPD_USUARIO_SEQ_DB`, `SOURCE`, `ATIVO`, `DELETED`, `RO`, `ID`, `POSTO_TANQUE_SEQ_DB`, `CODIGO`, `DESCRICAO`, `CENTRO_CUSTO_SEQ_DB`) VALUES(4, 9999, 9999, 9999, '2019-10-10 17:36:04', 1, '2019-10-10 17:36:04', NULL, NULL, 1, 0, 0, 4, 3, 'B-D01', 'Bomba de Diesel 01', 1);

Configurações extras

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);

Introdução ao Painel Geral

O Painel Geral tem como objetivo exibir os dados relacionados de uma determinada Entidade, evitando ter que acessar diversos ambientes para encontrar informações específicas.

Pré-requesitos

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

  1. Tabela "nfs_core_general":
CREATE TABLE `nfs_core_general` (
  `SEQ_DB` bigint(20) NOT NULL AUTO_INCREMENT,
  `EMPRESA` int(11) DEFAULT NULL,
  `FILIAL` int(11) DEFAULT NULL,
  `LOCAL` int(11) DEFAULT NULL,
  `TABLENAME` varchar(100) NOT NULL,
  `CONFIG` text,
  `DESCRIPTION` varchar(300) DEFAULT NULL,
  `ENABLED` tinyint(1) DEFAULT '1',
  PRIMARY KEY (`SEQ_DB`),
  KEY `nfs_core_general` (`SEQ_DB`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

Onde:

  • TABLENAME: nome da Entidade;
  • CONFIG: configuração JSON para colunas da Entidade e Paineis FKDOWN.

Configuração Inicial

Não é ncessária nenhuma configuração inicial, porém, dependendo da quantidade de colunas a exibir, os paineis das tabelas relacionadas ficarão desconfigurados / incompletos (ocultando colunas, da esquerda para a direita).

Exemplo de Configuração para Entidade OS (TABLENAME = 'OS', CONFIG = json):

{
    "COLS": {
        "0":"SEQ_DB", "1":"CODIGO", "2":"DATA_ABERTURA", "3":"CLIENTE_FK", "4":"STATUS_OS_FK"
    },
    "FKDOWN": {
       "APONTAMENTO": {
           "0":"INI_DH", "1":"FIM_DH", "2":"AUXILIAR", "3":"TIPO_APONTAMENTO", "4":"PARADA"
       },
       "OS_SERVICO": {
           "0":"SERVICO", "1":"ATIVO"
       },
       "OS_TECNICO": {
           "0":"FUNCIONARIO", "2":"STATUS_OS", "3":"ATIVO"
       }
    }
}

Onde:

  • COLS: colunas que serão exibidas da Entidade (cabecalho da SO selecionada);
  • FKDOWN: cada painel geral com o nome da tabela relacionada e suas respectivas colunas a exibir

Observações

  • Os paineis gerados estão associados com a coluna FK_DOWN da Entidade; assim, se a tabela relacionada não existir em FK_DOWN, nenhuma informação será exibida;
  • As colunas especificadas para a Entidade, assim como para as tabelas relacionadas, precisam estar definidas para exibição (permissões e GRID > 0). Estas seguem as mesmas regras da Classe CRUD.

Para criar o link de acesso ao Painel Geral basta adicionar a opção no OPTIONS da nfs_core_ds_tabela. Se não existir configuração para a Entidade (p. ex. NOME = 'OS'), adicionar o json abaixo:

{
    "general": true
}

Se já existir uma configuração, adiciona-se a opção:

{
    "favorite_name": "OS Offline",
    "favorite": true,
    "general": true
}

Configuração Painel

O Painel com Query Builder tem a função de ter um maior perfomace e flexibilidade na hora de exibir informações importantes.

Primeira Configuração

Exemplo de um Painel do Padrão do SMARTOS:

{
  "QUERY_BUILDER": {
    "main_table": {
      "select": [
        "FUNCIONARIO.seq_db SEQ_DB",
        "FUNCIONARIO.nome NOME_FUNCIONARIO",
        "FUNCIONARIO.cracha CRACHA_FUNCIONARIO",
        "FUNCIONARIO.FOTO_SEQ_DB FOTO_SEQ_DB",
        "IF(FUNCIONARIO.FLAG_FERIAS = 1, 'Férias',NULL) as VACATION"
      ],
      "from": "FUNCIONARIO FUNCIONARIO",
      "where": [
        "FUNCIONARIO.QIG_DISPLAY = 1",
        "FUNCIONARIO.TIPO_FUNCIONARIO_SEQ_DB = 2"
      ],
      "group_result_by": "SEQ_DB",
      "order_by": [["FUNCIONARIO.NOME", "ASC"]]
    },
    "secondary_table": {
      "select": [
        "1 as funcionarioApontando",
        "BOLETIM.FIM_DH FIM_DH",
        "BOLETIM.FUNCIONARIO_SEQ_DB",
        "BOLETIM.SEQ_DB BOLETIM_SEQ_DB",
        ":NFS_NOW AGORA",
        "(select sec_to_time(sum(time_to_sec(TIMEDIFF(if(a.FIM_DH is null,:NFS_NOW,a.FIM_DH),a.INI_DH)))) from app_apontamento a inner join app_boletim b on b.SEQ_DB = a.SEQ_DB_DEVICE_MASTER_SEQ_DB where a.TIPO_APONTAMENTO = 1 and b.FIM_DH is null and a.SEQ_DB_DEVICE_MASTER_SEQ_DB = BOLETIM.SEQ_DB) as WORKING"
      ],
      "from": "BOLETIM",
      "left_join": [
        [
          "BOLETIM",
          "FUNCIONARIO",
          "FUNCIONARIO",
          "BOLETIM.FUNCIONARIO_SEQ_DB = FUNCIONARIO.SEQ_DB"
        ]
      ],
      "where": [
        "BOLETIM.FIM_DH is NULL",
        "FUNCIONARIO.SEQ_DB IN (:funcionario)"
      ],
      "group_result_by": "FUNCIONARIO_SEQ_DB"
    },
    "additional_tables": {
      "apontamento": {
        "select": [
          "( select if( ( ( p / total )* 100 ) is not null, ( p / total )* 100, 0 ) resultado from ( select sum( IMPRODUTIVA ) i, sum( PRODUTIVA ) p, ( sum( IMPRODUTIVA )+ sum( PRODUTIVA ) ) total from ( select ( select sum( ap.INI_FIM_DIFF_SEC ) from app_apontamento ap where ap.SEQ_DB = a.SEQ_DB and ap.TIPO_APONTAMENTO = 2 and ap.FUNCIONARIO_SEQ_DB = ap.AUXILIAR_SEQ_DB ) IMPRODUTIVA, ( select sum( ap.INI_FIM_DIFF_SEC ) from app_apontamento ap where ap.SEQ_DB = a.SEQ_DB and ap.TIPO_APONTAMENTO = 1 and ap.FUNCIONARIO_SEQ_DB = ap.AUXILIAR_SEQ_DB ) PRODUTIVA from app_apontamento a inner join app_boletim b on b.SEQ_DB = a.SEQ_DB_DEVICE_MASTER_SEQ_DB where b.FIM_DH is null AND  a.EMPRESA = :NFS_EMPRESA and a.FILIAL = :NFS_FILIAL and a.`LOCAL` = :NFS_LOCAL) as result) as retorno  ) horas",
          "APONTAMENTO.SEQ_DB SEQ_DB",
          "APONTAMENTO.INI_DH INI_DH_APONTAMENTO",
          "APONTAMENTO.CODIGO_OS CODIGO_OS",
          "PARADA.DESCRICAO PARADA",
          "SERVICO.DESCRICAO SERVICO",
          "SERVICO.QII_COLOR QII_COLOR_SERVICO",
          "PARADA.QII_COLOR QII_COLOR_PARADA",
          "APONTAMENTO.SEQ_DB_DEVICE_MASTER_SEQ_DB BOLETIM_SEQ_DB"
        ],
        "from": "APONTAMENTO APONTAMENTO",
        "left_join": [
          [
            "APONTAMENTO",
            "PARADA",
            "PARADA",
            "PARADA.SEQ_DB = APONTAMENTO.PARADA_SEQ_DB"
          ],
          [
            "APONTAMENTO",
            "SERVICO",
            "SERVICO",
            "SERVICO.SEQ_DB = APONTAMENTO.SERVICO_SEQ_DB"
          ]
        ],
        "where": ["PARADA.SEQ_DB IN (:parada)"],
        "group_result_by": "BOLETIM_SEQ_DB"
      },
      "apontamento_status_os": {
        "select": [
          "OS.CODIGO OS",
          "APONTAMENTO_STATUS_OS.SEQ_DB_DEVICE_MASTER_SEQ_DB BOLETIM_SEQ_DB"
        ],
        "from": "APONTAMENTO_STATUS_OS APONTAMENTO_STATUS_OS",
        "left_join": [
          [
            "APONTAMENTO_STATUS_OS",
            "OS",
            "OS",
            "OS.SEQ_DB = APONTAMENTO_STATUS_OS.OS_SEQ_DB"
          ]
        ],
        "group_result_by": "BOLETIM_SEQ_DB"
      }
    }
  }
}

Configuração Filtro

Configuração Filtro do Painel:

Inserir no campo FILTERS da tabela nfs_qig_panels, como no modelo abaixo:

{
  "funcionario": {
    "type": "Table",
    "options": {
      "label": "Encarregado",
      "required": false,
      "multiple": "multiple",
      "attr": {
        "data-filter": "funcionario.SEQ_DB",
        "data-where": "funcionario.flag_apontador = 1"
      }
    }
  }
}

ATENÇÃO O "data-filter" é obrigatório, caso não seja inserido será exibido no console do navegador e no Log uma mensagem de erro. ex: "Falha na configuração do painel : esperado elemento 'data-filter'." {.is-warning}

OBS:

Configurações OBRIGATÓRIAS

Group by Result

O group_by_result é um item obrigatória quando for configurar um painel com NFS Query Builder, sendo responsável por realizar a ligação entre as tabelas configuradas na additional_tables com secondary_table e secondary_table com main_table e deve estar em cada um das configurações como é mostrado abaixo.

ATENÇÃO Sem configurar o group group_by_result o painel não vai funcionar corretamente!!! {.is-warning}

DICA As consultas em NFS Query Builder usadas como exemplo estão na Primeira Configuração

  • main_table : O SEQ_DB da tabela que está sendo pesquisada, por exemplo, se a main table é FUNCIONARIO então o "group_by_result": "SEQ_DB". Para entender melhor use o "dump" : "result" para ver o seu retorno, exemplo:
{
  "615": Array[1][
    {
      "SEQ_DB": "615",
      "NOME_FUNCIONARIO": "ELTON BERNINI",
      "CRACHA_FUNCIONARIO": "PM0034",
      "FOTO_SEQ_DB": "3",
      "VACATION": null,
      "last_BOLETIM": "562",
      "seq_db": "615"
    }
  ]
}

Como foi possível observar veio agrupado pelo SEQ_DB do ELTON.

  • secondary_table: Nessa tabela geralmente é boletim/turno de uma máquina, funcionário, entre outros e também é agrupado por essa informação com isso fazendo a conexão com painel. Por exemplo se for boletim ele tem um funcionário_seq_db que deve ser passado no select e no "group_result_by": "FUNCIONARIO_SEQ_DB", que se realizarmos o "dump " : "result" irá aparecer:
{
  "615": Array[1][
    {
      "funcionarioApontando": "1",
      "FIM_DH": null,
      "FUNCIONARIO_SEQ_DB": "615",
      "BOLETIM_SEQ_DB": "562",
      "AGORA": "2018-06-01 14:20:32",
      "WORKING": null,
      "last_apontamento": "2923",
      "last_apontamento_status_os": "1413"
    }
  ]
}
  • additional_tables: Último nível do painel onde encontrar-se Apontamento, Apontamento de OS e etc. A configuração group_result_by será o BOLETIM_SEQ_DB que deve estar estar no select para ser comparado depois com o da seconday_table.

Podemos observar o resultado quando feito um "dump" : "result" que o BOLETIM_SEQ_DB é 562 igual da secondary_table no json acima e logo seus apontamento está no segundo bloco:

{
  "556": Array[1][
    {
      "horas": "0.0000",
      "SEQ_DB": "2909",
      "INI_DH_APONTAMENTO": "2018-02-20 16:56:34",
      "CODIGO_OS": null,
      "PARADA": "À disposição",
      "SERVICO": null,
      "QII_COLOR_SERVICO": null,
      "QII_COLOR_PARADA": "#f70000",
      "BOLETIM_SEQ_DB": "556",
      "APONTAMENTO_INI_DH": "2018-02-20 16:56:34"
    }
  ],
  "562": Array[1][
    {
      "horas": "0.0000",
      "SEQ_DB": "2923",
      "INI_DH_APONTAMENTO": "2018-04-06 13:20:37",
      "CODIGO_OS": null,
      "PARADA": "À disposição",
      "SERVICO": null,
      "QII_COLOR_SERVICO": null,
      "QII_COLOR_PARADA": "#f70000",
      "BOLETIM_SEQ_DB": "562",
      "APONTAMENTO_INI_DH": "2018-04-06 13:20:37"
    }
  ],
  "590": Array[1][
    {
      "horas": "0.0000",
      "SEQ_DB": "2980",
      "INI_DH_APONTAMENTO": "2018-04-06 13:09:43",
      "CODIGO_OS": null,
      "PARADA": null,
      "SERVICO": "REVISAO DE PRE ENTREGA",
      "QII_COLOR_SERVICO": null,
      "QII_COLOR_PARADA": null,
      "BOLETIM_SEQ_DB": "590",
      "APONTAMENTO_INI_DH": "2018-04-06 13:09:43"
    }
  ]
}

Legendas (Subtitles)

Configurar uma lengenda é um item obrigatório de certa forma porque se não o painel perde uma boa parte de seu objetivo de exisiter, então para configura-la na coluna SUBTITLES como pode ser observado no json a seguir:

{
  "SUBTITLES": {
    "secondary_table": {
      "DEFAULT_COLOR": "#FFFF00",
      "TEXT_COLOR": "#000000",
      "DESCRIPTION": "DESCRICAO"
    },
    "additional_tables": {
      "tabela": {
        "DESCRICAO_LEGENDA": {
          "TABLE_FIELD_COLOR": "COLUNA_COR",
          "DEFAULT_COLOR": "#b71c1c"
        }
      },
      "tabela2": {
        "DESCRICAO_LEGENDA": {
          "TABLE_FIELD_COLOR": "COLUNA_COR",
          "DEFAULT_COLOR": "#FFA500"
        }
      }
    }
  }
}

Secondary_table

Essa tabela geralmente representa o Boletim.

  • DEFAULT_COLOR: A cor padrão quando um boletim quando ainda não.
  • DESCRIPTION: Uma descrição para o boletim tem a data final não nula, geralmente descrito como Boletim Aberto, Turno Aberto e etc.

Additional_table

O Additional diferente da Secondary table pode ter uma ou mais tabelas como é nos cliente, quando precisa exibir Apontamento e Apontamento de OS, ambas podem refletir seus estados no painel.

  • tabela: Nome da tabela que deseja exibir uma legenda, por exemplo Apontamento;
  • DESCRICAO_LEGENDA: Nome da coluna que está no Query Builder com uma descrição, por exemplo, supondo que deve mostrar o Serviço, então na Query Builder terá uma coluna chamada SERVICO, nela deve ser retornado a código e/ou descrição do serviço.
  • TABLE_FIELD_COLOR: Na mesma query onde trago o SERVICO vou trazer a cor que deve ser exibida, entao vou criar mais uma coluna chamada QII_SERVICO_COR, que será um hexadecimal.

Modelo de Subtitle

{
  "SUBTITLES": {
    "additional_tables": {
      "apontamento": {
        "PARADA": {
          "TABLE_FIELD_COLOR": "QII_COLOR_PARADA",
          "DEFAULT_COLOR": "#b71c1c"
        },
        "SERVICO": {
          "TABLE_FIELD_COLOR": "",
          "DEFAULT_COLOR": "#1b5e20"
        }
      },
      "apontamento_status_os": {
        "OS": {
          "TABLE_FIELD_COLOR": "",
          "DEFAULT_COLOR": "#FFA500"
        }
      }
    },
    "secondary_table": {
      "DEFAULT_COLOR": "#FFFF00",
      "DESCRIPTION": "Boletim Aberto"
    }
  }
}

Legendas com imagem

Também é possível configurar uma imagem na legenda, mais usado nas tabelas de additional_tables. São necessários os seguintes passos:

  1. Na tabela alvo, por exemplo PARADA, é necessário na nfs_core_ds_tabela_campo adicionar a coluna SUBTITLE como IMG_MOBILE.

  2. No json de configuração do adicionar a chave TABLE_FIELD_IMG que será o nome da coluna que usado no Query Builder, ex.:

...
"additional_tables": {
      "apontamento": {
        "PARADA": {
          "TABLE_FIELD_IMG": "QII_IMG_PARADA",
          "TABLE_FIELD_COLOR": "QII_COLOR_PARADA",
          "DEFAULT_COLOR": "#b71c1c"
        }
      }
...
  1. Agora no Query Builder não é necessário passar o campo da imagem na coluna QII_IMG_PARADA, mas sim o SEQ_DB porque no painel ela é carregada por ajax usando esse valor, assim não adicionando um base64 que é extenso na query. Também é necessário colocar uma condição porque há casos de não ter imagem e usar a cor IF(PARADA.SUBTITLE is not null,PARADA.SEQ_DB,'') QII_IMG_PARADA . Então se tem imagem obtem o seq_db da parada se não passa um valor vazio, assim vai pegar a cor definida. Ex. de uma configuração additional_tables parte do Query Builder:
{
  "additional_tables": {
    "apontamento": {
      "select": [
        "(SELECT (SUM(IF(a.TIPO_APONTAMENTO = 1,TIME_TO_SEC(TIMEDIFF(IF(a.FIM_DH IS NULL,:NFS_NOW, a.FIM_DH), a.INI_DH)),0)) / (SUM(TIME_TO_SEC(TIMEDIFF(IF(a.FIM_DH IS NULL,:NFS_NOW, a.FIM_DH), a.INI_DH))) - SUM(IF(p.FLAG_REFEICAO = 1, TIME_TO_SEC(TIMEDIFF(IF(a.FIM_DH IS NULL,:NFS_NOW, a.FIM_DH), a.INI_DH)),0))) *100) AS PRODUTIVIDADE FROM app_apontamento a INNER JOIN app_boletim b ON (b.SEQ_DB = a.SEQ_DB_DEVICE_MASTER_SEQ_DB AND b.ATIVO = 1 AND b.DELETED = 0) INNER JOIN app_funcionario f ON (f.SEQ_DB = a.AUXILIAR_SEQ_DB AND f.ATIVO = 1 AND f.DELETED = 0) LEFT JOIN app_parada p ON (p.SEQ_DB = a.PARADA_SEQ_DB AND p.ATIVO = 1 AND p.DELETED = 0) WHERE b.FIM_DH IS NULL AND a.ATIVO = 1 AND a.DELETED = 0 AND a.EMPRESA = :NFS_EMPRESA AND a.FILIAL = :NFS_FILIAL AND a.`LOCAL` = :NFS_LOCAL) as horas",
        "APONTAMENTO.SEQ_DB SEQ_DB",
        "DATE_FORMAT(APONTAMENTO.INI_DH,'%d/%m/%Y %H:%i:%s')INI_DH_APONTAMENTO",
        "APONTAMENTO.CODIGO_OS OS",
        "PARADA.DESCRICAO PARADA",
        "SERVICO.DESCRICAO SERVICO",
        "SERVICO.QII_COLOR QII_COLOR_SERVICO",
        "PARADA.QII_COLOR QII_COLOR_PARADA",
        "IF(PARADA.SUBTITLE is not null,PARADA.SEQ_DB,'') QII_IMG_PARADA",
        "APONTAMENTO.SEQ_DB_DEVICE_MASTER_SEQ_DB BOLETIM_SEQ_DB"
      ],
      "from": "APONTAMENTO APONTAMENTO",
      "left_join": [
        [
          "APONTAMENTO",
          "PARADA",
          "PARADA",
          "PARADA.SEQ_DB = APONTAMENTO.PARADA_SEQ_DB"
        ],
        [
          "APONTAMENTO",
          "SERVICO",
          "SERVICO",
          "SERVICO.SEQ_DB = APONTAMENTO.SERVICO_SEQ_DB"
        ],
        [
          "APONTAMENTO",
          "BOLETIM",
          "BOLETIM",
          "APONTAMENTO.SEQ_DB_DEVICE_MASTER_SEQ_DB = BOLETIM.SEQ_DB"
        ],
        [
          "BOLETIM",
          "FUNCIONARIO",
          "FUNCIONARIO",
          "BOLETIM.FUNCIONARIO_SEQ_DB = FUNCIONARIO.SEQ_DB"
        ]
      ],
      "where": ["PARADA.SEQ_DB IN (:parada)"],
      "group_result_by": "BOLETIM_SEQ_DB"
    },
    "apontamento_status_os": {
      "select": [
        "OS.CODIGO OS",
        "APONTAMENTO_STATUS_OS.SEQ_DB_DEVICE_MASTER_SEQ_DB BOLETIM_SEQ_DB"
      ],
      "from": "APONTAMENTO_STATUS_OS APONTAMENTO_STATUS_OS",
      "left_join": [
        [
          "APONTAMENTO_STATUS_OS",
          "OS",
          "OS",
          "OS.SEQ_DB = APONTAMENTO_STATUS_OS.OS_SEQ_DB"
        ]
      ],
      "group_result_by": "BOLETIM_SEQ_DB"
    }
  }
}

Como podemos observar no select do apontamento é feita a verificação descrita acima.

Mensagem

Para aparecer a mensagem no nfs_qig_panels na coluna MESSAGE_TABLE deve estar configurada com a tabela de mensagem.

Localização

Para mostrar a Localização ter o tracking habilitado no entry point e MAIN_TABLE configurada.

Configuração para exibir o painel em modo Kanban

É possível definir em qual modo o painel será aberto inicialmente pela própria interface do NFS:

  1. Acesse Meus dados/Perfil de Usuário, note que em Opções do Ambiente será exibida a seguinte opção:

img_01_398.png

Por padrão essa opção virá desabilitada, ao mudar para sim uma alteração será enviada para o banco de dados setando na tabela nfs_acl_usuario_empresa no campo OPTIONS (objeto) um novo indice chamado "PANEL_DISPLAY_DEFAULT" com valor "S" ao mudar para não o indice é mantido mas o valor "N" será setado.

{
	"theme":"orange",
	"bg-img":"201",
	"PANEL_DISPLAY_DEFAULT":"N"  // <<<
}

Sim: Ao acessar o painel 2 (painelNew) ele será aberto no modo Kanban. Não: Ao acessar o painel 2 (painelNew) ele será aberto no modo Cards.

O painel 1 (painel old) não possui o modo Kanban.

Criar um Novo Boletim

Para habilar o botão para criar um novo boletim é necessário:

  • Usar o Field Action
  • Realizar a permissão a um grupo/usuário corretamente no Controle de Acesso.
  • Verificar se o campo está sendo exibido no detalhes, para isso tem que chegar o display da nfs_qig_details nas colunas MAIN_TABLE_OPTIONS, SECONDARY_TABLE_OPTIONS e ADDITIONAL_TABLES.
  • Verificar se o campo não está como DISABLED igual a 1 no nfs_core_ds_tabela_campo, se tiver tem que mudar para 0, porque do mesma forma que é possível no crud editar deve ser possível inserir.

Foi criado um entry point com valor 50 que é executado antes de carregar a tela, estão da para definir alguns valores como padrão. Temos os seguintes arrays:

$valores['secondaryConfig']

Tabela com os valores configurados na secondary table do detalhes, para acessar o valor MEDICAO_FIM e alterar para OBRIGATORIO = 1, os campos que existem são os mesmos da nfs_core_ds_tabela_campo.

$valores['secondaryConfig']['MEDICAO_FIM']->OBRIGATORIO = 1;

$valores['displayAdditional']

Um array com as tabelas additional, para ver o nome de cada additional utilize o código abaixo

foreach ($valores["additionals"] as $additionalName => $values) {
  NfsLogger::error(" Additional [{$additionalName}]", "DETALHES");
}

dentro de cada additional tem os campos da nfs_core_ds_tabela_campo.

Exemplo código em php:

/*
	Entry ponint 50
*/

/*
  Variaveis importantes

  Mostrar os valores
*/
NfsLogger::error(" SecondaryName: {$valores['secondaryName']} ", 'DETALHES');
NfsLogger::error(" SeqDB da MainTable: {$valores['seqDb']} ", 'DETALHES');
NfsLogger::error(" seqQii do Painel: {$valores['seqQii']} ", 'DETALHES');
NfsLogger::error(" seqPanel do Painel: {$valores['seqPanel']} ", 'DETALHES');
NfsLogger::error(" displayNameTables são as tabelas que são usadas na inserção de novos detalhes: {$valores['displayNameTables']} ", 'DETALHES');
NfsLogger::error(" displayAdditional modelo da aditional para quando for inserir um novo apontamento: {$valores['displayAdditional']} ", 'DETALHES');
NfsLogger::error(" secondaryConfig modelo da secondary: {$valores['secondaryConfig']} ", 'DETALHES');


// Exemplo para definir valores padrão quando inicia tela de detalhes
$valores['secondaryConfig']['INI_DH_TIPO']->VALOR_DISPLAY = 'Manual';
$valores['secondaryConfig']['INI_DH_TIPO']->VALOR = 'M';
$valores['secondaryConfig']['MEDICAO_INICIO']->VALOR = '10';
$valores['secondaryConfig']['MEDICAO_FIM']->OBRIGATORIO = 1;

No exemplo do código em php é salvo no log os valores de algumas variáveis e depois atualizado alguns valores como padrão.

Exemplo query entry point:

INSERT INTO nfs_homol_suzanosa_bobagro.nfs_entry_point (SEQ_DB, 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) VALUES(156, 'SYSTEM', NULL, NULL, NULL, 9999, 9999, 9999, NULL, 50, '$valores[''secondaryConfig''][''INI_DH_TIPO'']->VALOR_DISPLAY = ''Manual'';
$valores[''secondaryConfig''][''INI_DH_TIPO'']->VALOR = ''M'';
$valores[''secondaryConfig''][''MEDICAO_INICIO'']->VALOR = ''10'';
$valores[''secondaryConfig''][''MEDICAO_FIM'']->OBRIGATORIO = 1;', 0, 0, 1, 'PHP', NULL, NULL, '2018-09-28 11:33:26.000', '2019-04-26 11:17:09.000');

No exemplo de query entry point está definindo alguns valores como padrão.

Outras Configurações

Em construção

Data do Último Apontamento

No Painel com Query Builder é dada a opção caso haja mais de sempre exibir a data do último apontamento feito, basta na coluna DISPLAY_FIELDS da tabela nfs core painels em uma das opções de F1 até F6 na opção FIELD o valor LAST_DATE. Exemplo:

"F3": [
    {
        "FIELD": "LAST_DATE",
        "LABEL": "",
        "TOOLTIP": ""
    }
],

Ele vai funcionar apenas para todas as tabelas dentro do additional_tables.

Scroll automático

Atenção O scroll automático não está implementado quando o painel está em fullscreen pelo portlet (ícones na tela) {.is-warning}

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

Legenda no Card do QII

Para habilitar a legenda no CARD é necessáro habilitar/adicionar o seguinte parâmetro na nfs_core_par_parametros:

BORDER_QII_OLD_PANEL

Com valor igual a 1 ou true.

Essa funcionalidade usa a cor da legenda e adiciona na borda do card.

border_card_qi.png

Painel Completo Por Entry Point

Uma terceira opção para criar um Painel é ser feito totalmente por entry point, nesse tipo o Dev tem total controle do que está sendo criado, como:

  • Ordenação na forma que achar melhor
  • Montar os QII
  • Poder otimizar as consultas do painel

Ponto importante nessa configuração é necessário no options ter o seguinte atributo:

{
	"panel-full-entry-point": true
}

Se não tiver o panel-full-entry-point ele é false por padrão.

Vou passar um exemplo comentado que foi feito pela Paloma e depois modifiquei alguns pontos para atender todas as condições do painel.

// dados que vem do filtro
$decode = json_decode($this->filters, true);
$equipamento = [];
$tipoEquipamento = [];
$classeOperacional = [];
$atividade = [];
$fazenda = [];
$setor = [];
$frente = [];

// funcão que vai para o core, mas é para monstar os itens do card/qii
function buildItemCard(string $pos, string $tooltip  ='', $field, string $label = ''){
    $item =[];
    $item[$pos]['TOOLTIP'] =$tooltip;
    $item[$pos]['FIELD'] =$field;
    $item[$pos]['LABEL'] =$label;
    return $item;
}

// função para montar os indicadores
function buildIndicatorsCard(string $pos, string $tooltip  ='', $field=0 , string $color ='', string $icon = 'fa fa-circle'){
    $item =[];
    $item[$pos]['TOOLTIP'] =$tooltip;
    $item[$pos]['FIELD'] =strval($field);
    $item[$pos]['COLOR'] =$color;
    $item[$pos]['ICON'] =$icon;
    return $item;
}

// função para montar as legendas dos qii
function buildSubtitle(string $color = 'gray', string $textColor = '#fff', string $subtitle = 'sem subtitle', $id = "sem_id", $panel,$total = 1,array $img = []){
    $sub =[];
    $sub['color'] = $color;
    $sub['textColor'] = $textColor;
    $sub['subtitle'] = $subtitle;
    $sub['id'] = $id;
    $sub['panel_seq_db'] = $panel;
    $sub['total'] = $total;
    if (!empty($img)){
        $sub['img']['seqDB']= $img['SEQ_DB'] ?? '';
        $sub['img']['tableName']= $img['column'] ?? '';
    }
    return $sub;
}

// função para montar as legendas que fazem parte do filtro
function buildSubtitleCard(string $color = 'gray',string $subtitle = 'VAZIO', $id = "sem_id", int $panel ){
    $sub =[];
    $sub['color'] = $color;
    $sub['subtitle'] = $subtitle;
    $sub['id'] = Utils::transformStringToKebabPattern($id);
    $sub['panel_seq_db'] = $panel;
    return $sub;
}


// funcão para criar uma nova legenda ou contar se já existe
function addSubtitle(array $subtitles, array $subs, $id){
    if(isset($subtitles) && !empty($subtitles[$id])){
        $subtitles[$id]['total'] = $subtitles[$id]['total'] +  1;
    } else {
        $subtitles[$id] = buildSubtitle($subs['color'],'#fff', $subs['subtitle'], $subs['id'], $subs['panel_seq_db']);
    }
    return $subtitles;
}

// funcão para obter os valores do filtro
function getValueOfFilter(string $key, array $filters): array {
    $result = [];
    if (isset($filters[$key])){
        foreach ($filters[$key]["value"] as $val){
            $result[] = $val;
        }
    }
    return $result;
}

// se tiver filtro sendo usado deve mudar para true
$hasFilter = false;

// condição que cria um flag para que se tiver algum filtros
// sendo utilizado marcar ela como true
if (!empty($decode)){
    $equipamento = getValueOfFilter('EQUIPAMENTO', $decode);
    $tipoEquipamento = getValueOfFilter('TIPO_EQUIPAMENTO', $decode);
    $classeOperacional = getValueOfFilter('CLASSE_OPERACIONAL', $decode);
    $atividade = getValueOfFilter('ATIVIDADE', $decode);
    $fazenda = getValueOfFilter('FAZENDA', $decode);
    $setor= getValueOfFilter('SETOR', $decode);
    $frente = getValueOfFilter('FRENTE', $decode);
    if ( !empty($equipamento) ||
        !empty($tipoEquipamento)  ||
        !empty($classeOperacional)  ||
        !empty($atividade) ||
        !empty($fazenda) ||
        !empty($setor) ||
        !empty($frente) )
    {
        $hasFilter = true;
    }
}

$subtitles = [];
// subtituir pelo valor do painel que está na nfs_qig_panels
$panelSeqDb = 30; 
// subtituir pelo valor do painel que está na nfs_qig_details
$detailSeqDb = 2; 

// busca pelo os equipamentos com boletim aberto
$eqp = Dao::table('EQUIPAMENTO','e')
	->select(['distinct e.SEQ_DB', 'e.CODIGO','e.FOTO_SEQ_DB','e.FLAG_SATELITAL'])
	->leftJoin('e','BOLETIM_MAQUINA','b','e.SEQ_DB = b.EQUIPAMENTO_SEQ_DB')
	->leftJoin('b','APONTAMENTO_MAQUINA','a','b.SEQ_DB = a.SEQ_DB_DEVICE_MASTER_SEQ_DB')
	->leftJoin('b','FRENTE','f','f.SEQ_DB = b.FRENTE_SEQ_DB')
	->where(['e.QIG_DISPLAY'=> 1,'b.ATIVO'=> 1])
	->whereIsNull('b.FIM_DH')
	->whereIn('e.SEQ_DB', $equipamento)
	->whereIn('e.CLASSE_OPERACIONAL_SEQ_DB', $classeOperacional)
	->whereIn('e.TIPO_EQUIPAMENTO_SEQ_DB', $tipoEquipamento)
	->whereIn('a.ATIVIDADE_SEQ_DB', $atividade)
	->whereIn('a.SETOR_SEQ_DB', $setor)
	->whereIn('a.FAZENDA_SEQ_DB', $fazenda)
	->whereIn('b.FRENTE_SEQ_DB', $frente)
	->orderBy('f.DESCRICAO', 'ASC')
	->orderBy('e.CODIGO', 'ASC')
	->get();

// Se tiver filtro não busca pelos boletins fechados que não estão abertos
if (!$hasFilter){
    $openBulletins =  array_column($eqp, 'SEQ_DB');
    $closedBulletins = Dao::table('EQUIPAMENTO','e')
        ->select(['distinct e.SEQ_DB', 'e.CODIGO','e.FOTO_SEQ_DB','e.FLAG_SATELITAL'])
        ->where(['e.QIG_DISPLAY'=> 1,'e.ATIVO'=> 1])
        ->whereNotIn('e.SEQ_DB', $openBulletins)
        ->orderBy('e.CODIGO', 'ASC')
        ->get();

    $eqp = array_merge($eqp, $closedBulletins);
}

$newPanelData = [];
$newPanelData['QII'] = [];
$seqDbsEqps = array_column($eqp, 'SEQ_DB');
$boletins = Dao::table('BOLETIM_MAQUINA')
->select([
    "INI_DH DATA_ABERTURA_BOLETIM",
    "FIM_DH",
    "EQUIPAMENTO_SEQ_DB",
    "SEQ_DB",
	"FRENTE_SEQ_DB"
])
->whereIn('EQUIPAMENTO_SEQ_DB', $seqDbsEqps)
->whereIn('FRENTE_SEQ_DB', $frente)
->whereIsNull('FIM_DH')
->get();
$listaBols = array_column($boletins, null, 'SEQ_DB');
$lastsBols = array_column($boletins, 'SEQ_DB');
$bol_eqp = array_column($boletins, null, 'EQUIPAMENTO_SEQ_DB');
$lastApts = Dao::table('APONTAMENTO_MAQUINA')
->select([
    'SEQ_DB',
    'SEQ_DB_DEVICE_MASTER_SEQ_DB',
    'TIPO_ATIVIDADE',
    'INI_DH',
    'EQUIPAMENTO_SEQ_DB',
    'OPERADOR_SEQ_DB',
    'ATIVIDADE_SEQ_DB',
	'TIPO_ATIVIDADE',
	'SETOR_SEQ_DB',
	'FAZENDA_SEQ_DB',
	'FRENTE_SEQ_DB',
	'OS_SEQ_DB'
])
->whereIn('SEQ_DB_DEVICE_MASTER_SEQ_DB', $lastsBols)
->whereIn('EQUIPAMENTO_SEQ_DB', $equipamento)
->whereIn('ATIVIDADE_SEQ_DB', $atividade)
->whereIn('SETOR_SEQ_DB', $setor)
->whereIn('FAZENDA_SEQ_DB', $fazenda)
->whereIsNull('FIM_DH')
->get();

$fil_frente = array_unique(array_column($bol_eqp, 'FRENTE_SEQ_DB'), SORT_REGULAR);
$apt_eqp = array_column($lastApts, null, 'EQUIPAMENTO_SEQ_DB');
$fil_atividade = array_unique(array_column($lastApts, 'ATIVIDADE_SEQ_DB'), SORT_REGULAR);
$fil_setor = array_unique(array_column($lastApts, 'SETOR_SEQ_DB'), SORT_REGULAR);
$fil_fazenda = array_unique(array_column($lastApts, 'FAZENDA_SEQ_DB'), SORT_REGULAR);
$fil_os = array_unique(array_column($lastApts, 'OS_SEQ_DB'), SORT_REGULAR);
$fil_seqApt = array_column($lastApts,'SEQ_DB');
foreach($listaBols as $key => $bol){
    $ltem_apt = 'N';
    foreach($apt_eqp as $key1 => $apt){
        if($key == $apt['SEQ_DB_DEVICE_MASTER_SEQ_DB'] && $key1 == $bol['EQUIPAMENTO_SEQ_DB']){
            $ltem_apt = 'S';
        }else{
            $ltem_apt = 'N';
        }
    }
    $listaBols[$key]['FLAG_TEM_APT'] = $ltem_apt;
}
$atividade_list = Dao::table('ATIVIDADE')->select(['SEQ_DB','CODIGO','DESCRICAO','QII_COLOR','FLAG_PRODUTIVA'])->all()->get();
$listTasks = array_column($atividade_list, null, 'SEQ_DB');
$setor_list = Dao::table('SETOR')->select(['SEQ_DB','CODIGO','DESCRICAO'])->whereIn('SEQ_DB', $fil_setor)->all()->get();
$lista_setores = array_column($setor_list,null,'SEQ_DB');
$fazenda_list = Dao::table('FAZENDA')->select(['SEQ_DB','CODIGO','DESCRICAO'])->whereIn('SEQ_DB', $fil_fazenda)->all()->get();
$lista_fazendas = array_column($fazenda_list,null,'SEQ_DB');
$frente_list = Dao::table('FRENTE')->select(['SEQ_DB','CODIGO','DESCRICAO'])->whereIn('SEQ_DB', $fil_frente)->all()->get();
$lista_frentes = array_column($frente_list,null,'SEQ_DB');
$osList = Dao::table('OS')->select(['SEQ_DB','CODIGO','DESCRICAO'])->whereIn('SEQ_DB', $fil_os)->all()->get();
$lista_OSs = array_column($osList,null,'SEQ_DB');
$aptTalhao_list = Dao::table('APONTAMENTO_MAQUINA_TALHAO','a')->select(['a.SEQ_DB','a.APONTAMENTO_MAQUINA_SEQ_DB',"a.TALHAO_SEQ_DB"])
	->whereIn('a.APONTAMENTO_MAQUINA_SEQ_DB', $fil_seqApt)->get();
$fil_aptsTalhoes = array_unique(array_column($aptTalhao_list, 'TALHAO_SEQ_DB'), SORT_REGULAR);
$talhao_list = Dao::table('TALHAO')->select(['SEQ_DB','CODIGO','DESCRICAO'])->whereIn('SEQ_DB', $fil_aptsTalhoes)->all()->get();
$lista_Talhoes = array_column($talhao_list,null,'SEQ_DB');
$antenaDados_list = Dao::table('EQP_ANTENA_DADOS')->select(['SEQ_DB', 'EQUIPAMENTO_SEQ_DB', 'MAX(DATA_TRANSMISSAO) DATA_TRANSMISSAO','TIPO_COMUNICACAO'])->groupBy('EQUIPAMENTO_SEQ_DB')->get();
$lista_antenaDados = array_column($antenaDados_list,null,'EQUIPAMENTO_SEQ_DB');
$date = DateUtils::getDate(0,'BR');
$apts = Dao::table('APONTAMENTO_MAQUINA')
->select([
    'SEQ_DB',
    'SEQ_DB_DEVICE_MASTER_SEQ_DB',
    'TIPO_ATIVIDADE',
    'INI_DH',
    'EQUIPAMENTO_SEQ_DB',
    'OPERADOR_SEQ_DB',
    'ATIVIDADE_SEQ_DB',
	'TIPO_ATIVIDADE',
	'SETOR_SEQ_DB',
	'FAZENDA_SEQ_DB',
	'FRENTE_SEQ_DB',
	'OS_SEQ_DB'
])
->whereIn('SEQ_DB_DEVICE_MASTER_SEQ_DB', $lastsBols)
->whereIn('EQUIPAMENTO_SEQ_DB', $equipamento)
->whereIn('ATIVIDADE_SEQ_DB', $atividade)
->whereIn('SETOR_SEQ_DB', $setor)
->whereIn('FAZENDA_SEQ_DB', $fazenda)
->whereIn('FRENTE_SEQ_DB', $frente)
->whereDate('INI_DH', $date)
->get();
$aux = [];
foreach($apts as $apt){
    if(!array_key_exists($apt['EQUIPAMENTO_SEQ_DB'],$aux)){
        $aux[$apt['EQUIPAMENTO_SEQ_DB']]=[
            'WORKING' => [],
            'NOT_WORKING' => []
        ];
    }
	if($apt['TIPO_ATIVIDADE'] == 1){
		$aux[$apt['EQUIPAMENTO_SEQ_DB']]['WORKING'][] = $apt['ATIVIDADE_SEQ_DB'];
	}elseif($apt['TIPO_ATIVIDADE'] == 0){
		$aux[$apt['EQUIPAMENTO_SEQ_DB']]['NOT_WORKING'][] = $apt['ATIVIDADE_SEQ_DB'];
	}
}

// busca os ultimos apontamentos
$lastAptsByBol = [];
foreach($lastApts as $apt) {
    $seqBol = $apt['SEQ_DB_DEVICE_MASTER_SEQ_DB'];
    if (empty($lastAptsByBol[$seqBol])){
        $lastAptsByBol[$seqBol] = $apt;
    } else {
        $lastDate = DateTime::createFromFormat('Y-m-d H:i:s', $lastAptsByBol[$seqBol]['INI_DH']);
        $currDate = DateTime::createFromFormat('Y-m-d H:i:s', $apt['INI_DH']);
        if ($lastDate->getTimestamp() < $currDate->getTimestamp()){
            $lastAptsByBol[$seqBol] = null;
            $lastAptsByBol[$seqBol] = $apt;
        }
    }
}
$lastElement = 0;
$count = 0;
$totAberto = 0;
$totFechado = 0;

// Loop importante onde vai montar o QII
foreach ($eqp as $e) {
    $qii = [];
    $working = 0;
    $not_working = 0;
    $apt = $apt_eqp[$e['SEQ_DB']] ?? null;
	$bol = $bol_eqp[$e['SEQ_DB']] ?? null;

    // ponto importante onde o qii devem ter os campos abaixo
	$qii['QII_SEQ_DB'] = $e['SEQ_DB'];
	$qii['PANEL_SEQ_DB'] = $panelSeqDb;
	$qii['DETAILS_SEQ_DB'] = $detailSeqDb;
	$qii['ORDER'] = $count++;
	$qii['DISPLAY'] = true;
	$qii['MAIN_TABLE'] = 'EQUIPAMENTO'; // Aqui podemos ter a main table, só na configuração do banco que não
	$qii['MESSAGE_TABLE'] = 'APONTAMENTO_MAQUINA_MENSAGEM';

    // Obter o total de mensagens não lidas, os parametros são:
    // seqDb da entidade do qii, logo se for um painel por equipamento, vai ser o seq_Db dele
    // Nome da tabela que é vai ser usando para obter os dados de mensagem
    // Nome da tabela que recebe mensagens do mobile
	$qii['MESSAGE'] = $this->panelFactory->getTotalMessage($e['SEQ_DB'], 'EQUIPAMENTO', 'APONTAMENTO_MAQUINA_MENSAGEM');
	$qii['MAPS'] = true; // Lembrando que tem que ter a configuração de tracking no entry point e lat/long no Equipamento ou dono do painel
	$qii['UPDATED'] = false;
	$qii["COMMSIGNAL"]=  "signal-offline";
	$qii += buildItemCard('F1', 'Equipamento', $e['CODIGO']);

    // Importante, sempre trazer a imagem numa consulta isolado ao invés de trazer na consulta principal
	$image = ImageService::getThumbnailFromUpload($e['FOTO_SEQ_DB']); 
	if (isset($image) && !empty($image)) {
		$qii['F7'] = $image; 
	} else {
		$qii["F7"] = "assets/img/painel/colhedora.png";
	}
    
    // parte importante preenche alguns dados dentro do QII somente se tiver apontamento com boletim aberto
	if(!empty($apt)){

        // Aqui dentro vcs podem colocar a lógica necessário para montar os dados do qii
        // Tente sempre tratar os valores nulos ou chaves que não existem
        $ati = isset($apt['ATIVIDADE_SEQ_DB']) && array_key_exists($apt['ATIVIDADE_SEQ_DB'], $listTasks) ? $listTasks[$apt['ATIVIDADE_SEQ_DB']] : null;
        $set = isset($apt['SETOR_SEQ_DB']) ? $lista_setores[$apt['SETOR_SEQ_DB']] : null;
        $faz = isset($apt['FAZENDA_SEQ_DB']) ? $lista_fazendas[$apt['FAZENDA_SEQ_DB']] : null;
        $fre = isset($apt['FRENTE_SEQ_DB']) ? $lista_frentes[$bol['FRENTE_SEQ_DB']] : null;
        $separador = null;
        $satelitalComunication = isset($apt['EQUIPAMENTO_SEQ_DB']) ? ($e['FLAG_SATELITAL'] == true ? $lista_antenaDados[$apt['EQUIPAMENTO_SEQ_DB']] : null) : null;
        $dateApt = isset($apt['INI_DH']) ? DateUtils::formatFromTable($apt['INI_DH'],'d/m/Y H:i:s') : null;
        $talhoes = null;
        foreach($aptTalhao_list as $aptTalhao){
            if(isset($apt['SEQ_DB']) && $apt['SEQ_DB'] == $aptTalhao['APONTAMENTO_MAQUINA_SEQ_DB']){
                $talhaoApontado = $lista_Talhoes[$aptTalhao['TALHAO_SEQ_DB']] ?? null;
                if(!empty($talhaoApontado)){
                    $talhoes = !empty($talhoes) ? $talhoes.'::'.$talhaoApontado['CODIGO'] : $talhaoApontado['CODIGO'];
                }
            }
        }
        $atividade = $ati ? $ati['CODIGO'].'::'.$ati['DESCRICAO'] : "";
        $frente = $fre ? $fre['DESCRICAO'] : "";
        $setor = $set ?  $set['CODIGO'].'::'.$set['DESCRICAO'] : "";
        $fazenda = $faz ?  $faz['CODIGO'].'::'.$faz['DESCRICAO'] : "";
        $separador = !empty($set) && !empty($faz) ? ' | ' : "";
        
        // buildItemCard é bastente usando para montar os valores do F1, F2, ...F7
        $qii += buildItemCard('F2', 'Frente de Trabalho', $frente, 'Módulo: ');
        $qii += buildItemCard('F3', 'Projeto', $fazenda.$separador.$setor, 'Projeto'.' | '.'Setor:'); 
        $qii += buildItemCard('F4', 'Up', $talhoes, 'Up: ');
        $qii += buildItemCard('F5', 'Operação', $atividade, 'Operação: ');
        $qii += buildItemCard('F6', 'Data do último apontamento', $dateApt, 'Apontado em:');
        if(!empty($satelitalComunication)){
            $qii['TIME_LAST_NOTE'] = $satelitalComunication['DATA_TRANSMISSAO'];
        }
		$subtitleID = $ati['DESCRICAO'] ?? "vazio";
        $typeTask = $apt['TIPO_ATIVIDADE'] ?? 3;
		if($typeTask == 1){
			$color = $ati['QII_COLOR'] ?? ($ati['FLAG_PRODUTIVA'] == true ? '#05ed38' : '#f50505');
		}elseif($typeTask == 0){
			$color = $ati['QII_COLOR'] ?? ($ati['FLAG_PRODUTIVA'] == false ? '#f50505' : '#05ed38');
		}
		$working =0; //isset($aux[$e['SEQ_DB']]) ? count($aux[$e['SEQ_DB']]['WORKING']) : 0;
		$qii += buildIndicatorsCard('F8', 'Produtiva', $working, 'green');
		$not_working = 0;
		$not_working = 1;//isset($aux[$e['SEQ_DB']]) ? count($aux[$e['SEQ_DB']]['NOT_WORKING']) : 0;
		$qii += buildIndicatorsCard('F9', 'Improdutiva', $not_working, 'red');
		$total = $working+$not_working;
		$qii += buildIndicatorsCard('F11', 'Totalizador', $total, 'blue');
	}else{
        // Se tem boletim e não tem apontamento o turno do equipamento ou outro objeto está aberto
        // Se não ele está fechado 
		if(!empty($bol)){
			$atividade = 'Boletim Aberto';
			$subtitleID = 'Boletim Aberto';
			$color = '#C09B28';
			$totAberto++;
		}else{
			$atividade = 'Boletim Fechado';
			$subtitleID = 'Boletim Fechado';
			$color = '#3C3C3C';
			$totFechado++;
		}
	}

    // Importante parte para montar as legendas do QII e do QIG
    $qii['SUBTITLE'] = buildSubtitleCard($color, $atividade, $subtitleID, $panelSeqDb);
	$subtitles = addSubtitle($subtitles, $qii['SUBTITLE'],$subtitleID);
	$newPanelData['QII'][$e['SEQ_DB']] = $qii;
    $lastElement = $e['SEQ_DB'];
}

if (!empty($eqp)){

    $aptFull = DAO::table('APONTAMENTO_MAQUINA', 'a')
	->select(['SUM(if(a.FIM_DH is not null, a.INI_FIM_DIFF_SEC, (NOW() - a.INI_DH))) TOTAL',
		'SUM(if(atv.FLAG_MANUTENCAO = 1,if(a.FIM_DH is not null, a.INI_FIM_DIFF_SEC, (NOW() - a.INI_DH)),0)) TOTAL_MANUTENCAO',
		'SUM(if(atv.FLAG_PRODUTIVA = 1,if(a.FIM_DH is not null, a.INI_FIM_DIFF_SEC, (NOW() - a.INI_DH)),0)) TOTAL_PRODUTIVA',
		'SUM(if(atv.FLAG_PRODUTIVA = 0 AND ga.FLAG_PARADA_PROCESSO = 1, if(a.INI_FIM_DIFF_SEC IS NULL, TIMESTAMPDIFF(SECOND,a.INI_DH, NOW()), a.INI_FIM_DIFF_SEC),0))TOTAL_PROCESSO',
		'SUM(if(atv.FLAG_PRODUTIVA = 0 AND ga.FLAG_PARADA_OPERACIONAL = 1, if(a.INI_FIM_DIFF_SEC IS NULL, TIMESTAMPDIFF(SECOND,a.INI_DH, NOW()), a.INI_FIM_DIFF_SEC),0))TOTAL_OPERACIONAL'
	])
	->innerJoin('a','BOLETIM_MAQUINA','b','a.SEQ_DB_DEVICE_MASTER_SEQ_DB = b.SEQ_DB')
	->innerJoin('a', 'EQUIPAMENTO', 'e', 'a.EQUIPAMENTO_SEQ_DB = e.SEQ_DB')
	->innerJoin('a', 'ATIVIDADE', 'atv', 'a.ATIVIDADE_SEQ_DB = atv.SEQ_DB')
	->innerJoin('a', 'GRUPO_ATIVIDADE', 'ga', 'a.GRUPO_ATIVIDADE_SEQ_DB = ga.SEQ_DB')
	->where(['atv.FLAG_CALCULO_INDICADOR' => 1,'e.QIG_DISPLAY'=> 1,'b.ATIVO'=> 1,'a.ATIVO'=> 1])
	->whereIsNull('b.FIM_DH')->get();

$totalDiff = 0;
$totalManut = 0;
$totalProd = 0;
$totalProcesso = 0;
$totalOperacional = 0;
foreach($aptFull as $aptTotal){
	$totalDiff += $aptTotal['TOTAL'];
	$totalManut += $aptTotal['TOTAL_MANUTENCAO'];
	$totalProd += $aptTotal['TOTAL_PRODUTIVA'];
	$totalProcesso += $aptTotal['TOTAL_PROCESSO'];
	$totalOperacional += $aptTotal['TOTAL_OPERACIONAL'];
}
$dm = ($totalDiff - $totalProcesso) > 0 ? ((($totalDiff - $totalProcesso) - $totalManut)/($totalDiff - $totalProcesso))*100 : 0;
$eo = ($totalDiff - $totalProcesso - $totalManut) > 0 ? (($totalDiff - $totalProcesso - $totalManut - $totalOperacional) / ($totalDiff - $totalProcesso - $totalManut))*100 : 0;
$tu = ($totalDiff - $totalProcesso) > 0 ? (($totalDiff - $totalProcesso - $totalManut - $totalOperacional) / ($totalDiff - $totalProcesso))*100 : 0;
$newPanelData['QII'][$lastElement]['INDICATORS_OPTIONS']['DISPONIBILIDADE_MECANICA'] = $dm; 
$newPanelData['QII'][$lastElement]['INDICATORS_OPTIONS']['EFICIENCIA_OPERACIONAL'] = $eo; 
$newPanelData['QII'][$lastElement]['INDICATORS_OPTIONS']['TAXA_UTILIZACAO'] = $tu;
}
$newPanelData['MAIN_TABLE'] = 'EQUIPAMENTO';
$newPanelData['MESSAGE_TABLE'] = 'APONTAMENTO_MAQUINA_MENSAGEM';
$newPanelData['SUBTITLE'] = $subtitles;
$this->outputValues = $newPanelData;

📊 Painel de Checkout - Documentação Completa

1. Visão Geral

Módulo para monitoramento integrado de produção com:

  • Comparativo entre previsto × realizado
  • Cálculo automático de PPC (Planejamento e Programação de Controle)
  • Visualização de ocorrências e métricas operacionais
  • Filtros avançados por período, equipe e hierarquia

1.1 Visão Geral - Explicação Visual do Painel pelo figma:

🔗 Clique para visualizar o painel interativo no Figma

1.2 Calculos - Com Exemplos Práticos

1. PPC Semanal

Como funciona:
Conta todas as tarefas válidas de todas EQUIPES (por encarregados) durante o período de dias definido no filtro.
Verifica quantas dessas tarefas estão 100% concluídas (PPC_TAREFA == 100).

Fórmula:

  • PPC Geral Semana = (Nº de tarefas 100% concluídas / Total de tarefas válidas) × 100 Exemplo:
  • Período do filtro: 01/01/2025 até 07/01/2025
  • Total de tarefas válidas: 200
  • Tarefas 100% concluídas: 150
  • Cálculo: (150/200) × 100 = 75%

2. PPC por Encarregado

Como funciona:
Conta todas as tarefas válidas de uma EQUIPE baseada no ENCARREGADO durante o período do filtro.

Fórmula:

  • PPC Encarregado = (Nº de tarefas 100% concluídas pela equipe / Total de tarefas válidas da equipe) × 100

Exemplo:

  • Período do filtro: 01/01/2025 até 07/01/2025
  • Encarregado: João Silva
  • Total de tarefas válidas: 20
  • Tarefas 100% concluídas: 10
  • Cálculo: (10/20) × 100 = 50%

3. PPC por Tarefa Individual

Como funciona:
Avaliação binária (100% ou 0%) de cada tarefa durante o período do filtro.

Critério:

  • Se produção_atingida ≥ meta_prevista → 100% Senão → 0%

Exemplo:

  • Tarefa: Instalação elétrica
  • Meta prevista: 100 unidades
  • Produção atingida: 98 unidades
  • Resultado: 0% (não atingiu 100%)

4. % Produção do Dia

Como funciona:
Cálculo diário para um encarregado específico.

Fórmula:

  • % Produção Dia = (Nº tarefas 100% concluídas no dia / Total tarefas válidas no dia) × 100

Exemplo:

  • Data: 03/01/2025
  • Encarregado: Maria Souza
  • Total tarefas válidas: 8
  • Tarefas concluídas: 6
  • Cálculo: (6/8) × 100 = 75%

2. Configuração Inicial

2.1 Registro para ativação do painel no Menu

INSERT INTO nfs_core_menu 
(SEQ_DB, EMPRESA, FILIAL, LOCAL, DESCRICAO, TYPE, ATIVO, FATHER, MENUORDER, URL, FILTER, ICON) 
VALUES 
(
    10070, 
    9999, 
    9999, 
    9999, 
    'Painel de Checkout', 
    'LINK', 
    1, 
    9999, 
    1, 
    'checkoutPanel', 
    NULL, 
    'fa fa-tachometer'
);

3. Customização de Estrutura (Nos casos em que as tabelas app não existem no cliente que quer configurar o painel)

Contexto

O painel pressupõe o uso de tabelas padronizadas do sistema (
app_efetivo_funcionario app_mo_oper app_oper_grupo_metrica app_efetivo_engenheiro app_folha_tarefa app_unidade_medida app_mo_apt_motivo app_tarefa app_frente app_motivo app_folha_tarefa_n_frente app_mo_boletim ).

Entretanto, para clientes que utilizam estrutura diferente dessa, é possível adaptar o módulo por meio de um parâmetro JSON configurável via entry point.

Registro de Tabelas e Colunas Ausentes ⚠️

O painel de checkout inclui um mecanismo automático de log que registra nos logs quais tabelas ou colunas necessárias não existem na estrutura do cliente.

3.1 SQL para ativação das customizações por parâmetros se necessário'

INSERT INTO nfs_entry_point (
    SEQ_DB,
    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 (
    765, -- [EXEMPLO] próximo SEQ_DB disponível na tabela
    'PANEL',
    'CHECKOUT_PANEL_CUSTOM',
    NULL,
    NULL,
    9999,
    9999,
    9999,
    '',
    NULL,
    '{
      "panel_remap": {
      },
      "customize": {
      }
    }'-- [EXEMPLO] Configuração do JSON de customizações e remap
    ,
    1,
    NULL,
    1,
    'JSON',
    NULL,
    NULL,
    '2022-09-23 22:46:08',
    '2025-07-08 01:49:52',
    'simova.admin@simova.com.br',
    'exemplo@000.00.000.000'-- [EXEMPLO] vai salvar o usuário da alteração
);

3.2 Exemplos de configurações de JSON da coluna 'CODE' da tabela 'nfs_entry_point'

Configuração para substituir tabelas dentro da chave 'panel_remap':

{
  "panel_remap": {
    "tabela_original": "tabela_customizada",
    "tabela.original_2": "tabela_customizada_2"
  }
}

Configuração para substituição de Colunas:

{
  "panel_remap": {
    "tabela_original.coluna_original": "coluna_customizada",
    "tabela_original.coluna_original_2": "coluna_customizada_2"
  }
}

Traduzindo o valor padrão de cada título usado no customize para utilizar na configuração JSON:

Tabela Principal (ordem da esquerda para direita sem estar com as tarefas expandidas)

Chave JSONPosiçãoExemplo de Valor Padrão
table_column_title_1"Gestor"
table_column_title_2"Supervisor"
table_column_title_3"Encarregado/Líder"
table_column_ppc_week"PPC Semana"
table_column_ppc_in_charge"PPC Encarregado"
table_column_ppc_task"PPC Acumulado Tarefa"

Subtabela (Detalhamento por Equipe)

Chave JSONPosiçãoExemplo de Valor Padrão
subtable_title_1"Tarefa (CPO/FECHA)"
subtable_title_2"OBSERVAÇÃO (PLANEJAMENTO)"
subtable_title_3"UNIDADE MEDIDA"
subtable_title_4"SEMANA"
subtable_title_5"AVANÇO DE PRODUÇÃO"

Seção de Ocorrências

Chave JSONPosiçãoExemplo de Valor Padrão
occurrence_title_1"DATA OCORRENCIA"
occurrence_title_2"TAREFA (CPO / FECHA)"
occurrence_title_3"TIPO DE OCORRÊNCIA"
occurrence_title_4"DETALHE DA OCORRÊNCIA"
occurrence_title_5"CAUSA"
occurrence_title_6"SOLUCIONADO"

Ex Customização de títulos de Colunas passando dentro da chave 'customize' se baseando nos valores acima

{
  "customize": {
    "table_column_title_1": "titulo_customizado",
    "table_column_title_2": "titulo_customizado2",
    "table_column_title_2": "titulo_customizado3",
    "table_column_ppc_week": "titulo_customizado_ppc_week",
    "table_column_ppc_in_charge": "titulo_customizado_ppc_in_charge",
    "table_column_ppc_task": "titulo_customizado_ppc_task",
    "subtable_title_1": "titulo_customizado_subtable",
    "subtable_title_2": "titulo_customizado_subtable2",
    "subtable_title_3": "titulo_customizado_subtable3",
    "subtable_title_4": "titulo_customizado_subtable4",
    "subtable_title_5": "titulo_customizado_subtable5",
    "occurrence_title_1": "titulo_customizado_ocurrence1",
    "occurrence_title_2": "titulo_customizado_ocurrence2",
    "occurrence_title_3": "titulo_customizado_ocurrence3",
    "occurrence_title_4": "titulo_customizado_ocurrence4",
    "occurrence_title_5": "titulo_customizado_ocurrence5",
    "occurrence_title_6": "titulo_customizado_ocurrence6"
  }
}

Customização para ocultar colunas específicas seguindo a mesma lógica da customização de títulos porémm sem a possibilidade de ocultar as principais colunas subtabela, apenas da tabela principal, PPC e das ocorrencias, qualquer um desses, se estiver presente no JSON de configuração com o valor 1, ocultará a coluna referenciada

{
  "customize": {
    "table_column_hide_1": 1,
    "table_column_hide_2": 1,
    "table_column_hide_3": 1,
    "table_column_ppc_hide": 1,
    "occurrence_hide_1": 1,
    "occurrence_hide_2": 1,
    "occurrence_hide_3": 1,
    "occurrence_hide_4": 1,
    "occurrence_hide_5": 1,
    "occurrence_hide_6": 1
  }
}

Customização de cores à principio apenas da tabela e subtabela passando código hexadecimal da COR dentro do próprio 'customize'

{
  "customize": {
    "table_color": "#ffaa00",
    "subtable_color": "#D35400"
  }
}

Processamento

Configurações

Obter dados do NFS para xMova

VER CONFIGURAÇÃO NO DS_TABELA_CAMPO ORDER BY

Possiveis valores do campo IND_SIT_PROC

        1 =>  'Existe no param_mobile - Aguardando processamento',
        2 =>  'Insert na APP',
        3 =>  'Update na APP',
        4 =>  'Update - SEM dados alterados',
        5 =>  'Registro está DELETED: ',
        10 => 'Uso não definido para Mobile',
        115 => 'Descartado manualmente',
        -1 => 'Erro Parse Location',
        -2 => 'Erro no Insert',
        -3 => 'Erro no Update',
        -4 => 'Não localizou Boletim: ',
        -10 => 'Erro no Parse Json'
        -11 => 'Existem campos inválidos na coluna TABLE_FIELD da tabela nfs_core_par_parametros_mobile'

Validação de campos inválidos

Tanto na inserção quanto na atualização o processamento valida se os FIELDS configurados na parâmetros mobile de fato existem na tabela de referência, caso tenham campos inexistentes configurados os itens ficaram com a situação -11.

Um log semelhante a esse será gerado informando quais campos estão com problema e qual foi a requisição xMova.

Valor(es) para TABLE_FIELD inválido(s) na tabela nfs_core_par_parametros_mobile: EQP_SEQ_DBX,CLIENTE_SEQ_DBX - REQ_XMOVA_SEQ_DB: 316

O que é o XMova JSON?

O XMova JSON é o json principal que vem diretamente do XMova e é primeiramente salvo no NFS, depois é feito a sua separação na tabela nfs_sync_req_xmova_list e que foi fim é feito o seu DE/PARA com a nfs_core_param_mobile para salvar nas tabelas de Boletim e Apontamentos do cliente.

Como é salvo o XMova JSON?

Na imagem abaixo temos um fluxo simples desde o envio do XMova JSON até sua persistência na nfs_sync_req_xmova_list.

save_json_xmova(2).png

Como obter o XMova JSON?

O XMova JSON está sendo salvo no AWS S3 Bucket para que não seja necessário ser salvo no banco de dados, assim cada linha da tabela não fica pesada e somente com uma chave para o arquivo, no caso o json do xmova.

Quando for necessário fazer alguma análise é necessário primeiro ir na nfs_sync_req_xmova, nela vamos ter uma coluna chamada PATH_DATA_FILE:

image.png

O seu conteúdo é uma chave que é muito semelhante a um path, então pelo REQ_XMOVA_SEQ_DB, podemos filtrar pelo SEQ_DB da nfs_sync_req_xmova, ou mesmo podemos a partir de um ind_sit_proc_st inválido verificar o conteúdo do JSON, então agora copie o valor do PATH_DATA_FILE alvo, abra a página do cliente e vá no Admin console e clique no atalho AWS S3:

s3_admin_console.png

Agora copie a chave, cole e clique baixar para sua análise!!

save_json_from_s3.png

JSON:

xmova_json_downloaded.png

Introdução ao Processamento xMova

Esse processo consiste em receber e processar requisições vindas de dispositivos mobile. Os tipos de requisição são:

  1. GET: solicitação de dados de identificação/validação;
  2. LIST: solitação de massa de dados;
  3. SEND: envio de dados armazenados nos dispositivos;
  4. XFS: solicitação de dados de processos de XFS.

Informações Relevantes

  1. Todo o processamento é executando usando o timezone "America/Sao_Paulo";
  2. Os dados de MARTE e TELEMETRY, apesar de recebidos, são processados em um processo separado;
  3. Durante a recepção dos dados são registrados dados do request e do dispositivos. Esses dados são registrados na tabela app_simovaapps_device;
  4. Durante a execução de todas as etapas do processo são exibidos retornos direcionados para a tela e/ou log. Dentre essas informações estão a hora de início e fim do processo, status de retorno e, dependendo da etapa do processo, informações referentes aos dados processados;
  5. Requisições sem dados a processar apresentarão o seguinte retorno:
{
	"status": 98
}
  1. Requisição não salvas por inconsistência e/ou erro apresentarão o seguinte retorno:
{
	"status": 97
}
  1. Requisição GET retornando vazio:
{
	"status": 2
}

Status do Processando de Dados recebidos

Tabela nfs_sync_req_xmova

IND_SIT_PROCDescrição
0Registro recebido e aguardando início do processamento
1Dados distribuidos nas tabelas auth, binary, list, location, marte e telemetry
5Início do Processamento dos dados
50Em processamento
-5DS não carregado / Usuário DS diferente do Request XMova
-10JSON inválido
-115Desconsiderar Request

Tabela nfs_sync_req_xmova_list

IND_SIT_PROCDescrição
0Recebido de Mobile
1Registro definido para ser processado (existe na par_parametros_mobile)
2Registro processado e feito INSERT na tabela APP (FK_OBJECT e FK_SEQ_DB)
3Registro processado e feito UPDADE na tabela APP (FK_OBJECT e FK_SEQ_DB)
4Registro processado e feito UPDADE SEM ALTERACADO DE DADOS NO REGISTRO
5Registro está DELETED
10Uso não definido para (não existe na par_parametros_mobile)
115Descartado manualmente - Não processar (ex: Sem boletim no sistema)
-1Erro Parse Location
-2Erro no INSERT do Apontamento/Boletim
-3Erro UPDATE do Apontamento/Boletim
-4Não localizou o boletim
-10Erro no Parse JSON
-115Desconsiderar Request

As colunas FK_OBJECT e FK_SEQ_DB agora estão sendo preenchidas conforme o uso definido

Início do Processamento / Reprocessamento

  1. Início do processamento:
php xmova/nfs_xmova_init_process.php nfs.local init
  1. Reprocessamento de Registro:
php xmova/nfs_xmova_init_process.php nfs.local try {SEQ_DB}

Caso não seja informado SEQ_DB, serão processados os registros que apresentaram erro (IND_SIT_PROC < 0 na tabela nfs_sync_req_xmova_list).

Campos do tipo FKL e Atualização destes nas tabelas

Foi adicionado um recurso chamado FK Latest - FKL - ao Core.

O objetivo desse recurso é dar rapidez aos painéis e outros recursos de gestão à vista. Como exemplo, imagine o painel de equipamentos ou técnicos, basta uma única consulta nessa tabela para obter uma referência rápida dos mais recentes dados que estão ligados a esta tabela, sem a necessidade de efetuar consultas ORDER BY INI_DH DESC LIMIT 1 que levam tempo e consomem muitoa concorrência no I/O do banco de dados.

Seu funcionamento é da seguinte forma:

O CORE analisa todas as tabelas Boletim e Apontamento (TABELA->MOBILE_TABLE > 0), usando o TABELA->FK_UP como referência. A tabela FK_UP é uma tabela USADA pela tabela boletim ou apontamento, por exemplo, EFETIVO_FUNCIONARIO é uma tabela "UP" da tabela EQP_BOLETIM pois o boletim de equipamento tem o funcionário como um campo do tipo FK. Após essa análise de FK_UP o CORE criará as colunas NOME_DA_TABELA_FKL e NOME_DA_TABELA_FKL_DH na tabela FK_UP, exemplo: Tabela EFETIVO_FUNCIONARIO campo EQP_BOLETIM_FKL Tabela EFETIVO_FUNCIONARIO campo EQP_BOLETIM_FKL_DH Tabela EQUIPAMENTO campo EQP_BOLETIM_FKL Tabela EQUIPAMENTO campo EQP_BOLETIM_FKL_DH Essa mesma regra é efetuada para tabelas de APONTAMENTO: Tabela OPERACAO campo EQP_APT_FKL Tabela OPERACAO campo EQP_APT_FKL_DH Tabela FAZENDA campo EQP_APT_FKL Tabela FAZENDA campo EQP_APT_FKL_DH

Quando esses campos são atualizados:

Todas as vezes que o core recebe dados do SimovaApps esses campos serão analisados e atualizados conforme uso definido na PARAM_MOBILE. Regra de atualização: O campo _FKL_DH recebe o valor de INI_DH ou FIM_DH (Quando for Boletim e estiver preenchido)

O valor INI_DH deve ser maior que o valor que já existe no campo _FKL_DH do registro.

O campo FKL terá sempre o último registro ENVIADO PELO SIMOVAAPPS - ainda não estamos tratando edição / inclusão manual / split / abertura automática

like a boss

Se você quer otimizar ainda mais o tempo de resposta do painel e até mesmo outras informações:

Por exemplo:

Saber quais foram os últimos apontamentos de atividade improdutiva, produtiva, abastecimento e manutenção, como fazer?

Criar campos do tipo FK na tabela EQUIPAMENTO: ULT_ATIVIDADE_IMPRODUTIVA ULT_ATIVIDADE_PRODUTIVA ULT_ABASTECIMENTO ULT_MANUTENCAO

Criar um entryPoint 19 (ver sobre entryPoint) na tabela de Apontamento e usar o array $valores para analisar a condição que você deseja, se for necessário o uso de outros campos desse apontamento, você pode consultar o registro que acabou de ser inserido:

Opção 1:


	$apontamento = Dao::table('eqp_apt')
						->where($_lastInsertId);
	
	$operacoes = Dao::table('oper')
				->groupBy('SEQ_DB')
				->toArray()
				->get();

	$atualizarEquipamento = array();
	
	if ($apontamento['TIPO_OPERACAO] == '1') {  // atenção para certificar que o campo existe no array ou usar antes um isset ou empty
		$atualizarEquipamento['ULT_ATIVIDADE_PRODUTIVA'] = $apontamento['SEQ_DB'];
	} else {
		$atualizarEquipamento['ULT_ATIVIDADE_IMPRODUTIVA'] = $apontamento['SEQ_DB'];
		$operacao = $operacoes[$apontamento['OPER_SEQ_DB']];

		if ($operacao['FLAG_ABASTECIMENTO']) {
			$atualizarEquipamento['ULT_ABASTECIMENTO'] = $apontamento['SEQ_DB'];
		} else if ($operacao['QIG_DISPLAY_MECANICO']) {
			$atualizarEquipamento['ULT_MANUTENCAO'] = $apontamento['SEQ_DB'];
		}
	}
	
	$retorno = Dao::table('eqp')
					->where($apontamento['EQP_SEQ_DB'])
					->updateRow($atualizarEquipamento);

Opção 2:


	$operacaoSeqDb = $valores['OPER_SEQ_DB']''
	
	$operacoes = Dao::table('oper')
				->groupBy('SEQ_DB')
				->toArray()
				->get();

	$operacao = $operacoes[$apontamento['OPER_SEQ_DB']];

	$atualizarEquipamento = array();

	if ($operacao['FLAG_IMPRODUTIVA']) {
		$atualizarEquipamento['ULT_ATIVIDADE_IMPRODUTIVA'] = $apontamento['SEQ_DB'];
	} else if ($operacao['FLAG_ABASTECIMENTO']) {
		$atualizarEquipamento['ULT_ABASTECIMENTO'] = $apontamento['SEQ_DB'];
	} else if ($operacao['QIG_DISPLAY_MECANICO']) {
		$atualizarEquipamento['ULT_MANUTENCAO'] = $apontamento['SEQ_DB'];
	}
	if (!empty($atualizarEquipamento)) {
		$retorno = Dao::table('eqp')
					->where($apontamento['EQP_SEQ_DB'])
					->updateRow($atualizarEquipamento);
	}

tip Links úteis https://www.php.net/manual/pt_BR/function.empty.php https://www.php.net/manual/pt_BR/function.isset

Reconhecimento Imagem

Texto

É possível dentro do NFS fazer leitura de imagens para obter os seus textos e para isso usamos o Rekognition da AWS que usa o OCR

Os dados seguem pelo seguinte caminho após tudo configurado:

  • xMova envia um Apontamento com Foto que é salvo no NFS;
  • O processamento de Imagem para Texto procura por todas as imagens que não foram processadas;
  • Dentro do Processamento a Imagem é enviada para AWS Rekognition que faz o reconhecimento dos seus textos;
  • AWS Rekognition retorna um array com todos textos encontrados que segue a ordem da orientação da imagem;
  • Por fim o processo do NFS procura pelo chave/valor que corresponde a tabela de destino;

Importante

É importante dizer que é importante que a imagem esteja em pé, o que queremos dizer é que ela deve estar de acordo com o que a gente le, por exemplo:

right

Se enviar a imagem deitada:

wrong

Pode ser que a leitura fique incorreta.

Configuração

Os passos de configuração devem ser na mesma ordem que apresentados nessa documentação.

Parâmetro

Adicionar na nfs_core_par_parametros o parâmetro:

RECOGNITION

com valor igual 1.

Sem ele não funciona.

EMPRESAFILIALLOCALNOMECONTEUDOTIPO
499999999RECOGNITION11

Rodar o DS que vai criar uma nova coluna chamada nfs_conf_recognition.

Primeira configuração da nfs_conf_recognition

Para ser feita a primeira configuração do processamento das imagens obter seu texto é necessário preencher essa com os seguintes dado.

  • TABELA: Nessa campo é necessário a tabela de destino, no caso onde vão ficar os textos reconhecidos;
  • TABELA_FOTO: Nessa campo é necessário adicionar qual vai ser a tabela de onde vamos obter a foto;

Exemplo:

SEQ_DBINS_DHUPD_DHTABELATABELA_FOTOENABLED
12025-02-25 17:31:39NULLDADOS_PLAQUETASAPONTAMENTO_FOTO_PLAQUETA1

Rodar o DS que vão ser criadas novas colunas tanto na TABELA_FOTO, onde vai ter colunas de controle do processamento, quanto na TABELA que vai ser criada uma FK para a TABELA_FOTO para termos o rastreamento dos dados;

Campos de Reconhecimento

O próximo passo é configurar na nfs_core_ds_tabela_campo a tabela que você configurou na coluna TABELA na nfs_conf_recognition.

Na ds tabela campo vamos no registro em que vai representar os dados da foto, na coluna config e vamos adicionar um json que vai dizer quais labels esse coluna representa:

{
  "recognition": {
    "labels": [
      "Produto tipo",
      "Tipo"
    ]
  }
}

No caso do exemplo acima a minha coluna representa o Produto Tipo e Tipo, isso porque a mesma informação pode estar com labels diferentes com a mesma informação.

Exemplo:

TABELA_NOMENOMESEQSYSSEND_XMOVAGRIDGRID_MOBILEDESCRICAODESCRICAO_RESUMIDATIPOOBRIGATORIOTAMANHOTAMANHO_DECIMALMASCARAINSERT_UPDATEDISABLEDHINTLINKVALIDACAOOPCOESVALOR_DEFAULTVALIDACAO_VIEWFILTRO_VIEWTOOLTIP_MESSAGEPROPERTIESCONFIG
DADOS_PLAQUETASDATA_FABRICACAO30111Data de FabricaçãoData de FabricaçãoTXT0NULLNULLNULLIU0NULLNULLNULLNULLNULLNULLNULLNULLNULL{"recognition":{"labels": ["Fabricado em", "Ano de fabricação"]}}
DADOS_PLAQUETASNUMERO_SERIE20111Número de SérieNúmero de SérieTXT0NULLNULLNULLIU0NULLNULLNULLNULLNULLNULLNULLNULLNULL{"recognition":{"labels": ["№ de série"]}}
DADOS_PLAQUETASTIPO10111TipoTipoTXT0NULLNULLNULLIU0NULLNULLNULLNULLNULLNULLNULLNULLNULL{"recognition":{"labels": ["Produto tipo", "Tipo"]}}

Last but not least, Ativar o processamento da Imagem

Até a presente data ainda não foi definido como fazer isso de forma automática, estamos analisado, mas pode pedir ao Dalton para configurar os processamentos.

Relatórios

Hoje dentro do NFS os tipos abaixo de relatórios:

  • DataTables A partir do HTML gerado após configurado com twig ser possível se exportar para excel e pdf
  • Editor Editor para criação de Relatórios
  • EntryPoint Relatório por EntryPoint
  • Excel Geração de Excel usando o PhpSpreadsheet
  • Excel Background Gerar Relatório em Excel no Background
  • Excel Nativo Geração de Excel com um pouco mais de liberdade pode criar headers e footers personalizados
  • Marte Telemetria Criação do Relatório de Telemetria do Marte
  • Obter Html Relatório Obter Html de um Relatório
  • Twig Gera Relatório a partir do Twig

DataTables

Editor De Relatórios

Feito para auxilar alterações e testes em relatórios. Irá exibir da tabela nfs_reports campos TEMPLATE, FILTERS, QUERIES, EXPORT_OPTIONS, DISPLAY_NAME e NAME. Relatórios que usam entrypoint também terão o campo ENTRY_POINT para edição desde que o vinculo com o relatório pelos campos ACTION x NAME esteja correto.

O recurso de execução do relatório pelo Editor de Relatórios é exclusivo de templates base.twig relatórios PHPExcel podem ser alterados normalmente mas não terá opção de executar.

A execução correta depende de todos os campos exceto o campo TEMPLATE, que é considerado mesmo sem estar com o conteúdo salvo, com isso todos esses campos tem conteúdos editados serão salvos ao executar o relatório, o mesmo ocorre com a habilitação do salvamento automático, existindo campos editados e não salvos e o salvamento automatico for habilitado todos campos editados terão as alterações salvas.

A ferramenta está disponível em Admin Console no grupo NFS ENTRY POINT.

editor-img-01.png


Seleção do relatório:

Estarão disponíveis relatórios base.twig para manutenção e testes. Basta selecionar o relatório e clicar em Carregar template, é necessário que o relatório esteja funcionando e tenha dados para que o mesmo seja gerado corretamente e possa ter seu template testado.

editor-img-02.png

Também é possível acessar diretamente usando o SeqDb do relatório. (Ex. reports/editor/106)

Filtros:

O filtro do relatório selecionado será carregado conforme configuração. (mesmo comportamento da tela de relatórios).

editor-img-03.png

Template:

Container onde será carregado o template do relatório para edição/testes.

template-full.png

Área de edição:

Aqui serão exibixos todos campos disponíveis para alteração, basta ir alternando as abas para ter acesso ao conteúdo de cada campo.

Para que aba do entrypoint seja exibida é necessário o vinculo pelo NAME do relatório e o ACTION do entrypoint. Necessário atualizar a página para carregar as alterações.

edit-contents-2.png

Ações:

No container TEMPLATE temos os botões de ação, que são eles:

btn-actions.png

Executar código:

executar_btn.png Executa o código atual que está no template, e exibe o relatório em tela. É possível utilizar o comando Ctrl + Enter para realizar a execução.

Após execução o relatório será exibido na parte inferior da tela.

editor-img-06.png

Auto Save:

autosave_btn.png Ativa o salvamento automático.

Gravar:

gravar_btn.png Botão apresentado quando o salvamento automático está desligado. Salva as alterações no banco de dados.

Ao editar um dos campos veremos um sinal visual indicando que o conteúdo daquela aba foi editado em algum momento e não foi salvo.

pending-save-2.png

Word Wrap:

wordwrap_btn.png Ativa a quebra de linha, toda linha será exibida ajustada a largura da janela.

Ocultar template:

ocultar_btn.png Oculta o container do template.

Modo escuro:

modo-escuro.png

Ativa/Desativa o tema escuro.

dark-theme.png

Atalho

Na tela do relatório perfis SuperAdmin poderão ver um novo ícone ao lado do nome do relatório, esse ícone irá abrir em uma nova aba o relatório no Editor.

shortcut.png

Relatório por EntryPoint

Configurações

  • As consultas configuradas na coluna QUERIES, podem ser removidas dessa coluna e configuradas no entry_point do tipo PHP
{
	"teste": {
		"table": "chave_acesso"
	}
}

Na coluna QUERIES da tabela nfs_reports, pode ser inserido o valor "ENTRY_POINT" ao invés de uma configuração obrigatória como era feito antigamente, isso fará com que o relatório utilize apenas os valores vindos do entry_point {.is-info}

  • O entry_point deve ter a seguinte configuração nas colunas:
  1. FILE_OR_DOMAIN = REPORTS
  2. ACTION = mesmo valor usado no campo NAME da tabela nfs_reports
  3. CODETYPE = PHP
  4. CODE = ver exemplo abaixo:

4.1 - Para debugar o entryPoint pelo Test Code, adicionar as 2 linhas abaixo no inicio do entry_point

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

4.2 - Para executar: a. Clicar em Testar b. Abrir o relatório em uma segunda aba e executar c. Voltar no Test Code e clicar em Testar novamente

  • Os filtros serão retornados no campo $this->inputValues Exemplo de retorno para os campos de data:
Array
(
    [inicio] => 01/02/2021
    [inicioIni] => 01/02/2021 00:00:00
    [inicioFim] => 01/02/2021 23:59:59
    [inicioDb] => 2021-02-01
    [inicioIniDb] => 2021-02-01 00:00:00
    [inicioFimDb] => 2021-02-01 23:59:59
    [fim] => 25/08/2021
    [fimIni] => 25/08/2021 00:00:00
    [fimFim] => 25/08/2021 23:59:59
    [fimDb] => 2021-08-25
    [fimIniDb] => 2021-08-25 00:00:00
    [fimFimDb] => 2021-08-25 23:59:59
    [eqp] => 1
    [eqpSqlIn] => 1
    [eqp_fk] => Array
        (
            [0] => 1 :: EQP 1001
        )

)

Os valores retornados podem ser acessados da seguinte maneira:

$inicioIniDb = $this->inputValues['inicioIniDb'] ?? NULL;
$fimFimDb = $this->inputValues['fimFimDb'] ?? NULL;
$eqp = $this->inputValues['eqp'] ?? NULL;

Desta forma o valor poderá ser usado para passar como variável nas consultas

//Consulta na tabela de apontamentos
$listaAptFull = Dao::table('eqp_apt','a')
->select(
	"a.SEQ_DB",
	"a.INI_DH",
	"a.OPER_SEQ_DB",
)
->whereBetween('a.INI_DH', $inicioIniDb, $fimFimDb);
->orderBy('a.INI_DH','ASC')
->get();

A lista com os dados devem ser enviados para o template através do $this->queryData

$this->queryData['apontamento_list'] = $listaApt;

Exemplo completo de entry_point:

//Para debugar o entryPoint pelo Test Code, adicionar as 2 linhas abaixo no inicio do entry_point
//$_SESSION['DEBUG_MODE'] = 1;
//$this->inputValues = $_SESSION['DEBUG_PARAMS'];

//Os filtros serão retornados no campo **$this->inputValues**
$inicioIniDb = $this->inputValues['inicioIniDb'] ?? NULL;
$fimFimDb = $this->inputValues['fimFimDb'] ?? NULL;
$eqp = $this->inputValues['eqp'] ?? NULL;

//Consulta na tabela de apontamentos
$queryBuilder = Dao::table('eqp_apt','a')
->select(
	"a.SEQ_DB",
	"a.SEQ_DB_DEVICE_MASTER_SEQ_DB",
	"a.INI_DH",
	"a.OPER_SEQ_DB",
)
->whereBetween('a.INI_DH', $inicioIniDb, $fimFimDb);
//Caso exista filtro por equipamento
if (isset($eqp)) {
	$queryBuilder		
	->whereIn('a.EQP_SEQ_DB', $eqp);
}
$listaAptFull = $queryBuilder
->orderBy('a.INI_DH','ASC')
->get();

//Adiciona SEQ_DB como indice do array
$listaApt = [];
$listaApt = array_column($listaAptFull,null,'SEQ_DB');

//Pegar o campo OPER_SEQ_DB de cada Apontamento, e agrupar por Equipamento (SEQ_DB)
//Fazer a consulta fora do foreach, para evitar fazer a mesma consulta mais de uma vez
$listaOperFK = array_column($listaAptFull,"OPER_SEQ_DB","OPER_SEQ_DB");

//Retorna a lista com todas as Operações, filtras pelos apontamentos
$listaOperFull = Dao::table('OPER')->select(['SEQ_DB','CODIGO','DESCRICAO'])->whereIn('SEQ_DB', $listaOperFK)->get();
$listaOper = [];
$listaOper = array_column($listaOperFull,null,'SEQ_DB');

foreach ($listaApt as $row) {
	$seqApt = $row['SEQ_DB'];
	//Converter o formato de apresentação da DATA, não fazer a conversão diretamente pela consulta SQL (DAO)
	$listaApt[$seqApt]['DATA'] = date('d/m/Y H:i:s', strtotime($row['INI_DH']));
	
	//Pegar o valor de DISPLAY de um campo do tipo FK. Evitar fazer inner join pela consulta SQL (DAO)
	$oper_row = $listaOper[$row['OPER_SEQ_DB']] ?? null;
	if (!empty($oper_row)) {
		$listaApt[$seqApt]['OPERACAO'] = $oper_row['CODIGO'].' - '.$oper_row['DESCRICAO'];					
	}
}
//A lista com os dados devem ser enviados para o template através do **$this->queryData**
$this->queryData['apontamento_list'] = $listaApt;

Painel

Configurações

Para configurar um painel QIG (Quadro de Indicador Geral) + QII (Quadro de Indicador Individual) através do entry_point, é necessário criar um registro na tabela nfs_qig_panels com os seguintes campos:

O nfs_qig_panels deve ter a seguinte configuração nas colunas:

NAME = usar um valor único que deverá ser usado no entry_point DISPLAY_NAME = Nome do Painel REFRESH_INTERVAL = valor será multiplicado por 20 segundos (ex.: 3 = 60 segundos), usado para definir o tempo de atualização do painel MAIN_TABLE = Tabela que será usada para apresentar o QII (ex.: EQP) SECONDARY_TABLE = Tabela que tem relacionamento direto com a MAIN_TABLE (ex.: EQP_BOLETIM) SCRIPT_PHP = ENTRY_POINT SECONDARY_TABLE_OPTIONS = {} OPTIONS = {"signal": {"online": 30, "standby": 120, "offline":240}} ENABLE = 1

O entry_point deve ter a seguinte configuração nas colunas:

  1. FILE_OR_DOMAIN = PANEL
  2. ACTION = mesmo valor usado no campo NAME da tabela nfs_qig_panels
  3. CODETYPE = PHP
  4. CODE = ver exemplo abaixo:

4.1 - Para debugar o entryPoint pelo Test Code

  • adicionar as 3 linhas abaixo no inicio do entry_point:
$_SESSION['DEBUG_MODE'] = 1;
$dadosPainel = $_SESSION['DEBUG_DATA'];
$mainTable = $_SESSION['DEBUG_RAW_DATA']['main_table'];
  • e remover as linhas:
$dadosPainel = $this->inputValues;
$mainTable = $this->defaultQigRawData['main_table'];

4.2 - Para executar: a. Clicar em Testar b. Abrir o painel em uma segunda aba e executar c. Voltar no Test Code e clicar em Testar novamente

POSIÇÕES [F1 .. F11]

img30.png

  • Nas posições de F1 à F11 (exceto F7), é possível definir: FIELD = Campo LABEL = Descrição do campo que será apresentado antes do valor TOOLTIP = Descrição do campo ao passar o mouse sobre ele COLOR = Cor do campo ICON = Icone do campo (somente para os campos de F8 à F11)
$qii['F1']['LABEL'] = 'Equipamento:';
$qii['F1']['TOOLTIP'] = 'Equipamento';
$qii['F1']['FIELD'] = $mainTable[$index]['CODIGO'];
$qii['F1']['COLOR'] = 'black';
$qii['F8']['ICON'] = 'fa-battery-full';
  • Na posição F7 é possível definir uma imagem do servidor:
$qii['F7'] = 'assets/img/painel/colhedorajd.png';
  • Além das posições de F1 à F11, é possível definir um FOOTER, onde é possível criar um link para uma url
$qii['FOOTER']['FIELD'] = DateUtils::formatFromTable('now','d/m/Y H:i:s');

Campo F12

No F12 é possível utilizar a propriedade CLASS, que aceita receber o valor 'blink-icon', quando essa propriedade é adicionada aplica um efeito de "pisca" no ícone.

  • Exemplos de configuração:
$qii['F12'] = [	
	  'FIELD' => 'Atenção',
	'TOOLTIP' => 'Tempo expirado',
	  'COLOR' => '#000',
 'ICON_COLOR' => '#FF0000', 
       'ICON' => 'fa-exclamation-triangle',
      'CLICK' => "url",
       'COLS' => '',
	  'CLASS' => 'blink-icon'
	];
	$qii['F12'] = ['FIELD' => 'Caution',
		'ICON_COLOR' => '#FF0000',
		'ICON' => 'fa-exclamation',
		'CLASS' => 'blink-icon'
   ];
  • O mínimo de configuração esperada para apresentação do icone é:
$qii['F12'] = ['FIELD' => 'Caution',
	'ICON' => 'fa-exclamation-triangle'
 ];
  • Posição: Campo está posicionado no canto inferior direito do card nos 3 painéis (P1, P2 e Kanban).

_f12.png

SUBTITLE

Por padrão todo painel tem um filtro/legenda por Boletim Fechado

  • Desabilitar legenda:
unset($dadosPainel['SUBTITLE']['BOLETIM_FECHADO']);
  • Inserir nova legenda:
$dadosPainel['SUBTITLE']['PRODUTIVO'] =['color' => "green", 'textColor' => "black", 'subtitle' => "Serviço", 'id' => "PRODUTIVO", 'panel_seq_db' => 1];
  • Adicionar id e cor da legenda no QII
$qii['SUBTITLE']['id'] = 'PRODUTIVO';
$qii['SUBTITLE']['color'] = $dadosPainel['SUBTITLE']['PRODUTIVO']['color'];

ICON

  • Somente para os campos de F8 à F11

Utilizar icones disponíveis em: METRONIC

$qii['F8']['ICON'] = 'fa-battery-full';

ORDEM DO QII

$qii['ORDER'] = 10005;

COLOR

COR DE FUNDO

$qii['BG_COLOR'] = 'yellow';

COR DO CAMPO

$qii['BG_COLOR'] = 'yellow';

É necessário montar um link para o relatório já com os parametros passados no filtro. Veja o vídeo de como pegar a url:

  • Pagar a URL do relatório que deseja apresentar img29.png

  • Montar a URL do relatório, passando os filtros de acordo com o QII

$REPORTS_NAME = 'relatorio_pdf';

$form_default = 'form%5B';
$igual = '%5D=';
$data_inicio = date('d/m/Y');
$data_fim = date('d/m/Y', strtotime('+1 days'));

$inicio_formatado = str_replace("/", "%2F",$data_inicio);
$fim_formatado = str_replace("/", "%2F",$data_inicio);

$filtro_inicio = $form_default.'inicio'.$igual.$inicio_formatado;
$filtro_fim = $form_default.'fim'.$igual.$fim_formatado;

$EQP_SEQ_DB = $mainTable[$index]['SEQ_DB'];
$filtro_eqp = $form_default.'eqp'.$igual.$EQP_SEQ_DB;
$filtro_relatorio = $filtro_inicio.'&'.$filtro_fim.'&'.$filtro_eqp;

$reportLink = "nfsui.newWindow({url:'reports/".$REPORTS_NAME."?$filtro_relatorio'});";

$reportAHref = '<a class="" href="javascript:;"  onclick="'.$reportLink.'">'.$mainTable[$index]['CODIGO'].'</a>';

O link pode ser configurado apenas nas posições FI_LINK ou FOOTER A posição F1 deve ser oculta ao usar o FI_LINK

unset($qii['F1']);

EXEMPLO

  • Exemplo simples de entry_point:
$dadosPainel = $this->inputValues;
$mainTable = $this->defaultQigRawData['main_table'];

foreach ($dadosPainel['QII'] as $index => $qii) {
	$qii['F1']['TOOLTIP'] = 'Código do Equipamento';
	$qii['F1']['FIELD'] = $mainTable[$index]['CODIGO'];	
	
	$qii['F2']['LABEL'] = 'Placa do Equipamento';
	$qii['F2']['TOOLTIP'] = 'Placa';
	$qii['F2']['FIELD'] = $mainTable[$index]['PLACA'];
	
	$dadosPainel['QII'][$index] = $qii;
}
$this->outputValues = $dadosPainel;
  • Exemplo completo de entry_point:
//Habilitar campos abaixo para teste (TestCode)
//$_SESSION['DEBUG_MODE'] = 1;
//$dadosPainel = $_SESSION['DEBUG_DATA'];
//$mainTable = $_SESSION['DEBUG_RAW_DATA']['main_table'];


//Habilitar campos abaixo ao adicionar no entryPoint
$dadosPainel = $this->inputValues;
$mainTable = $this->defaultQigRawData['main_table'];

//Desabilita o legenda de Boletim Fechado
unset($dadosPainel['SUBTITLE']['BOLETIM_FECHADO']);
//Inserir nova legenda
$dadosPainel['SUBTITLE']['PRODUTIVO'] =['color' => "green", 'subtitle' => "Serviço", 'id' => "PRODUTIVO", 'panel_seq_db' => 1];
$dadosPainel['SUBTITLE']['IMPRODUTIVO'] =['color' => "red", 'subtitle' => "Parada", 'id' => "IMPRODUTIVO", 'panel_seq_db' => 1];

//Pegar o campo EQP_APT_FKL de cada Equipamento, e agrupar por Equipamento (SEQ_DB)
$listaDasFKL = array_column($mainTable,"EQP_APT_FKL","SEQ_DB");
//print_r($listaDasFKL);

//Obter apontamentos relacionados ao equipamento através do campo de EQP_APT_FKL(FK do último apontamento)
$apt_list = Dao::table('EQP_APT')->select(['SEQ_DB','TIPO_OPERACAO','INI_DH','EQP_SEQ_DB'])->whereIn('SEQ_DB', $listaDasFKL)->get();
//print_r($apt_list);

//Agrupar apontamentos por Equipamento, retorna apenas o ultimo registro de cada apontamento por equipamento
$apt_array = [];
if(!empty($apt_list)) {
	$apt_array = array_column($apt_list, null,'EQP_SEQ_DB');
}

//Inicia montagem da URL do relatório
$REPORTS_NAME = 'relatorio_pdf';

$form_default = 'form%5B';
$igual = '%5D=';
$data_inicio = date('d/m/Y');
$data_fim = date('d/m/Y', strtotime('+1 days'));

$inicio_formatado = str_replace("/", "%2F",$data_inicio);
$fim_formatado = str_replace("/", "%2F",$data_inicio);

$filtro_inicio = $form_default.'inicio'.$igual.$inicio_formatado;
$filtro_fim = $form_default.'fim'.$igual.$fim_formatado;


//Para cada item do painel (QII)
$qii_array = [];


$qtdEQPComApontamento = 0;
$qtdEQPSemApontamento = 0;
foreach ($dadosPainel['QII'] as $index => $qii) {
	
	//Obtem o SEQ_DB do Equipamento
	$EQP_SEQ_DB = $mainTable[$index]['SEQ_DB'];
	
	//Verifica se o equipamento tem apontamento
	$apt_rec = $apt_array[$EQP_SEQ_DB] ?? null;	
	if(!empty($apt_rec)) {
		$qtdEQPComApontamento ++;
		
		//Cria link para o relatório		
		$filtro_eqp = $form_default.'eqp'.$igual.$EQP_SEQ_DB;
		$filtro_relatorio = $filtro_inicio.'&'.$filtro_fim.'&'.$filtro_eqp;
		
		$reportLink = "nfsui.newWindow({url:'reports/".$REPORTS_NAME."?$filtro_relatorio'});";
		//print_r($reportLink);
		
		//$reportLink = "nfsui.newWindow({url:'reports/".$REPORTS_NAME."?form%5Beqp%5D=".$EQP_SEQ_DB."'});";	
		$reportAHref = '<a class="" href="javascript:;"  onclick="'.$reportLink.'">'.$mainTable[$index]['CODIGO'].'</a>';

		unset($qii['F1']);/*se usar o LINK, remover o campo F1 (só usar link no F1_LINK ou no FOOTER)*/
		$qii['F1_LINK']['TOOLTIP'] = 'Código do Equipamento';
		$qii['F1_LINK']['FIELD'] = $reportAHref;
		
		//Apresenta a descrição do Equipamento
		$qii['F3']['LABEL'] = 'Equipamento:';
		$qii['F3']['TOOLTIP'] = 'Equipamento';
		$qii['F3']['FIELD'] = $mainTable[$index]['DESCRICAO'];
		
		//Apresenta a data do ultimo apontamento relacionado ao equipamento, através do campo FKL
		$qii['F4']['LABEL'] = 'Data:';
		$qii['F4']['TOOLTIP'] = 'Data do último apontamento';
		$qii['F4']['FIELD'] = DateUtils::formatFromTable($apt_rec['INI_DH'],'d/m/Y H:i:s');		
		
		//unset($qii['F5']);
		//unset($qii['F6']);
		
		//Utiliza imagens gravadas no servidor
		$qii['F7'] = 'assets/img/painel/colhedorajd.png';
		$qii['F8']['ICON'] = 'fa-battery-full';
		
		//unset($qii['F9']);
		//unset($qii['F10']);
		//unset($qii['F11']);
		
		if ($apt_rec['TIPO_OPERACAO'] == 1) {
			//SERVIÇO
			//Adiciona cor e id da legenda no QII
			$qii['SUBTITLE']['id'] = 'PRODUTIVO';
			$qii['SUBTITLE']['color'] = $dadosPainel['SUBTITLE']['PRODUTIVO']['color'];
			
			//Cria uma barra e um link 
			$qii['FOOTER']['FIELD'] = '<div style="background-color:#bbffc0;">&nbsp;Ver: '.$reportAHref.'</div>';
			//Altera a cor do fundo
			$qii['BG_COLOR'] = '#bbffc0';
		} else {
			//PARADA
			$qii['SUBTITLE']['id'] = 'IMPRODUTIVO';
			$qii['SUBTITLE']['color'] = $dadosPainel['SUBTITLE']['IMPRODUTIVO']['color'];
			$qii['FOOTER']['FIELD'] = '<div style="background-color:#ffacac;">&nbsp;Ver: '.$reportAHref.'</div>';
			$qii['BG_COLOR'] = '#ffacac';
		}
		//Cria um link 
		//$qii['FOOTER']['FIELD'] = 'Ver: '.$reportAHref;
		
		//Define a ordem do registro
		$qii['ORDER'] = $EQP_SEQ_DB;
		
		
		$qii_array[$index] = $qii;
	} else { 
		$qtdEQPSemApontamento ++;
	}
}
$dadosPainel['QII'] = $qii_array;
$this->outputValues = $dadosPainel; 
$this->indicatorsValues['EQP_COM_APONTAMENTO'] = $qtdEQPComApontamento;
$this->indicatorsValues['EQP_SEM_APONTAMENTO'] = $qtdEQPSemApontamento;
//echo "<pre>";
//print_r($qii_array);
//echo "</pre>";

Introdução

A Classe NfsReport foi criada com o objetivo de abstrair a crição de Planilhas Eletrônicas e manter as práticas correntes desse processo no NFS.

Criar e Manipular Planilha

/**
 * Cria objeto NfsReport permintindo o preenchimento e formatação.
 *
 * @param array $options Opções da planilha
 * $options = [
 *		'title' => ?string = 'título do documento'
 *		'subject' => ?string = 'assunto do documento'
 *		'sheetName' => ?string = 'nome da planilha'
 *		'userName' => ?string = 'nome do usuário'
 * ];
 */
public static function createSpreadsheet(array $options): self
/**
 * Cria uma nova aba em uma Planilha.
 *
 * @param string $sheetName Nome da aba / worksheet
 * @param int    $index     Índice da aba / worksheet
 */
public function createSheet(string $sheetName, int $index = 0): self
/**
 * Define planilha ativa por índice.
 *
 * @param int $index Índice da planilha (começa no 0)
 */
public function setActiveSheetByIndex(int $index): self
/**
 * Define planilha ativa pelo nome da planilha.
 *
 * @param string $name Nome da planilha
 */
public function setActiveSheetByName(string $name): self
/**
 * Define o título da planilha.
 *
 * @param string $title Título da planilha
 */
public function setTitle(string $title): self
/**
 * Finaliza objeto e libera recursos alocados.
 */
public function close()

Manipulação de Dados

/**
 * Define valor de uma célula.
 *
 * @param string $cell     Referência da Célula [A1|..|ZN]
 * @param mixed  $value    Valor da célula
 * @param string $dataType Tipo de dado (Referência Constantes Tipos de Dado)
 */
public function setCellValue(string $cell, mixed $value, ?string $dataType = null): self
/**
 * Preenche planilha com valores do array.
 *
 * @param array  $rows Dados a adicionar
 * @param string $cell Célula de referência para preenchimento
 */
public function addFromArray(array $rows, string $cell = 'A1'): self
/**
 * Mescla celulas e preenche.
 *
 * @param string $reference Referência para mesclagem e preenchimento
 * @param mixed  $value     Valor de preenchimento da célula mesclada
 * @param mixed  $wrapText  Se o valor aceita quebra de linha ou não
 */
public function mergeCells(string $reference, mixed $value = null, bool $wrapText = true): self
/**
 * Define explicitamento o tipo de dados de uma célula.
 *
 * @param string $cell     Referência da célula
 * @param string $dataType $dataType Tipo de dado
 */
public function setCellDataType(string $cell, string $dataType): self
/**
 * Define a formatação de campos/bloco de células.
 * IMPORTANTE: usar a string do formato, não as constantes definidas na classe.
 *
 * @param string $reference Referência da célula ('A1'..'Z9'|) ou bloco ('A1:C3'..'A1:Z5')
 * @param string $format    String do formato (Referência Constantes Tipos de Dado)
 */
public function setNumberFormat(string $reference, string $format): self
    /**
     * Insere imagem vinculada a uma célula.
     *
     * @param string     $base64    Base64 da imagem que será inserida
     * @param string     $reference Referência da célula
     * @param null|array $props     Propriedades da imagem
     *                              [height, width, name, description, offSetX, offSetY, rotation]
     */
    public function addImageFromBase64(string $base64, string $reference, ?array $props = []): self

Formatação de Dados

/**
 * Define largura da coluna.
 *
 * @param string $col   Referência da coluna (A..Z..AA..ZZ)
 * @param float  $width Largura relativa ao tamanho da fonte
 */
public function setColWidth(string $col, float $width): self
 /**
 * Define altura da linha.
 *
 * @param int   $row    Referência da linha
 * @param float $height Altura da linha relativa ao tamanho da fonte
 */
public function setRowHeight(int $row, float $height): self
/**
 * Define referência para aplicação de propriedades.
 *
 * @param string $reference Célula ou bloco de células
 */
public function getStyle(string $reference): self
/**
 * Aplica estilos usando array de propriedades.
 * IMPORTANTE: usar a string do formato, não as constantes definidas na classe.
 * Usar as strings definidas em cada propriedade (de acordo com os métodos dessa classe).
 *
 * @param array $props Propriedades definidas (Referência Externa Estilos)
 */
public function applyFromArray(array $props): self
/**
 * Define o alinhamento da célula ou celulas.
 *
 * @param string $reference Referência para mesclagem e preenchimento
 * @param string $horAlign  Alinhamento horizontal (Referência Constantes Alinhamento Horizontal)
 * @param string $vertAlign Alinhamento vertical (Referência Constantes Alinhamento Vertical)
 * @param bool   $wrapText  Se o valor aceita quebra de linha ou não
 */
public function setAlignment($reference, $horAlign = 'general', $vertAlign = 'v-top', $wrapText = false): self
/**
 * Define formatação da célula ou células.
 *
 * @param string $reference  Referência para mesclagem e preenchimento
 * @param float  $fontSize   Tamanho da fonte
 * @param string $fontColor  Cor da fonte
 * @param bool   $fontBold   Se o texto estará em negrito ou não
 * @param bool   $fontItalic Se o texto estará em itálico ou não
 * @param string $bgColor    Cor de fundo / background
 */
public function setStyle(string $reference, float $fontSize = 11, ?string $fontColor = null, bool $fontBold = false, bool $fontItalic = false, ?string $bgColor = null): self
/**
 * Tenta determinar a largura de uma coluna
 * Se o parâmetro não for especificado, define autosize para todas as colunas preenchidas.
 *
 * @param string $col Referência da coluna (A-Z..AA..ZZ)
 */
public function autoSize(?string $col = null): self
/**
 * Define célula/bloco como.
 *
 * @param string $reference Referência célula/bloco
 * @param bool   $prop      Se o valor aceita quebra de linha ou não
 */
public function setWrapText(string $reference, bool $prop = true): self

Quando se exporta o crud list num excel os campos LOVN são exibidos juntos, para facilitar a quebra de linha foi criado o parâmetro CRUD_LIST_LOVN_LINE_BREAK para fazer o parse no excel.

CRUD_LIST_LOVN_LINE_BREAK

Referências

Constantes Definidas

Tipo de Preenchimento de Cor de Fundo / Background

Opções: 'none', 'solid', 'linear'

Alinhamento

Opções Horizontal: 'general', 'left', 'right', 'center', 'justify' Opções Vertical: 'v-top', 'v-center', 'v-bottom', 'v-justify'

Tipos de Dado

Opções: 'string', 'formula', 'numeric', 'boolean', 'null', 'inline', 'error'

Tipos de Borda

Opções: 'none', 'dashDot', 'dashDotDot', 'dashed', 'dotted', 'double', 'hair', 'medium', 'mediumDashDot', 'mediumDashDotDot', 'mediumDashed', 'slantDashDot', 'thick', 'thin'

Fontes Sublinhadas / Underline

Opções: 'none', 'double', 'single'

Referências Externas

IMPORTANTE: nos relatórios gerados através da tabela nfs_reports o nome do objeto criado pelo processo foi definido como $sheet (na versão antiga era $objPHPExcel). {.is-warning}

Casos de Uso

Criação e Preenchimento

$report = NfsReport::createSpreadsheet([
	'title' => 'Planilha',
	'subject' => 'Teste de classe NfsReport',
	'userName' => 'Teste Unitário',
	'sheetName' => 'Planilha Teste',
]);

$report->addFromArray([
	[1, 2, 3, 4, 5],
	[6, 7, 8, 9, 10],
	[11, 12, 13, 14, 15],
	[16, 17, 18, 19, 20],
], 'A1');

$report->CreateSheet('Planilha 2', 1);
$report->addFromArray([
	['Core', 'Dev', 'Support'],
	['Simova', 'NFS', 'Report'],
], 'A1');

$report->setActiveSheetByIndex(0);
$report->addFromArray([
	['Core', 'Dev', 'Support'],
	['Simova', 'NFS', 'Report'],
], 'A5');

Criação, Preenchimento e Formatação

$report = NfsReport::createSpreadsheet([
	'title' => 'Planilha',
	'subject' => 'Teste de classe NfsReport',
	'userName' => 'Teste Unitário',
]);

$report->addFromArray([
	[1, 2, 3, 4, 5],
	[6, 7, 8, 9, 10],
	[11, 12, 13, 14, 15],
	[16, 17, 18, 19, 20],
], 'A1');

$report->getStyle('A1:E1')
	->applyFromArray([
  	'fill' => [
			'fillType' => 'solid',
      'color' => [
      	'rgb' => 'FF0000',
			],
		],
		'font' => [
    	'name' => 'Calibri',
			'bold' => true,
			'underline' => 'double',
			'color' => [
				'rgb' => 'FFFFFF',
			],
			'size' => 14.5,
		],
		'borders' => [
			'allBorders' => [
				'borderStyle' => 'double',
				'color' => [
					'argb' => '000000',
				],
			],
		],
		'alignment' => [
			'horizontal' => 'center',
			'vertical' => 'v-center',
			'wrapText' => true,
		],
		'quotePrefix' => true,
	]);

$report->getStyle('A2:E2')
	->applyFromArray([
		'fill' => [
			'fillType' => 'solid',
			'color' => [
				'rgb' => '00FF83',
			],
		],
		'font' => [
			'name' => 'Arial',
			'bold' => true,
			'italic' => true,
			'superscript' => true,
			'underline' => 'single',
			'strikethrough' => true,
			'color' => [
				'rgb' => '808080',
			],
			'size' => 11,
		],
		'borders' => [
			'allBorders' => [
				'borderStyle' => 'dashDot',
				'color' => [
					'rgb' => 'f9a11a',
				],
			],
		],
		'alignment' => [
			'horizontal' => 'center',
			'vertical' => 'v-center',
			'wrapText' => true,
		],
		'quotePrefix' => true,
	]);

$report->getStyle('A3:E4')
	->applyFromArray([
		'font' => [
			'name' => 'Tahoma',
			'superscript' => true,
			'color' => [
				'rgb' => 'FF000000',
			],
			'size' => 8,
		],
	]);

Outro Exemplo

$display_name = 'MSI';
$sheet = NfsReport::createSpreadsheet([
  'title' => $display_name,
  'subject' => $display_name,
  'sheetName' => $display_name
]);
$row = 1;

$styleTitleCenter = [
	'font' => [
		'size' => 12,
		'bold' => true,
		'color' => ['rgb' => '000000']
	],
	'fill' => [
		'fillType' => 'solid',
		'color' => ['rgb' => 'FFFFFF']
	],
	'alignment' => [
		'horizontal' => 'center',
        'vertical' => 'v-center'
	],
	'borders' => [
		'allBorders' => [
			'borderStyle' => 'thin',
			'color' => ['rgb' => 'FFFFFF']
		]
	]
];

$styleTitleLeft = [
	'font' => [
		'size' => 12,
		'bold' => true,
		'color' => ['rgb' => '000000']
	],
	'fill' => [
		'fillType' => 'solid',
		'color' => ['rgb' => 'FFFFFF']
	],
	'alignment' => [
		'horizontal' => 'left',
        'vertical' => 'v-center'
	],
	'borders' => [
		'allBorders' => [
			'borderStyle' => 'thin',
			'color' => ['rgb' => 'FFFFFF']
		]
	]
];

$styleTitleRight = [
	'font' => [
		'size' => 12,
		'bold' => true,
		'color' => ['rgb' => '000000']
	],
	'fill' => [
		'fillType' => 'solid',
		'color' => ['rgb' => 'FFFFFF']
	],
	'alignment' => [
		'horizontal' => 'right',
        'vertical' => 'v-center'
	],
	'borders' => [
		'allBorders' => [
			'borderStyle' => 'thin',
			'color' => ['rgb' => 'FFFFFF']
		]
	]
];

$styleHeadCenterBlue = [
	'font' => [
		'size' => 11,
		'bold' => true,
		'color' => ['rgb' => 'FFFFFF']
	],
	'fill' => [
		'fillType' => 'solid',
		'color' => ['rgb' => '002060']
	],
	'alignment' => [
		'horizontal' => 'center',
        'vertical' => 'v-center'
	],
	'borders' => [
		'allBorders' => [
			'borderStyle' => 'thin',
			'color' => ['rgb' => '002060']
		]
	]
];

$styleHeadLeftBlue = [
	'font' => [
		'size' => 11,
		'bold' => true,
		'color' => ['rgb' => 'FFFFFF']
	],
	'fill' => [
		'fillType' => 'solid',
		'color' => ['rgb' => '002060']
	],
	'alignment' => [
		'horizontal' => 'left',
        'vertical' => 'v-center'
	],
	'borders' => [
		'allBorders' => [
			'borderStyle' => 'thin',
			'color' => ['rgb' => '002060']
		]
	]
];

$styleHeadRightBlue = [
	'font' => [
		'size' => 11,
		'bold' => true,
		'color' => ['rgb' => 'FFFFFF']
	],
	'fill' => [
		'fillType' => 'solid',
		'color' => ['rgb' => '002060']
	],
	'alignment' => [
		'horizontal' => 'right',
        'vertical' => 'v-center'
	],
	'borders' => [
		'allBorders' => [
			'borderStyle' => 'thin',
			'color' => ['rgb' => '002060']
		]
	]
];

$styleHeadCenterPurple = [
	'font' => [
		'size' => 11,
		'bold' => true,
		'color' => ['rgb' => 'FFFFFF']
	],
	'fill' => [
		'fillType' => 'solid',
		'color' => ['rgb' => '421c5e']
	],
	'alignment' => [
		'horizontal' => 'center',
        'vertical' => 'v-center'
	],
	'borders' => [
		'allBorders' => [
			'borderStyle' => 'thin',
			'color' => ['rgb' => '421c5e']
		]
	]
];

$styleHeadLeftPurple = [
	'font' => [
		'size' => 11,
		'bold' => true,
		'color' => ['rgb' => 'FFFFFF']
	],
	'fill' => [
		'fillType' => 'solid',
		'color' => ['rgb' => '421c5e']
	],
	'alignment' => [
		'horizontal' => 'left',
        'vertical' => 'v-center'
	],
	'borders' => [
		'allBorders' => [
			'borderStyle' => 'thin',
			'color' => ['rgb' => '421c5e']
		]
	]
];

$styleHeadRightPurple = [
	'font' => [
		'size' => 11,
		'bold' => true,
		'color' => ['rgb' => 'FFFFFF']
	],
	'fill' => [
		'fillType' => 'solid',
		'color' => ['rgb' => '421c5e']
	],
	'alignment' => [
		'horizontal' => 'right',
        'vertical' => 'v-center'
	],
	'borders' => [
		'allBorders' => [
			'borderStyle' => 'thin',
			'color' => ['rgb' => '421c5e']
		]
	]
];

$styleCenterWhite = [
	'font' => [
		'size' => 11,
		'bold' => false,
		'color' => ['rgb' => '000000']
	],
	'fill' => [
		'fillType' => 'solid',
		'color' => ['rgb' => 'FFFFFF']
	],
	'alignment' => [
		'horizontal' => 'center',
        'vertical' => 'v-center'
	],
	'borders' => [
		'allBorders' => [
			'borderStyle' => 'thin',
			'color' => ['rgb' => 'FFFFFF']
		]
	]
];

$styleLeftWhite = [
	'font' => [
		'size' => 11,
		'bold' => false,
		'color' => ['rgb' => '000000']
	],
	'fill' => [
		'fillType' => 'solid',
		'color' => ['rgb' => 'FFFFFF']
	],
	'alignment' => [
		'horizontal' => 'left',
        'vertical' => 'v-center'
	],
	'borders' => [
		'allBorders' => [
			'borderStyle' => 'thin',
			'color' => ['rgb' => 'FFFFFF']
		]
	]
];

$styleRightWhite = [
	'font' => [
		'size' => 11,
		'bold' => false,
		'color' => ['rgb' => '000000']
	],
	'fill' => [
		'fillType' => 'solid',
		'color' => ['rgb' => 'FFFFFF']
	],
	'alignment' => [
		'horizontal' => 'right',
        'vertical' => 'v-center'
	],
	'borders' => [
		'allBorders' => [
			'borderStyle' => 'thin',
			'color' => ['rgb' => 'FFFFFF']
		]
	]
];

$styleCenterBlue = [
	'font' => [
		'size' => 11,
		'bold' => false,
		'color' => ['rgb' => '000000']
	],
	'fill' => [
		'fillType' => 'solid',
		'color' => ['rgb' => 'dae3f3']
	],
	'alignment' => [
		'horizontal' => 'center',
        'vertical' => 'v-center'
	],
	'borders' => [
		'allBorders' => [
			'borderStyle' => 'thin',
			'color' => ['rgb' => 'dae3f3']
		]
	]
];

$styleLeftBlue = [
	'font' => [
		'size' => 11,
		'bold' => false,
		'color' => ['rgb' => '000000']
	],
	'fill' => [
		'fillType' => 'solid',
		'color' => ['rgb' => 'dae3f3']
	],
	'alignment' => [
		'horizontal' => 'left',
        'vertical' => 'v-center'
	],
	'borders' => [
		'allBorders' => [
			'borderStyle' => 'thin',
			'color' => ['rgb' => 'dae3f3']
		]
	]
];

$styleRightBlue = [
	'font' => [
		'size' => 11,
		'bold' => false,
		'color' => ['rgb' => '000000']
	],
	'fill' => [
		'fillType' => 'solid',
		'color' => ['rgb' => 'dae3f3']
	],
	'alignment' => [
		'horizontal' => 'right',
        'vertical' => 'v-center'
	],
	'borders' => [
		'allBorders' => [
			'borderStyle' => 'thin',
			'color' => ['rgb' => 'dae3f3']
		]
	]
];


$styleLightBlue = [
	'font' => [
		'size' => 11,
		'bold' => false,
		'color' => ['rgb' => '000000']
	],
	'fill' => [
		'fillType' => 'solid',
		'color' => ['rgb' => 'dae3f3']
	],
	'alignment' => [
		'horizontal' => 'right',
        'vertical' => 'v-center'
	],
	'borders' => [
		'allBorders' => [
			'borderStyle' => 'thin',
			'color' => ['rgb' => 'dae3f3']
		]
	]
];

$styleDarkBlue = [
	'font' => [
		'size' => 11,
		'bold' => false,
		'color' => ['rgb' => '000000']
	],
	'fill' => [
		'fillType' => 'solid',
		'color' => ['rgb' => 'b4c7e7']
	],
	'alignment' => [
		'horizontal' => 'right',
        'vertical' => 'v-center'
	],
	'borders' => [
		'allBorders' => [
			'borderStyle' => 'thin',
			'color' => ['rgb' => 'b4c7e7']
		]
	]
];


$styleCenterPurple = [
	'font' => [
		'size' => 11,
		'bold' => false,
		'color' => ['rgb' => '000000']
	],
	'fill' => [
		'fillType' => 'solid',
		'color' => ['rgb' => 'ccccff']
	],
	'alignment' => [
		'horizontal' => 'center',
        'vertical' => 'v-center'
	],
	'borders' => [
		'allBorders' => [
			'borderStyle' => 'thin',
			'color' => ['rgb' => 'ccccff']
		]
	]
];

$styleLeftPurple = [
	'font' => [
		'size' => 11,
		'bold' => false,
		'color' => ['rgb' => '000000']
	],
	'fill' => [
		'fillType' => 'solid',
		'color' => ['rgb' => 'ccccff']
	],
	'alignment' => [
		'horizontal' => 'left',
        'vertical' => 'v-center'
	],
	'borders' => [
		'allBorders' => [
			'borderStyle' => 'thin',
			'color' => ['rgb' => 'ccccff']
		]
	]
];

$styleRightPurple = [
	'font' => [
		'size' => 11,
		'bold' => false,
		'color' => ['rgb' => '000000']
	],
	'fill' => [
		'fillType' => 'solid',
		'color' => ['rgb' => 'ccccff']
	],
	'alignment' => [
		'horizontal' => 'right',
        'vertical' => 'v-center'
	],
	'borders' => [
		'allBorders' => [
			'borderStyle' => 'thin',
			'color' => ['rgb' => 'ccccff']
		]
	]
];


$styleLightPurple = [
	'font' => [
		'size' => 11,
		'bold' => false,
		'color' => ['rgb' => '000000']
	],
	'fill' => [
		'fillType' => 'solid',
		'color' => ['rgb' => 'ccccff']
	],
	'alignment' => [
		'horizontal' => 'right',
        'vertical' => 'v-center'
	],
	'borders' => [
		'allBorders' => [
			'borderStyle' => 'thin',
			'color' => ['rgb' => 'ccccff']
		]
	]
];

$styleDarkPurple = [
	'font' => [
		'size' => 11,
		'bold' => false,
		'color' => ['rgb' => '000000']
	],
	'fill' => [
		'fillType' => 'solid',
		'color' => ['rgb' => '9b9bff']
	],
	'alignment' => [
		'horizontal' => 'right',
        'vertical' => 'v-center'
	],
	'borders' => [
		'allBorders' => [
			'borderStyle' => 'thin',
			'color' => ['rgb' => '9b9bff']
		]
	]
];

foreach (range('A','X') as $column) {
	$sheet->getColumnDimension($column)->setAutoSize(true);
}
foreach ($templateData['projeto_list'] as $s) {
	$sheet->getStyle("A{$row}")->applyFromArray($styleTitleLeft);
	$sheet->getStyle("B{$row}:I{$row}")->applyFromArray($styleTitleCenter);
	$sheet->SetCellValue("A{$row}", 'DISCIPLINA');
	$sheet->SetCellValue("B{$row}", 'PROJETO');
	$sheet->SetCellValue("C{$row}", 'ÁREA');
	$sheet->SetCellValue("D{$row}", 'SEMANA');
	$sheet->SetCellValue("E{$row}", 'INFORMAÇÃO');
	$sheet->SetCellValue("F{$row}", 'PROGRAMAÇÃO');
	$sheet->SetCellValue("G{$row}", 'MEDIÇÃO');
	$sheet->SetCellValue("H{$row}", 'Cód');
	$sheet->SetCellValue("I{$row}", 'Unid.');
	$row ++;
	foreach ($templateData['processo_list'][$s['CHAVE']] as $p) {
		$tipo_processo = $p['TIPO_PROCESSO'];
		
		$styleHeadCenter = $styleHeadCenterBlue;
		$styleHeadLeft = $styleHeadLeftBlue;
		$styleHeadRight = $styleHeadRightBlue;
		if ($tipo_processo == '1') {
			$styleHeadCenter = $styleHeadCenterBlue;
			$styleHeadLeft = $styleHeadLeftBlue;
			$styleHeadRight = $styleHeadRightBlue;
		} else {
			$styleHeadCenter = $styleHeadCenterPurple;
			$styleHeadLeft = $styleHeadLeftPurple;
			$styleHeadRight = $styleHeadRightPurple;	
		}

		$sheet->getStyle("A{$row}:I{$row}")->applyFromArray($styleHeadCenter);
		$sheet->getStyle("A{$row}")->applyFromArray($styleHeadLeft);
		$sheet->SetCellValue("A{$row}", $p['DISCIPLINA']);
		$sheet->SetCellValue("B{$row}", $p['PROJETO']);
		$sheet->getStyle("C{$row}")->applyFromArray($styleHeadLeft);
		$sheet->SetCellValue("C{$row}", $p['AREA']);
		$sheet->SetCellValue("D{$row}", $p['SEMANA']);
		$sheet->getStyle("E{$row}")->applyFromArray($styleHeadLeft);
		$sheet->SetCellValue("E{$row}", $p['INFORMACAO']);
		$sheet->getStyle("F{$row}:G{$row}")->applyFromArray($styleHeadRight);
		$sheet->SetCellValue("F{$row}", $p['PROGRAMACAO']);
		$sheet->SetCellValue("G{$row}", $p['MEDICAO']);
		$sheet->SetCellValue("H{$row}", $p['CODIGO']);
		$sheet->SetCellValue("I{$row}", $p['UNIDADE']);
		$row++;
		
		foreach ($templateData['modulo_list'][$p['CHAVE']] as $m) {
			$styleCenter = $styleCenterWhite;
			$styleLeft = $styleLeftWhite;
			$styleRight = $styleLightBlue;
			if ($tipo_processo == '1') {
				if (($row%2) == 1) {
					$styleCenter = $styleCenterWhite;
					$styleLeft = $styleLeftWhite;
					$styleRight = $styleLightBlue;
				} else {
					$styleCenter = $styleCenterBlue;
					$styleLeft = $styleLeftBlue;
					$styleRight = $styleDarkBlue;
				}
			} else {
				if (($row%2) == 1) {
					$styleCenter = $styleCenterWhite;
					$styleLeft = $styleLeftWhite;
					$styleRight = $styleLightBlue;
				} else {
					$styleCenter = $styleCenterPurple;
					$styleLeft = $styleLeftPurple;
					$styleRight = $styleDarkPurple;
				}
			}
			$sheet->getStyle("A{$row}:I{$row}")->applyFromArray($styleCenter);
			$sheet->getStyle("A{$row}")->applyFromArray($styleLeft);
			$sheet->SetCellValue("A{$row}", $m['DISCIPLINA']);
			$sheet->SetCellValue("B{$row}", $m['PROJETO']);
			$sheet->getStyle("C{$row}")->applyFromArray($styleLeft);
			$sheet->SetCellValue("C{$row}", $m['AREA']);
			$sheet->SetCellValue("D{$row}", $m['SEMANA']);
			$sheet->getStyle("E{$row}")->applyFromArray($styleLeft);
			$sheet->SetCellValue("E{$row}", $m['INFORMACAO']);
			$sheet->getStyle("F{$row}:G{$row}")->applyFromArray($styleRight);
			$sheet->SetCellValue("F{$row}", $m['PROGRAMACAO']);
			$sheet->SetCellValue("G{$row}", $m['MEDICAO']);
			$sheet->SetCellValue("H{$row}", $m['CODIGO']);
			$sheet->SetCellValue("I{$row}", $m['UNIDADE']);
			$row++;
		}
		$row++;
	}
	$row++;
}

Excel Com Execução em Background

Devido a configurações para melhor desempenho do sistema não recomendamos a gerar um excel que com muitos dados diretamente no navegador, se passou de 2 minutos já é muito tempo para um processo do sistema ficar exclusivo para isso. Também se leva em conta que o cliente pode tentar gerar o mesmo relatório várias vezes.

Dependendo do tempo parar gerar o relatório também não conseguimos dar esse responsabilidade para o Scheduler, porque vai gerar um timeout, porque o se aumentar o timeout dele pode ser que fique muito tempo preso numa mesma fila.

Para solucionar esse problemas estamos utilizando uma aplicação de código aberto chamada de Async Jobs, nele enviamos o relatório para que seja gerado em background.

nfs_excel_bg.png

Acima é representando o fluxo dos dados, que são:

  • É feita a ação de gerar o relatório
  • Dados do relatório são enviados para o Async Jobs e é mostrado um LINK para acessar a tela de Download
  • Após dois minutos depois do envio, é iniciado o processo do relatório
  • E criado um novo registro dizendo que a geração do relatório está em progresso
  • Ao finalizar a tela de Download atualizada e o arquvo é salvo no AWS S3 onde fica disponível para download

Configuração no relatório

No relatório Excel na coluna EXPORT_OPTIONS é necessário adiconar mais uma chave e valor, que é "background":true".

Ex.:

{
	"excel": {
		"template":  ""
	},
	"background":true
}

Create Table

Também é bom verificar se existe a tabela nfs_reports_background, caso não, executar o create table abaixo:

CREATE TABLE `nfs_reports_background` (
  `SEQ_DB` bigint NOT NULL AUTO_INCREMENT,
  `EMPRESA` int DEFAULT NULL,
  `FILIAL` int DEFAULT NULL,
  `LOCAL` int DEFAULT NULL,
  `REPORT_NAME` varchar(200) NOT NULL,
  `FILE_NAME` varchar(300) NOT NULL,
  `INS_DH` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `UPD_DH` timestamp null ON UPDATE CURRENT_TIMESTAMP,
  `USUARIO_SEQ_DB` bigint,
  `PROGRESS_STATUS` tinyint default 0,
  PRIMARY KEY (`SEQ_DB`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

Obter Html Relatório

É possível obter o html de um relatório para ser enviado por e-mail, ou qualquer que seja objetivo.

Para isso é necessário no entry realizar o uso do método getHtmlReport do ReportService:

Descrição do Método

/**
* Obtem HTML do relatório no formato de texto ou array($html , $queryData) caso $returnQueryData = true.
*
* @param string $reportName Nome do Relatório
* @param array $params          Parametros que o relatório utiliza
* @param mixed $useDefaultCss   Se deve usar estilos padrão ou não
* @param mixed $returnQueryData Se retorna html e dados ou somente html
* @param array $recipients      Lista de destinatários
*
* @return array|bool|string
*/
public function getHtmlReport(?string $reportName, ?array $params = [], ?bool $useDefaultCss = true, ?bool $returnQueryData = false, ?array $recipients = [])

Exemplo de Uso

// instanciar a classe ReportService
$reportService = new ReportService();

// Nome do Relatório
$reportName = 'name_of_my_awesome_report';

// Parâmetros do relatório, todos os parametros ilustrados nesse exemplo são fictícios
$params = ['os_offline' => $osOfflineSeqDb,
    'incluir_fotos' => true,
    'incluir_anotacoes' => $hasAnnotation,
    'incluir_horas_trabalhadas' => $hasHoursWorked,
    'incluir_checklist' => $hasChecklist
];

// Usa o CSS padrão, sempre true porque é para usar o nosso CSS, há casos dentro do core que essa flag pode mudar,
// para o uso do método getHtmlReport
$defaulCss = true;

// Parâmetro que permite obter os dados do relatório ao invés do HTML, logo como queremos o HTML ele é false. 
$queryData = false;

// Caso tenhamos a intenção de obter o idioma do destinário, ao invés do Local, estou descrevendo ele mas vc não precisa passar
$recipientes = []; // Se eu quiser obter o idioma de um email de usuário cadastrado no nfs: ['my.awesome@email.com']

$html = $reportService->getHtmlReport($reportName, $params, $defaulCss, $queryData, $recipients);

Nesse exemplo passei todos os parâmetros para todas saberem as propriedades possíveis, mas na maioria dos casos você vai querer passar somente o $reportName e $params:

$html = $reportService->getHtmlReport($reportName, $params, $defaulCss, $queryData, $recipients);

Introdução

A Classe ReportPdfNative foi criada com o objetivo de abstrair a crição de relatórios em formato PDF de forma nativa, dispensando o uso de alguns filtros obrigatórios e permitindo que uma quantidade maior de dados seja renderizada, de maneira separada, dentro de um único documento PDF.

Objetivo e Vantagens

Relatórios criados de forma nativa dispensam a necessidade de uma conversão, todo relatório é construído utilizando apenas linguagem PHP e gerando um arquivo de extensão .PDF como produto final. O desenvolvimento do template é simples e objetivo, sendo necessário apenas configurar as células que irão compor as seções do documento (Cabeçalho, Rodapé e Conteúdo principal). O grande diferencial está na passibilidade de configurar cabeçalhos e rodapés dinâmicos para cada página gerada pelos dados do conteúdo principal.

Criar e Configurar Relatório

A criação do relatório dar-se à partir da instâcia de um objeto da classe ReportPdfNative, através de seu método createPdfNative(). A passagem da configuração do relatório dar-se pelo uso do método configGlobal().

/**
 * Cria objeto ReportPdfNative para construção de relatório em formato PDF.
 */
public static function createPdfNative(): self
/**
 * Define as configurações gerais do documento PDF a ser gerado.
 *
 * @param array	$configGlobal		Dados de configuração gerais do documento.
 *
 * $configGlobal = [
 * 		'title' => ?string = 'título do documento',
 * 		'file_name' => ?string = 'nome do arquivo',
 * 		'style' => [
 *       'title' => [
 *         'font_family' => ?string, nome da família da fonte
 *         'font_style' => ?string, estilo da fonte, 'B', 'I' ou 'U'
 *         'font_size' => ?int, tamanho da fonte
 *         'color' => ?string, cor do texto, em Hexadecimal
 *         'fill' => ?string, cor de preenchimento da célula, em Hexadecimal
 *       ],
 *       'subtitle' => [
 *         'font_family' => ?string, nome da família da fonte
 *         'font_style' => ?string, estilo da fonte, 'B', 'I' ou 'U'
 *         'font_size' => ?int, tamanho da fonte
 *         'color' => ?string, cor do texto, em Hexadecimal
 *         'fill' => ?string, cor de preenchimento da célula, em Hexadecimal
 *       ],
 *       'text' => [
 *         'font_family' => ?string, nome da família da fonte
 *         'font_style' => ?string, estilo da fonte, 'B', 'I' ou 'U'
 *         'font_size' => ?int, tamanho da fonte
 *         'color' => ?string, cor do texto, em Hexadecimal
 *         'fill' => ?string, cor de preenchimento da célula, em Hexadecimal
 *       ],
 *       'array' => [
 *         'font_family' => ?string, nome da família da fonte
 *         'font_style' => ?string, estilo da fonte, 'B', 'I' ou 'U'
 *         'font_size' => ?int, tamanho da fonte
 *         'color' => ?string, cor do texto, em Hexadecimal
 *         'fill' => ?string, cor de preenchimento da célula, em Hexadecimal
 *       ],
 *     ],
 *		'defaultCellHeight'=> ?int = 'altura padrão das células'
 * ];
 */
public function globalSettings(array $configGlobal): void
/**
* Sinaliza o final da configuração de um relatório pdf nativo.
*
* Obs: Quando usado em um loop, sinalizará o fim de relatório e início do próximo.
*/
public function end(): void

Manipulação de Dados

Os dados no relatório são inseridos através do conceito de células. As células são construídas de forma simples, onde deve-se informar sua largura, seu conteúdo, tipo do conteúdo que está sendo passado e seu alinhamento horizontal dentro da célula.

Existem métodos específicos para inserir células nos distintos elementos do documento; por elementos distintos entende-se o cabeçalho (header), o rodapé (footer), e o conteúdo principal (body).

/**
 * Insere uma célula no elemento cabeçalho (header) do arquivo PDF.
 *
 * @param int          $w           Largura da célula em "mm"
 * @param array|string $content     Valor do conteúdo a ser inserido.
 * @param string       $typeContent Identifica o tipo do conteúdo passado à célula
 * @param string       $hAlign      Alinhamento horizontal do texto. 'AlignH::CENTER', 'AlignH::LEFT', 'AlignH::RIGHT'
 * @param string       $vAlign      Alinhamento vertical do texto. 'AlignV::TOP', 'AlignV::CENTER', 'AlignV::BOTTOM'
 */
public function headerCell(int $w, array|string $content, $typeContent, string $hAlign, string $vAlign): self
/**
 * Insere uma célula no elemento rodapé (footer) do arquivo PDF.
 *
 * @param int          $w           Largura da célula em "mm"
 * @param array|string $content     Valor do conteúdo a ser inserido.
 * @param string       $typeContent Identifica o tipo do conteúdo passado à célula
 * @param string       $hAlign      Alinhamento horizontal do texto. 'AlignH::CENTER', 'AlignH::LEFT', 'AlignH::RIGHT'
 * @param string       $vAlign      Alinhamento vertical do texto. 'AlignV::TOP', 'AlignV::CENTER', 'AlignV::BOTTOM'
 */
public function footerCell(int $w, array|string $content, string $typeContent, string $hAlign, string $vAlign): self
/**
 * Insere uma célula no conteúdo principal do arquivo PDF.
 *
 * @param int          $w           largura da célula em "mm"
 * @param array|string $content     conteúdo a ser inserido na célula
 * @param string       $typeContent identifica o tipo do conteúdo para sinalizar qual formatação será aplicada a célula
 * @param string       $hAlign      Alinhamento horizontal do texto. 'AlignH::CENTER', 'AlignH::LEFT', 'AlignH::RIGHT'.
 * @param string       $vAlign      Alinhamento vertical do texto. 'AlignV::TOP', 'AlignV::CENTER', 'AlignV::BOTTOM'.
 */
public function bodyCell(int $w, array|string $content, string $typeContent, string $hAlign, string $vAlign): self

<!> Para preencher os parâmetros que identificam o tipo do conteúdo ($typeContent) e o alinhamento ($align) de uma célula, deve-se utilizar constantes previamente definidas. Ver Referências > Constantes.

<!> Para o parâmetro que define a largura ($w), admite-se um documento do tipo folha A4 com largura máxima de 190mm. Utilize esse valor como referência durante o cálculo do somatório das larguras da células configuradas para criação de uma nova linha.

Formatação de Dados

Durante a montagem do relatório alguns métodos podem ser utilizados para facilitar a manipulação das células, personalização e formatação correta do documento.

 /**
 * Modifica a altura da próxima célula a ser criada.
 *
 * @param int $h Altura da próxima célula a ser criada.
 */
 public function cellHeight(int $h = null): self
/**
 * Finaliza a configuração de uma linha, definindo a linha seguinte do template como local de inserção da próxima célula.
 */
public function nextLine(): self
/**
 * Insere uma quebra de linha do documento, construindo um espaço vazio com a altura desejada.
 *
 * @param int|void	$h		Altura da quebra em "mm". 
 *												Quando vazio/null, o valor será definido pela altura padrão da célula ($configGlobal['defaultCellHeight']).
 */
public function jumpLine(int $h = null): self
/**
 * Insere ao final de cada página, no elemento rodapé (footer), o título do documento informado nas configurações gerais,
 * e, um contador de páginas informando a 'página atual/total de páginas' do relatório, que pode ser desabilitado.
 *
 * @param bool $hasNumbPage    Indica se o contador de páginas deve ser exibido ou não. Possue 'true' como valor default.
 * @param bool $hasTitlePage   Indica se o título do documento deve ser exibido ou não. Possue 'true' como valor default.
 */
public function footerInfo(bool $hasNumbPage = true, bool $hasTitlePage = true): self
/**
 * Ignora a quebra automática do conteúdo quando este ultrapassa a largura da célula.
 */
public function ignoreTextBreak(): self
/**
 * Altera o tamanho da fonte do texto.
 *
 * @param int    $size      Tamanho da fonte.
 */
public function fontSize(int $size): self
/**
 * Altera o estilo do texto
 * Valores possívels: B = BOLD, I = ITALIC, U = UNDERLINE.
 *
 * @param string $style     Estilo do texto.
 */
public function fontStyle(string $style): self
/**
* Altera a cor do texto.
*
* @param string $color cor do texto, valor em hexadecimal
*/
public function textColor(string $color): self
/**
* Altera a cor de preenchimento da célula.
*
* @param string $color cor de preenchimento da célula, valor em hexadecimal
*/
public function fillColor(string $color): self
/**
 * Permite que uma linha contenha um conjuto de células, ou, 'sub-linhas'.
 * Funciona de forma semelhante a um 'rowspan'.
 *
 * @param int $numbSplit número de divisões, ou, sub-linhas a serem inseridas
 */
public function rowSplit(int $numbSplit): self
/**
 * Indica a quebra de linha dentro 'rowSplit', ou, o fim de uma 'sub-linha'.
 */
public function split(): self
/**
* Encerra as configuraçõs do template para um relatório. 
* Utilizar após definir todas as células dos elementos do documento (Header, Footer e Body).
*/
public function end()

<!> Sempre utilize o método 'cellHeight()' antes de criar uma célula do tipo 'IMG_BLOB', de maneira a garantir a altura e a largura da imagem renderizada.

<!> O método 'footerInfo()' deve ser, obrigatoriamente, inserido após todas as células do elemento rodapé (Footer) serem configuradas, pois, ele indica o fim da configuração do elemento rodapé.

<!> Ao usar o método 'rowSplit()', o valor passado no parâmetro $numbSplit deve ser igual ao número de vezes em que o método 'split()' aparece no template, de forma a indicar corretamente o fim do split criado.

Referências

Constantes

/** 
 * Para definir o tipo do conteúdo, use: 
 */
 Format::TEXT				# Célula contendo uma string configurada em $configGlobal['style']['text'] e que não possui cor de preenchimento padrão. 
 Format::TITLE 			# Célula contendo uma string configurada em $configGlobal['style']['title']
 Format::SUBTITLE		# Célula contendo uma string configurada em $configGlobal['style']['subtitle']
 Format::ARRAY			 # Célula contendo um Array de dados configurada em $configGlobal['style']['array'], onde cada item do array ocupará 'uma linha' dentro de uma mesma célula.
 Format::IMG_BLOB		# Célula que recebe uma string contendo o binário de uma imagem, aceitará binários que estão ou não em base64.
 Format::LOGO				# Célula que recebe uma string contendo o nome do arquivo de imagem da logo da empresa armazenado no diretório da aplicação.
/**
 * Para definir o alimento horizontal, use:
 */
 AlignH::CENTER			# Alinhar ao centro.
 AlignH::LEFT				# Alinhar a borda esquerda.
 AlignH::RIGHT			# Alinhar a borda direita.
/**
 * Para definir o alimento vertical, use:
 */
 AlignV::CENTER			# Alinhar ao centro.
 AlignV::TOP				# Alinhar a borda superior.
 AlignV::BOTTOM			# Alinhar a borda inferior.

<!> A lista completa com o nome das imagem de logo das empresas disponíveis no diretório da aplicação, em caso de uso do Format::LOGO, encontra-se nesta documentação em Referências > Logo Empresas.

<!> O parâmetro referente ao alinhamento não se aplica ao conteúdo do tipo imagem, neste caso, é possível apenas não passar este parâmetro.

<!> Para passar os dados para dentro do array das céluldas do tipo ARRAY, é necessário criar uma variável do tipo vetor associativo (array chave/valor), com as keys ‘value' e ‘style’, sendo a primeira obrigatória contendo a string/variável a ser renderizada, e, a segunda key será opcional e irá conter a personalização do conteúdo em questão, passando as configs conforme o padrão 'style' da $configGeral['style']['...'].

Logo Empresas

Lista dos nomes dos arquivos das logos das empresas disponíveis diretamente no diretório do NFS_CORE para utilizar nas células do tipo 'LOGO'.

  • agro_baggio.png :: Agro Baggio John Deere
  • bracos.png :: Braços Construções e Instalações de Gás
  • camargocorrea.png :: Construtora Camargo Corrêa
  • camargocorreainfra.png :: Camargo Corrêa Infra
  • case.png :: Case IH Agriculture
  • comgev.png :: Consórcio COMGEV
  • comid.png :: Comid John Deere
  • concremat.jpg :: Concremat Soluções Integradas de Engenharia
  • concremat.png :: Concremat Manutenção
  • eldorado.png :: Eldorado Brasil
  • enasa.png :: Enesa
  • energas.png :: Energás
  • fibria.png :: Fibria
  • fibria.jpg :: Fibria
  • gas_natural_fenosa.gif :: Gas Natural Fenosa
  • gbec.png :: Grupo Transtusa GBEC
  • ge.png :: GE Brasil
  • grupo_agis_branco.jpg :: Grupo AGIS
  • grupoagis.png :: Grupo AGIS
  • invepar.png :: Invepar Rodovias
  • jalles.png :: Jalles Machado
  • jd.png :: John Deere
  • jm.png :: Construtora JM Ltda
  • john_deere64.png :: John Deere
  • lavoro.png :: Lavoro John Deere
  • meridional.png :: Meridional John Deere
  • new_holland.png :: New Holland Agriculture
  • nip.png :: NIPBR Nipcable
  • novatec.png :: Novatec
  • portonovo.png :: Concessionária Porto Novo
  • primavera.jpg :: Primavera John Deere
  • real_guindaste.png :: Real Guindastes e Equipamentos
  • real.png :: Real Guindastes e Equipamentos
  • santa_fe.png :: Usina Santa Fé
  • santa_fe64.png :: Usina Santa Fé
  • socicam.jpg :: Socicam
  • suzano.png :: Suzano Papel e Celulose
  • tamoios.png :: Tamoios
  • via040.png :: VIA040

Dicas e Conceitos Importantes

Agumas DICAS e CONCEITOS para facilitar o processo de montagem do template que dará origem ao relatório pdf.

<!> Na construção do template é possível utilizar todas as funções PHP nativas. Faça uso de estruturas condicionais e de laços de repetição para manipular seus dados conforme necessário.

<!> Utilize do encadeamento de métodos. Crie as células de uma única linha em uma única chamada, facilitando a organização do código. Lembre-se de utilizar o método nextLine() para informar o fim daquela linha.

<!> A ordem de configuração é importante. É essencial que seja definido primeiro os elementos HEADER (cabeçalho) e FOOTER (rodapé), pois eles delimitam o espaço máximo disponível para renderizar corretamente o BODY (conteúdo principal).

<!> Os relatórios nativos são renderizados em papel A4 297x210mm, devido aos 10mm de margem configurado por padrão, tem-se 190mm de máxima largura (ou soma de larguras, em caso de diversas células) para uma única linha.

<!> A quebra de páginas ocorre de forma automática com base no tamanho do conteúdo da próxima linha. Sempre será inserido um cabeçalho de um rodapé para cada página criada.

<!> Utilize das funções isset() e empty() para validação de dados sempre que possível. Evite passagem de valores NULL ou VAZIOS para o CORE, isso pode afetar o desempenho e a formatação do seu documento PDF.

Casos de Uso

Criação, Preenchimento e Formatação - Exemplo Detalhado

Abaixo a construção de um relatório pdf baseado em um template real (Brasfels-dev.h -> EFL=[1-1-6] -> "relatorio_inspecao").

/* CONFIG SECTION */
$pdfNative = ReportPdfNative::createPdfNative();
$pdfNative->globalSettings([
	'title' => 'Inspeção / Suportes',
	'file_name' => $templateData['report_name'],
	'style' => [
		'title' => [
			'font_family' => 'Arial',
			'font_style' => 'B',
			'font_size' => 8,
			'color' => '#000000',
			'fill' => '#cccccc'
		],
		'subtitle' => [
			'font_family' => 'Arial',
			'font_style' => 'B',
			'font_size' => 7,
			'color' => '#000000',
			'fill' => '#f2f2f2'
		],
		'text' => [
			'font_family' => 'Arial',
			'font_style' => '',
			'font_size' => 7,
			'color' => '#000000',
			'fill' => '#ffffff'
		],
		'array' => [
			'font_family' => 'Arial',
			'font_style' => '',
			'font_size' => 7,
			'color' => '#000000',
			'fill' => '#ffffff'
		],
	],
	'defaultCellHeight'=> 5
]);
			
foreach($templateData['dados_check'] as $check) {
	$dados_suporte_rec = $templateData['dados_eng_suporte'][$check['SEQ_DB']][0] ?? null;
	/* HEADER SECTION */
	$pdfNative->cellHeight(20)->headerCell(70, $templateData['images']['logo']['image_base64'], Format::IMG_BLOB)
		->fontSize(12)->fontStyle('B')->headerCell(120, $check['CHECK_QUESTIONARIO'], Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->nextLine();
	$pdfNative->headerCell(10, 'Nº', Format::SUBTITLE, AlignH::LEFT, AlignV::CENTER)
		->headerCell(40, $check['NUMERO_RELATORIO'], Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->headerCell(20, 'OBRA/ SITE:', Format::SUBTITLE, AlignH::LEFT, AlignV::CENTER)
		->headerCell(40, $check['OBRA'], Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->headerCell(20, 'DATA/ DATE:', Format::SUBTITLE, AlignH::LEFT, AlignV::CENTER)
		->headerCell(60, $check['INI_DH'], Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->nextLine();		
	$pdfNative->headerCell(30, 'CLIENTE/ COMPANY:', Format::SUBTITLE, AlignH::LEFT, AlignV::CENTER)
		->headerCell(60, $check['CLIENTE'], Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->headerCell(40, 'PROJETO/ PROJECT:', Format::SUBTITLE, AlignH::LEFT, AlignV::CENTER)
		->headerCell(60, $check['PROJETO'], Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->nextLine();
	
	/* FOOTER SECTION */
	$pdfNative->footerCell(190, 'ASSINATURA/ SIGNATURE', Format::TITLE, AlignH::CENTER, AlignV::CENTER)
		->nextLine();
	$pdfNative->footerCell(50, 'INSPETOR/ INSPECTOR', Format::SUBTITLE, AlignH::CENTER, AlignV::CENTER)
		->footerCell(50, 'SUPERVISOR/ SUPERVISOR', Format::SUBTITLE, AlignH::CENTER, AlignV::CENTER)
		->footerCell(50, 'CLASSIFICADORA/ THIRD PART', Format::SUBTITLE, AlignH::CENTER, AlignV::CENTER)
		->footerCell(40, 'CLIENTE/ CLIENT', Format::SUBTITLE, AlignH::CENTER, AlignV::CENTER)
		->nextLine();
	$pdfNative->footerCell(50, 'Data: '.$check['DATA_INSPECAO'], Format::TEXT, AlignH::LEFT, AlignV::CENTER)
		->footerCell(50, 'Data: '.$check['DATA_APROVACAO'], Format::TEXT, AlignH::LEFT, AlignV::CENTER)
		->footerCell(50, 'Data: '.$check['DATA_APROVACAO_FABRICANTE'], Format::TEXT, AlignH::LEFT, AlignV::CENTER)
		->footerCell(40, 'Data: '.$check['DATA_APROVACAO_CLIENTE'], Format::TEXT, AlignH::LEFT, AlignV::CENTER)
		->nextLine();
	
	$preencheu_usuario_aprovacao = 'NA';
	if(isset($check['USUARIO_APROVACAO']) && !empty($check['USUARIO_APROVACAO'])){
		$preencheu_usuario_aprovacao = '';
	}
	$preencheu_usuario_aprovacao_fabricante = 'NA';
	if(isset($check['USUARIO_APROVACAO_FABRICANTE']) && !empty($check['USUARIO_APROVACAO_FABRICANTE'])){
		$preencheu_usuario_aprovacao_fabricante = '';
	}
	$preencheu_usuario_aprovacao_cliente = 'NA';
	if(isset($check['USUARIO_APROVACAO_CLIENTE']) && !empty($check['USUARIO_APROVACAO_CLIENTE'])){
		$preencheu_usuario_aprovacao_cliente = '';
	}
	
	$pdfNative->cellHeight(20)->footerCell(50, $check['ASSINATURA_INSPETOR'], Format::IMG_BLOB)
		->footerCell(50, $preencheu_usuario_aprovacao, Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->footerCell(50, $preencheu_usuario_aprovacao_fabricante, Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->footerCell(40, $preencheu_usuario_aprovacao_cliente, Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->nextLine();
	
	$array1 = [];
		if (isset($check['EFETIVO_FUNCIONARIO'])) {
			$data = [
				'value' => $check['EFETIVO_FUNCIONARIO']
			];
			array_push($array1, $value);
		}
		if (isset($check['EFETIVO_FUNCAO'])) {
			$data = [ 
				'value'=> $check['EFETIVO_FUNCAO']
			];
			array_push($array1, $value);
		}
		if (isset($check['CERTIFICACAO_INSPETOR'])) {
			$data = [ 
				'value'=> 'Certificações (SNCC, IS): '.$check['CERTIFICACAO_INSPETOR']
			];
			array_push($array1, $value);
		}
		if (isset($check['CRACHA_FUNCIONARIO'])) {
			$data = [ 
				'value'=> 'Matrícula: '.$check['CRACHA_FUNCIONARIO']
			];
			array_push($array1, $value);
		}
		$data = [ 
			'value'=> 'Estaleiro BrasFELS Ltda.'
		];
		array_push($array1, $value);
	
	$array2 = [];
		if (isset($check['NOME'])) {
			$data = [ 
				'value'=> $check['NOME']
			];
			array_push($array2, $data);
		}
		if (isset($check['CARGO'])) {
			$data = [ 
				'value'=> $check['CARGO']
			];
			array_push($array2, $data);
		}
		if (isset($check['USUARIO_APROVACAO'])) {
			$data = [ 
				'value'=> $check['USUARIO_APROVACAO']
			];
			array_push($array2, $data);
		}
		if (isset($check['MATRICULA'])) {
			$data = [ 
				'value'=> 'Matrícula: '.$check['MATRICULA']
			];
			array_push($array2, $data);
		}	
		if(isset($check['USUARIO_APROVACAO']) && !empty($check['USUARIO_APROVACAO'])){
			$data = [ 
				'value'=> 'Estaleiro BrasFELS Ltda.'
			];
			array_push($array2, $data);
			
			$data = [ 
				'value'=> 'Aprovado eletronicamente'
			];
			array_push($array2, $data);
		}
	
	$array3 = [];
		if (isset($check['NOME_FABRICANTE'])) {
			$data = [ 
				'value'=> $check['NOME_FABRICANTE']
			];
			array_push($array3, $data);
		}
		if (isset($check['CARGO_FABRICANTE'])) {
			$data = [ 
				'value'=> $check['CARGO_FABRICANTE']
			];
			array_push($array3, $data);
		}
		if (isset($check['USUARIO_APROVACAO_FABRICANTE'])) {
			$data = [ 
				'value'=> $check['USUARIO_APROVACAO_FABRICANTE']
			];
			array_push($array3, $data);
		}
		if(isset($check['MATRICULA_FABRICANTE'])){
			$data = [ 
				'value'=> 'Matrícula: '.$check['MATRICULA_FABRICANTE']
			];
			array_push($array3, $data);
		}
		if(isset($check['USUARIO_APROVACAO_FABRICANTE']) && !empty($check['USUARIO_APROVACAO_FABRICANTE'])){
			$data = [ 
				'value'=> 'Estaleiro BrasFELS Ltda.'
			];
			array_push($array3, $data);
			
			$data = [ 
				'value'=> 'Aprovado eletronicamente'
			];
			array_push($array3, $data);
		}

	$array4 = [];
		if (isset($check['NOME_CLIENTE'])) {
			$data = [ 
				'value'=> $check['NOME_CLIENTE']
			];
			array_push($array4, $data);
		}
		if (isset($check['CARGO_CLIENTE'])) {
			$data = [ 
				'value'=> $check['CARGO_CLIENTE']
			];
			array_push($array4, $data);
		}
		if (isset($check['USUARIO_APROVACAO_CLIENTE'])) {
			$data = [ 
				'value'=> $check['USUARIO_APROVACAO_CLIENTE']
			];
			array_push($array4, $data);
		}
		if(isset($check['MATRICULA_CLIENTE'])){
			$data = [ 
				'value'=> 'Matrícula: '.$check['MATRICULA_CLIENTE']
			];
			array_push($array4, $data);
		}
		if(isset($check['USUARIO_APROVACAO_CLIENTE']) && !empty($check['USUARIO_APROVACAO_CLIENTE'])){
			$data = [ 
				'value'=> 'Estaleiro BrasFELS Ltda.'
			];
			array_push($array4, $data);
			
			$data = [ 
				'value'=> 'Aprovado eletronicamente'
			];
			array_push($array4, $data);
		}
	
	$pdfNative->footerCell(50, $array1, Format::ARRAY, AlignH::CENTER, AlignV::CENTER)
		->footerCell(50, $array2, Format::ARRAY, AlignH::CENTER, AlignV::CENTER)
		->footerCell(50, $array3, Format::ARRAY, AlignH::CENTER, AlignV::CENTER)
		->footerCell(40, $array4, Format::ARRAY, AlignH::CENTER, AlignV::CENTER)
		->nextLine();
	
	$pdfNative->footerInfo(true,false);
	
	/* BODY SECTION */
	$pdfNative->bodyCell(20, 'Nº MÓDULO/ MODULE', Format::SUBTITLE, AlignH::CENTER, AlignV::CENTER)
		->bodyCell(20, 'Nº PAINEL/ PANEL', Format::SUBTITLE, AlignH::CENTER, AlignV::CENTER)
		->bodyCell(40, 'DISCIPLINA/ DISCIPLINE', Format::SUBTITLE, AlignH::CENTER, AlignV::CENTER)
		->bodyCell(30, 'TIPO/ TYPE', Format::SUBTITLE, AlignH::CENTER, AlignV::CENTER)
		->bodyCell(40, 'LOCAL/ LOCAL', Format::SUBTITLE, AlignH::CENTER, AlignV::CENTER)
		->bodyCell(40, 'Requer Inspeção Oficial?', Format::SUBTITLE, AlignH::CENTER, AlignV::CENTER)
		->nextLine();

	$pdfNative->bodyCell(20, $dados_suporte_rec["MODULO"], Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->bodyCell(20, '---', Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->bodyCell(40, $dados_suporte_rec["DISCIPLINA"], Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->bodyCell(30, 'Montagem', Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->bodyCell(40, $dados_suporte_rec["LOCAL_NOME"], Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->bodyCell(40, '', Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->nextLine();
		
	$desenho_desc = $dados_suporte_rec["DESENHO"] ?? '';
	$rev_desc = !empty($dados_suporte_rec["REV"]) ? ' / ' .$dados_suporte_rec["REV"] : '';
	
	$desenho_rev = $desenho_desc.$rev_desc;	
	$pdfNative->bodyCell(190, 'Nº DESENHO:/ DRAWING:', Format::SUBTITLE, AlignH::LEFT, AlignV::CENTER)
	->nextLine();
	$pdfNative->bodyCell(190, $desenho_rev, Format::TEXT, AlignH::LEFT, AlignV::CENTER)
	->nextLine();
	
	$pdfNative->bodyCell(20, 'CV./ FR.', Format::SUBTITLE, AlignH::LEFT, AlignV::CENTER)
		->bodyCell(75, '-', Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->bodyCell(20, 'até CV', Format::SUBTITLE, AlignH::LEFT, AlignV::CENTER)
		->bodyCell(75, '-', Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->nextLine();
	$pdfNative->bodyCell(20, 'EL', Format::SUBTITLE, AlignH::LEFT, AlignV::CENTER)
		->bodyCell(75, '-', Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->bodyCell(20, 'até EL', Format::SUBTITLE, AlignH::LEFT, AlignV::CENTER)
		->bodyCell(75, '-', Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->nextLine();
	$pdfNative->bodyCell(20, 'LONG:', Format::SUBTITLE, AlignH::LEFT, AlignV::CENTER)
		->bodyCell(75, '-', Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->bodyCell(20, 'até LONG:', Format::SUBTITLE, AlignH::LEFT, AlignV::CENTER)
		->bodyCell(75, '-', Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->nextLine();
	$pdfNative->bodyCell(20, 'BOMBORDO (PORT)', Format::SUBTITLE, AlignH::CENTER, AlignV::CENTER)
		->bodyCell(75, 'BORESTE (STBD)', Format::SUBTITLE, AlignH::CENTER, AlignV::CENTER)
		->bodyCell(20, 'VANTE (FWD)', Format::SUBTITLE, AlignH::CENTER, AlignV::CENTER)
		->bodyCell(35, 'RÉ (AFT)', Format::SUBTITLE, AlignH::CENTER, AlignV::CENTER)
		->bodyCell(40, 'CENTRO (CENTER)', Format::SUBTITLE, AlignH::CENTER, AlignV::CENTER)
		->nextLine();
	$pdfNative->bodyCell(20, '-', Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->bodyCell(75, '-', Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->bodyCell(20, '-', Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->bodyCell(35, '-', Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->bodyCell(40, '-', Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->nextLine();	
				
	$tipo_produto_desc = !empty($dados_suporte_rec["TIPO_PRODUTO"]) ? 'SUPORTE PARA ' .$dados_suporte_rec["TIPO_PRODUTO"] : '';
	$pdfNative->bodyCell(190, 'DESCRIÇÃO DA SOLICITAÇÃO:/ DESCRIPTION OF THE REQUEST:', Format::SUBTITLE, AlignH::LEFT, AlignV::CENTER)
		->nextLine();	
	$pdfNative->bodyCell(190, $tipo_produto_desc, Format::TEXT, AlignH::LEFT, AlignV::CENTER)
		->nextLine();
	
	$qtde_total = '';
	$peso_total = '';
	foreach($templateData['dados_eng_suporte'] as $eng_suporte) {
		if ($eng_suporte['CHECK_APT_SEQ_DB'] == $check['SEQ_DB']) {
			$qtde_total = $qtde_total + $eng_suporte['QTDE_1'];
			$peso_total = $peso_total + $eng_suporte['PESO_UNIT_1'];
		}
	}
	if ($qtde_total != '') {
		$qtde_total = $qtde_total.' Un';
	}
	if ($peso_total != '') {
		$peso_total = $peso_total.' Kg';
	}
	$pdfNative->bodyCell(40, 'QTDE. TOTAL:', Format::SUBTITLE, AlignH::LEFT, AlignV::CENTER)
		->bodyCell(150, $qtde_total, Format::TEXT, AlignH::LEFT, AlignV::CENTER)
		->nextLine();
	$pdfNative->bodyCell(40, 'PESO TOTAL:', Format::SUBTITLE, AlignH::LEFT, AlignV::CENTER)
		->bodyCell(150, $peso_total, Format::TEXT, AlignH::LEFT, AlignV::CENTER)
		->nextLine();
	$pdfNative->bodyCell(190, 'EM ANEXO/ ATTACHED.', Format::SUBTITLE, AlignH::LEFT, AlignV::CENTER)
		->nextLine();
	$pdfNative->bodyCell(40, 'SINETE RAÍZ: ROOT SIGNET:', Format::SUBTITLE, AlignH::LEFT, AlignV::CENTER)
		->bodyCell(150, '-', Format::TEXT, AlignH::LEFT, AlignV::CENTER)
		->nextLine();
	$pdfNative->bodyCell(40, 'SINETE ENCH/ ACAB: FINISH SIGNET:', Format::SUBTITLE, AlignH::LEFT, AlignV::CENTER)
		->bodyCell(150, $check['NUMERO_SINETE'], Format::TEXT, AlignH::LEFT, AlignV::CENTER)
		->nextLine();
	$pdfNative->bodyCell(40, 'EQUIPAMENTOS INSPEÇÕES:', Format::SUBTITLE, AlignH::LEFT, AlignV::CENTER)
		->bodyCell(150, '-', Format::TEXT, AlignH::LEFT, AlignV::CENTER)
		->nextLine();
		
	
	$pdfNative->jumpLine();	
	
	if ( isset($templateData['dados_check_grupo'][$check['SEQ_DB_DEVICE']]) ) {
		foreach($templateData['dados_check_grupo'][$check['SEQ_DB_DEVICE']] as $grupo) {
			$pdfNative->bodyCell(190, $grupo["DESCRICAO"], Format::TITLE, AlignH::CENTER, AlignV::CENTER)
				->nextLine();

			$pdfNative->bodyCell(50, 'QUESTOES/ QUESTIONS', Format::SUBTITLE, AlignH::CENTER, AlignV::CENTER)
				->bodyCell(30, 'RESPOSTA/ ANSWER', Format::SUBTITLE, AlignH::CENTER, AlignV::CENTER)
				->bodyCell(50, 'OBSERVAÇÃO/ OBSERVATION', Format::SUBTITLE, AlignH::CENTER, AlignV::CENTER)
				->bodyCell(60, 'FOTO/ PHOTO', Format::SUBTITLE, AlignH::CENTER, AlignV::CENTER)
				->nextLine();

			foreach($templateData['dados_check_resposta'][$grupo['CHAVE']] as $resposta) {
				$h = isset($resposta['FOTO']) ? 100 : null;
				$pdfNative->bodyCell(50, $resposta['CHECK_QUESTAO'], Format::TEXT, AlignH::LEFT, AlignV::CENTER)
					->bodyCell(30, $resposta['CHECK_TIPO_RESPOSTA'], Format::TEXT, AlignH::LEFT, AlignV::CENTER)
					->bodyCell(50, $resposta['OBSERVACAO'], Format::TEXT, AlignH::LEFT, AlignV::CENTER)
					->cellHeight($h)->bodyCell(60, $resposta['FOTO'], Format::IMG_BLOB)
					->nextLine();
			}
		}
		$pdfNative->jumpLine();
	}
	
	$pdfNative->bodyCell(190, 'OBSERVAÇÃO/ OBSERVATION', Format::TITLE, AlignH::CENTER, AlignV::CENTER)
		->nextLine();
	$pdfNative->bodyCell(190, $check["OBSERVACAO"], Format::TEXT, AlignH::CENTER, AlignV::CENTER)
		->nextLine();
	
	if ( isset($check["MOTIVO_REPROVACAO"]) && !empty($check["MOTIVO_REPROVACAO"])) {
		$pdfNative->bodyCell(190, $check["MOTIVO_REPROVACAO"], Format::TEXT, AlignH::CENTER, AlignV::CENTER)
			->nextLine();
	}

	if ( isset($templateData['dados_eng_suporte'][$check['SEQ_DB']]) ) {
		$pdfNative->jumpLine();
		$pdfNative->bodyCell(190, 'ANEXO/ ATTACHED', Format::TITLE, AlignH::CENTER, AlignV::CENTER)
			->nextLine();
			
		$pdfNative->bodyCell(30, 'TAG', Format::SUBTITLE, AlignH::CENTER, AlignV::CENTER)
			->bodyCell(40, 'Material', Format::SUBTITLE, AlignH::CENTER, AlignV::CENTER)		
			->bodyCell(30, 'Tipo de Produto', Format::SUBTITLE, AlignH::CENTER, AlignV::CENTER)		
			->bodyCell(30, 'Elev. Projeto', Format::SUBTITLE, AlignH::CENTER, AlignV::CENTER)
			->bodyCell(30, 'Qtde. Unit.', Format::SUBTITLE, AlignH::CENTER, AlignV::CENTER)
			->bodyCell(30, 'Peso Unit.', Format::SUBTITLE, AlignH::CENTER, AlignV::CENTER)
			->nextLine();
		
		foreach($templateData['dados_eng_suporte'][$check['SEQ_DB']] as $anexo) {
		
			$pdfNative->bodyCell(30, $anexo['TAG'], Format::TEXT, AlignH::CENTER, AlignV::CENTER)			
				->bodyCell(40, $anexo['MATERIAL'], Format::TEXT, AlignH::CENTER, AlignV::CENTER)
				->bodyCell(30, $anexo['TIPO_PRODUTO'], Format::TEXT, AlignH::CENTER, AlignV::CENTER)		
				->bodyCell(30, $anexo['ELEV_PROJETO'], Format::TEXT, AlignH::CENTER, AlignV::CENTER)
				->bodyCell(30, $anexo['QTDE_1'], Format::TEXT, AlignH::CENTER, AlignV::CENTER)
				->bodyCell(30, $anexo['PESO_UNIT_1'], Format::TEXT, AlignH::CENTER, AlignV::CENTER)
				->nextLine();
		}
	}
	/*END SECTION*/
	$pdfNative->end();
}

<!> Atentar-se as colunas 'TEMPLATE', onde o relaório deve ser descrito por completo seguindo a ordem de preenchimento conforme exposto nesse guia, e, a coluna 'EXPORT_OPTION' a qual deve explicitar o formato de exportação como "pdfNative".

Twig

Vídeos para ajuda

NFS - Relatórios com NFSQueryBuilder e Entry Point - Parte 1 de 3

NFS - Relatórios com NFSQueryBuilder e Entry Point - Parte 2 de 3

NFS - Relatórios com NFSQueryBuilder e Entry Point - Parte 3 de 3

NFS - Usando twig filters e preenchimento automático de data no filtro

Configuração tabela nfs_reports

O nome da tabela é nfs_reports.

ColunaSignificadoExemplos
NAMENome utilizado na url para localizar o relatório-
DISPLAY_NAMENome utilizado para exibição na tela de filtros e no relatório caso o mesmo use o template default-
FILTERSFiltros utilizados para gerar o relatórioExemplo do uso de filtros
QUERIESSelects feitos no banco de dadosExemplo do uso de QUERIES
TEMPLATETemplate do relatório utilizando HTML e TwigDetalhes de configuração de um Template
ENABLEDValores possíveis: 1 ou 0. Habilita/desabilita o relatório-
EXPORT_OPTIONSOpções de impressão do relatório Lista de opções da exportação para pdfExemplo de EXPORT_OPTIONS

Exemplo FILTERS

Filtros utilizados para gerar o relatório

Exemplo:

{
  "os": {
    "type": "Table",
    "options": {
      "label": "OS",
      "multiple": true
    }
  },
  "incluir_fotos": {
    "type": "Checkbox",
    "options": {
      "label": "Incluir fotos?",
      "required": false
    }
  },
  "inicio": {
    "type": "Datetime",
    "options": {
      "label": "De",
      "required": false
    }
  },
  "fim": {
    "type": "Date",
    "options": {
      "label": "Até",
      "required": false
    }
  }
}

Parâmetros:

Todos os filtros devem ter um nome, definido pela chave do objeto JSON.

Caso o filtro seja do tipo Table, o nome deve ser uma tabela válida do NFS (sem o prefixo app_)

type: Tipo do Filtro.

Tipos testados: Table, Date, Checkbox, Datetime

Outros tipos: https://symfony.com/doc/current/reference/forms/types.html

options: Opções de configuração do filtro

  • label: Texto que aparece ao lado do componente
  • required: Define se o filtro é obrigatório ou não. Default: true
  • multiple: Utilizado apenas para os tipos de filtro Table e Choice. Define se é possível selecionar mais de um item. Default: false

Mais de um filtro na mesma tabela

As vezes tem a necessidade de criar mais de um filtro num relatório com a mesma tabela, um exemplo claro é no caso de Técnico e Responsável, ambos estão na mesma tabela de app_funcionario, porém com funções diferentes, para isso tem uma opção chamada table e seu valor é a tabela a ser utilizada.

Exemplo:

{
  "responsavel": {
    "type": "Table",
    "table": "funcionario",
    "options": {
      "label": "Responsável",
      "multiple": true,
      "required": false,
      "attr": {
        "data-innerjoin": "funcionario:funcao:funcionario.funcao_seq_db = funcao.seq_db and funcao.tipo = 'RESPONSAVEL'"
      }
    }
  },
  "tecnico": {
    "type": "Table",
    "table": "funcionario",
    "options": {
      "label": "Técnico",
      "multiple": true,
      "required": false,
      "attr": {
        "data-innerjoin": "funcionario:funcao:funcionario.funcao_seq_db = funcao.seq_db and funcao.tipo = 'TECNICO'"
      }
    }
  }
}

Para utilizar no relatório no QUERIES é a mesma coisa :responsavel e :tecnico.

Inner join em um filtro

Foi criada a opção de configurar um inner join na busca do filtro para atender quando um dado para ser exibido depende de uma flag que está em outra tabela. Por exemplo:

No filtro de funcionario (Tabela funcioanrio), é necessário filtrar todos que sejam da equipe (Funcionario tem uma Equipe) de desenvolvimento (Na equipe tem a flag_dev).

O Filtro ficaria da seguinte forma:

{
  "funcionario": {
    "type": "Table",
    "options": {
      "label": "Funcionário",
      "multiple": true,
      "required": false,
      "attr": {
        "data-innerjoin": "funcionario:equipe:funcionario.equipe_seq_db = equipe.seq_db and equipe.flag_dev = 1"
      }
    }
  }
}

Como é possível observar somente foi necessário criar a chava attr e dentro dela o data-innerjoin que tem os seguintes parametros:

"data-innerjoin" : "tabela:innerjoin:condicao"

Com isso é feito um inner join com Equipe e retornada somente os funcionários quando a equipe tem a flag_dev igual a 1.

Filtrar por dados ativos e inativos

Para buscar por um registro ativo e inativo num filtro, é necessário adicionar a configuração "data-inactives" : "true" na chave attr dentro de options, exemplo:

{
  "funcionario": {
    "type": "Table",
    "options": {
      "label": "Técnico",
      "multiple": true,
      "required": false,
      "attr": {
        "data-inactives": "true"
      }
    }
  }
}

No caso acima será buscado tanto por funcionários ativos e inativos.

Filtro LOV1 ou Choice com um valor padrão

Caso seja necessário ter no filtro um valor padrão, que nesse caso quando não for selecionado nenhum item esse valor que prevalecera. Para isso é necessário adicionar dentro do options

"attr": {
    "data-default_item" : 1
}

Exemplo

{
  "tipo": {
    "type": "Choice",
    "options": {
      "label": "Tipo de Inconformidade",
      "choices": {
        "Inconformidade não resolvida": "1",
        "Inconformidade resolvida": "2",
        "Todas as inconformidades": "3"
      },
      "attr": {
        "data-default_item": 1
      }
    }
  }
}

No caso quando a label é igual a Tipo de Inconformidade e não for selecionado vai ser enviado com o valor igual a 1 e se tiver filtros nas queries usando o tipo será passado esse valor caso nenhum item do select seja escolhido.

Exemplo QUERIES

Exemplo:

{
  "os_list": {
    "main": true,
    "table": "os",
    "with": ["cliente:descricao,codigo", "funcionario:nome"],
    "where": ["os.SEQ_DB in (:os)", "os.FUNCIONARIO_SEQ_DB in (:funcionario)"]
  },
  "apt_servicos_deslocamentos": {
    "select": [
      "MIN(aps.OS_SEQ_DB) OS_SEQ_DB",
      "MIN(aps.INI_DH) INI_DH",
      "MIN(aps.FIM_DH) FIM_DH",
      "MIN(s.DESCRICAO) SERVICO_DESCRICAO",
      "MIN(f.NOME) FUNCIONARIO_NOME",
      "GROUP_CONCAT(a.NOME SEPARATOR ',\n') AUXILIAR_NOME",
      "((COUNT(aps.INI_FIM_DIFF_SEC) + 1) * MIN(aps.INI_FIM_DIFF_SEC)) INI_FIM_DIFF_SEC"
    ],
    "from": "apontamento_servico aps",
    "inner_join": [
      ["aps", "servico", "s", "s.SEQ_DB = aps.SERVICO_SEQ_DB"],
      ["aps", "funcionario", "f", "f.SEQ_DB = aps.FUNCIONARIO_SEQ_DB"],
      ["aps", "funcionario", "a", "a.SEQ_DB = aps.AUXILIAR_SEQ_DB"]
    ],
    "where": [
      "aps.OS_SEQ_DB in (:os)",
      "aps.FUNCIONARIO_SEQ_DB <> aps.AUXILIAR_SEQ_DB",
      "aps.FLAG_DESLOCAMENTO = 1"
    ],
    "group_by": [["FIM_DH"]],
    "order_by": [["INI_DH", "ASC"]],
    "group_result_by": "OS_SEQ_DB"
  }
}

Parâmetros:

Todos os selects devem ter um nome, definido pela chave do objeto JSON. O nome é utilizado nos templates para exibir os resultados. Caso seja utilizado o template padrão, apenas um select deve estar configurado com o nome rows.

main: Caso seja configurado como true, o select é marcado como principal e se não retornar nenhum resultado, o relatório não é gerado.

table: Tabela do NFS (sem o prefixo app_). O select busca todos os campos desta tabela exceto os dos seguintes tipos:

  • LOVN
  • LOVX
  • LINK
  • GPS_POINT

with: Faz left join com tabelas dos campos FKs

select: Define os campos usados no select. Não pode ser usado junto com a opção table; É usado quando é necessário especificar quais campos o select deve trazer ou para usar funções do mysql como SUM e MIN.

from: Usado em conjunto com a opção select. Define a tabela do select (também sem o prefixo app_). Um alias pode ser ser especificado, separado por um espaço. Ex: “tabela alias”.

inner_join: Define um array com as tabelas de inner join. Cada inner join deve ser um array com 4 valores:

  • alias da tabela do from ou nome da tabela se não for definido um alias
  • tabela para fazer join
  • alias da tabela de join
  • condição do join

left_join: igual ao inner join

right_join: igual ao inner join

where: Array com as condições do select

group_by: Array com os campos para agrupar

order_by: Array com 2 valores:

  • Campo para ordenar
  • ASC ou DESC

limit: Limite de resultados. Caso seja 1, será retornada a primeira (e única) posição do array

group_result_by: Agrupa o resultado do select por umas das colunas do select e contém um array como resposta. Não colocar alias da tabela (exemplo de como NÃO usar: apt.SEQ_DB).

group_result_by_unique: Agrupa o resultado do select por umas das colunas do select e contém um único objeto de retorno para cada item agrupado. Usado preferencialmente para o SEQ_DB de tabelas de cadastro.

if: Espera o nome de um filtro. Só executa o select caso o filtro seja especificado. Exemplo: um checkbox para incluir fotos. O select só é executado se o checkbox for marcado.

ATENÇÃO - Nova feature para usar um código PHP entre as QUERIES e ida para o template

Faça o INSERT de um registro na NFS_ENTRY_POINT com FILE_OR_DOMAIN = 'REPORTS' e ACTION = NFS_REPORTS.NAME

Dentro do campo CODE use os resultados das queries ou monte novas consultas, faça loops, etc. Os dados disponíveis são:


$reporConfig = $this->config;
$parameters = $this->parameters;
$resultadoDasQueries = $this->queryData;

$listaDasOrdemDeServico = $resultadoDasQueries['os'];
$observacaoDaOrdemDeServico = [];

foreach ($listaDasOrdemDeServico as $os) {
    if (empty($os['FIM_DH'])) {
        $observacaoDaOrdemDeServico[$os] = "OS ainda não foi encerrada";
    }
}

$resultadoDasQueries['observacoes'] = $observacaoDaOrdemDeServico;

$this->queryData = $resultadoDasQueries;

Exemplo

$reporConfig = $this->config;
$parameters = $this->parameters;
$resultadoDasQueries = $this->queryData;

// coverte para y-m-d
$beginDate = Utils::convertDateToMysqlWithoutHour($parameters['ini_dh']);
$endDate = Utils::convertDateToMysqlWithoutHour($parameters['fim_dh']);

// obtém um período de dias entre o ini_dh e fim_dh
list($arrayOfDays, $arrayOfDaysByDay, $totalOfDays) = DateUtils::getListOfDaysByPeriod($beginDate, $endDate);


$turnos =
	Dao::table('turno', 't')
		->select(['t.seq_db', 't.descricao'])
		->get();

$turnoByDay = [];

/**
 * É usado o dia como chave é adicionado
 * o turno e dentro do turno o total de segundos
 */
foreach($arrayOfDays as $day){
	foreach($turnos as $turno){
		$turnoByDay[$day][$turno['seq_db']] = $turno;
		$turnoByDay[$day][$turno['seq_db']]['totalSeconds'] = 0;
	}
}

/**
 * Obtem os dados do boletim com o turno, total de tempo em segundos
 *
 *
 *  Na coluna queries no nfs_reports do relatório tem o seguinte NFS Query Builder:
 *
 * {
*	"boletins":{
*		"select": [
*			"b.ini_fim_diff_sec sec",
*			"b.ini_dh",
*			"b.turno_seq_db",
*			"e.seq_db"
*		],
*		"from": "eqp_boletim b",
*		"inner_join" : [
*			["b","eqp", "e", "e.seq_db = b.eqp_seq_db"]
*		],
*		"where": [
*			"e.qig_display = 1",
*			"e.seq_db in (:eqp)"
*		]
*	}
*  }
 */
$boletins = $this->queryData['boletins'];

/*
*  É adicionado o turno e os dados do boletim
* para termos dentro de cada turno e dia da nossa estrutura
* o total de segundos, que é o período que o boletim ficou aberto
*/
foreach($boletins as $boletim){
	// converte data para y-m-d
	$dateBoletim = date('Y-m-d', strtotime($boletim['ini_dh']));
	$turnoBoletim = $boletim['turno_seq_db'];

	// verifica o dia e o turno
	if (isset($turnoByDay[$dateBoletim]) && isset($turnoByDay[$dateBoletim][$turnoBoletim])){
		$turnoData = $turnoByDay[$dateBoletim][$turnoBoletim];
		$turnoByDay[$dateBoletim][$turnoBoletim]['totalSeconds'] += $boletim['sec'];
	}
}

$resultadoDasQueries['days'] = $turnoByDay;
$this->queryData = $resultadoDasQueries;

Detalhes do TEMPLATE

Usando filters do NFS e do TWIG

Além dos filters do twig os filters abaixo estão disponíveis no NFS.
Para usá-los faça:

... {{ apontamento.INI_DH|nfs_dhs }}

Veja vídeo no começo dessa página.

Os códigos estão completos abaixo para que compreendam o que ele faz com o valor.

    $env->addFilter(new Twig_SimpleFilter('hour_minutes', function ($hours) {
		return floor($hours / 3600) .
			gmdate(':i', $hours % 3600);
	}));

	$env->addFilter(new Twig_SimpleFilter('nfs_type', function ($value, $type) {
		switch ($type) {
			case 'DHS':
				return date('d/m/Y H:i:s', strtotime($value));

			case 'HORA':
				return floor($value / 3600) .
					gmdate(':i:s', $value % 3600);

			default:
				return $value;
		}
	}));

	$env->addFilter(new Twig_SimpleFilter('nfs_date', function ($date) {
		if ($date === '' || is_null($date)) {
			return '';
		}
		return date('d/m/Y', strtotime($date));
	}));

	$env->addFilter(new Twig_SimpleFilter('nfs_dhs', function ($date) {
		if ($date === '' || is_null($date)) {
			return '';
		}
		return date('d/m/Y H:i:s', strtotime($date));
	}));

	$env->addFilter(new Twig_SimpleFilter('nfs_time', function ($date) {
		if ($date === '' || is_null($date)) {
			return '';
		}
		return date('H:i:s', strtotime($date));
	}));

	$env->addFilter(new Twig_SimpleFilter('nfs_number', function ($number) {
		if ($number === '' || is_null($number)) {
			return '';
		}
		return number_format($number, 2, ',', '.');
	}));

	$env->addFilter(new Twig_SimpleFilter('sort_by_key', function ($arr, $key) {
		if (is_array($arr)) {
			usort($arr, function ($a, $b) use ($key) {
				if (is_object($a)) {
					return $a->$key > $b->$key;
				}
				return $a[$key] > $b[$key];
			});
		}
		return $arr;
	}));

	$env->addFilter(new Twig_SimpleFilter('nfs_day_of_week', function ($date) {
		if ($date === '' || is_null($date)) {
			return '';
		}

		$daysOfWeek = [
			'Mon' => 'Seg',
			'Tue' => 'Ter',
			'Wed' => 'Qua',
			'Thu' => 'Qui',
			'Fri' => 'Sex',
			'Sat' => 'Sab',
			'Sun' => 'Dom'
		];
		$dayOfWeek = date('D', strtotime($date));
		return $daysOfWeek[$dayOfWeek];
	}));

Exibir filtros aplicados

Para exibir os filtros aplicados no relatório, deve-se incluir a seguinte linha no template:

{% include 'reports/_parameters.twig' %}
ATENÇÃO

Caso o include esteja dentro de um macro, é necessário colocar as variáveis filters e parameters na declaração e na chamada do macro.

Exemplo:

{% macro header(var1, var2, var3, filters, parameters) %}

...

{{ header(var1, var2, var3, filters, parameters) }}

Como alternativa, caso seja necessário fazer alguma alteração no layout padrão, utilize as variáveis:

  • filters: contém a configuração da coluna FILTERS
  • parameters: contém o valor aplicado para cada filtro.

Exemplo:

<div class="row space-before highlight">
    <div class="col-xs-12 text-center">
        <h5>Filtros aplicados</h5>
    </div>
</div>
<div class="row" style="padding:10px 0;">
    {% for name, config in filters %}
        <div class="col-xs-4">
            {{ config.options.label }}
        </div>
        <div class="col-xs-8">
            {% if config.type == 'Table' %}
                {{ parameters[name ~ '_fk']|join(', ')|default('-') }}
            {% elseif config.type == 'Checkbox' %}
               {{ parameters[name] ? 'Sim' : 'Não' }}
            {% else %}
               {{ parameters[name]|join(', ')|default('-') }}
            {% endif %}
        </div>
    {% endfor %}
</div>

EXPORT_OPTIONS

Opções de impressão do relatório.

JSON exemplo de configuração:

{
  "displayFooterInfo": true,
  "pdf": {
    "orientation": "Landscape",
    "footer-right": "[title] - [page]/[topage]"
  },
  "fileName": "os.CODIGO"
}

Parâmetro JSON - displayFooterInfo: Dados do ambiente no rodapé

A propriedade "displayFooterInfo" vai apresentar dados do ambiente no rodapé dos relatórios HTML e PDF.

Parâmetro JSON - pdf: Configurações do documento

Define opções específicas para a exportação do documento no formato PDF. Possui as configurações que define a orientação da página como horizontal (Landscape) ou o padrão vertical (Portrait). Permite também definir o conteúdo exibido no rodapé de cada página, neste exemplo, será exibido o título do documento seguido pela numeração da página no canto direito.

Parâmetro JSON - fileName: Personalizar nome do relatório PDF

A propriedade "fileName" pode ser usada em relatórios a serem exportados no formato PDF, permitindo alterar o nome padrão do arquivo. No exemplo acima o filtro de ‘OS’ será a referência, e, o campo ‘CODIGO’ deverá fornecer a string para o nome personalizado. Ps.: Esta configuração é valida somente para um único valor passado como filtro, no caso de múltiplos valores o arquivo mantem o nome padrão seguindo o nome do relatório.

Dados de informação no rodapé

Usar no twig, exemplo:

Exemplo:

    {% endif %}
    <div class="break-after">
			<div class="text-right">
				<font size="1">
					{{ footerInfo|raw }}
				</font>
			</div>
    </div>
  {% endfor %}

{% endblock %}

A propriedade displayFooterInfo vai apresentar dados do ambiente no rodapé dos relatórios HTML e PDF.

Lista de opções da exportação para pdf

Imagens

Para usar uma imagem direto do banco foi criada a tabela nfs_reports_images, possuindo os seguintes campos :

  • SEQ_DB: Gerado automaticamente pelo sistema.
  • INS_DH: Hora de inserção e é gerada automaticamente quando uma nova linha é adicionada.
  • ENABLED: Ativo (1) ou não ativo (0).
  • DESCRIPTION: Uma descrição se necessário.
  • NAME: Nome da imagem que será usada no relatório e na adicionada na export_options
  • IMAGE_BASE64: A imagem no formato base64, basta pegar a imagem original e fazer seu encode para base64, recomendamento o site Base64 Image Enconder e copiar o todo resultado que comece com data:image/png;base64,iVBO...

Caso no banco não exista tabela acima use o seguinte script para criar:

CREATE TABLE `nfs_reports_images` (
  `SEQ_DB` int(11) NOT NULL AUTO_INCREMENT,
  `INS_DH` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `ENABLED` tinyint(4) DEFAULT NULL,
  `DESCRIPTION` varchar(400) COLLATE utf8_bin DEFAULT NULL,
  `NAME` varchar(100) COLLATE utf8_bin DEFAULT NULL,
  `IMAGE_BASE64` text COLLATE utf8_bin COMMENT 'Imagem em formato base64',
  PRIMARY KEY (`SEQ_DB`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=DYNAMIC;

Na coluna EXPORT_OPTIONS, dentro do json adicionar um Array chamado images onde seu seus valores são os name das imagens que estão na tabela acima:

{
  "pdf": {
    ...
  },
  "images": [
    "jd",
    "real",
    "case",
    "new_holland"
  ]
}

Foi pensando que podemos usar uma ou mais imagens, então temos um Array onde podemos vamos obter as imagens com os nomes jd, real, case e new_holland da tabela nfs_reports_images.

No TEMPLATE é necessário usar a função img64 para obte-la é necessário fazer o seguinte import

{% from 'reports/macros.twig' import img64 %}

e para usar

{{ img64(images['case'].image_base64,'80px') }}

Dentro do array de images estão obtendo a chave case que tem como atributo image_base64 (Tudo minúsculo!! ) que é o mesmo nome da coluna na tabela e passando a altura de 80 pixels.

Agora todo o template sempre terá um array chamado images e o mesmo será preenchido quando for passado no EXPORT_OPTIONS, na dúvida use a função dump(images).

Funções customizadas para usar no TEMPLATE

Alterar orientação de uma imagem

É possível rotacionar uma imagem de acordo, se ela está no formato retrato é possível alterar para paisagem e vice-versa. Tudo isso é feito em memória. A Imagem é girada em 90 graus a esquerda.

Para usar a função é bem simples:

{{ orientation(imagem_base64, tipo_rotacao, graus) }}
  • imagem_base64: O primeiro parâmetro é a imagem original no formato de base64.
  • tipo_rotação: O tipo da rotação pode ser, portrait que é retrato e landscape que é paisagem, é usado para não mudar a orientação de uma imagem que já está correta.
  • graus: Por padrão é 90, então não é necessário passar esse parâmetro, mas caso queria rotacionar 180 graus é possível passando ele como o terceiro parâmetro.

Exemplos

  • Retrato
{{ orientation( foto_assinatura, "portrait") }}
  • Landscape
{{ orientation( foto_assinatura, "landscape") }}

Também é possível usar junto com a função imagem:

{{ img( orientation( foto_assinatura, 'landscape'), '160px') }}

Uma imagem original e abaixo uma que foi feita sua alteração para landscape : example_change_orientation.png

DataTable

É possível criar relatórios usando DataTable e assim disponibilizar funcionalidades.

É recomendável na div anterior ao elemento table adicionar as classes col-xs-12 e no-padding para que seja adicionado o autoscroll na horizontal para não quebrar o layout da página. Ex.:

<div class="col-xs-12 no-padding">
		<table class="table-condensed">
		</table>
</div>

Importante Uma vez usando DataTable é obrigatório que o <tbody>...</tbody> possua a mesma quantidade de colunas <td>...</td> que o cabeçalho, além de ser uma boa prática. Se essa regra não for cumprida o DataTable não carregará como esperado. Referência: DataTables example - Complex headers (rowspan and colspan) {.is-warning}

Em último caso É possível aplicar hacks para contornar essa limitação, mas não é 100% garantido! Referência: Propriedade colspan in table body TBODY

Exemplos HTML

Quebra de página ao gerar novo relatório

O código abaixo deve ser inserido no HTML para que possa ser feita a quebra de página ao gerar um novo relatório.

<div style="display:block; clear:both; page-break-after:always;"></div>

Informações do campo de uma tabela ultrapassam limite

A propriedade overflow do CSS controla o que acontece com o conteúdo que é muito grande para caber em uma área.

Exemplos de utilização e mais informações sobre o atributo overflow: https://www.w3schools.com/css/css_overflow.asp

exemplo:

<table style="width:100%">
  <tr>
    <th>Firstname</th>
    <th>Lastname</th>
    <th>Age</th>
  </tr>
  <tr>
    <td style="overflow:scroll">Jill</td>
    <td>Smith</td>
    <td>50</td>
  </tr>
</table>

Modal Para Envio de Relatórios Por E-mail

Introdução

Habilita um modal que permite o envio de relatórios por email na tela de CRUD. Quando configurado, um botão é exibido tanto na listagem dos registros quanto na tela de edição. Ao interagir com o botão o modal é aberto, onde é possível selecionar e-mails destinatários em uma lista, adicionar novos e-mails como destinatários, e, o relatório a ser enviado.

Configuração

Um novo parâmetro deve ser adicionado ao banco de dados na tabela nfs_core_par_parametros. O parâmetro deve ser um JSON de nome ‘SEND_REPORT_EMAIL’ com as seguintes chaves de configuração:

{
   "crud_table": "OS",
   "email_list": {
      "table": "CLIENTE",
      "columns": [
         "E_MAIL",
         "E_MAIL2",
         "E_MAIL3",
         "E_MAIL4",
         "E_MAIL5"
      ],
      "hasFilterByCrud": true
   },
   "reports": [
      {
         "display_name": "Assistência Técnica",
         "action":"assistencia_tecnica"
      }
   ]
}

Onde:

  • crud_table: Define em qual tela de CRUD deve ser exibido os botões de envio por e-mail, tanto na lista de registros quanto na tela edição.

  • email_list: Define a origem dos e-mails para receberem o relatório, devendo informar o nome da tabela (table) e quais colunas (columns) contem os e-mails. A flag hasFilterByCrud indica se os dados da tabela de emails deve ser filtrada pelo registro da tabela crud, por exemplo, no JSON acima os emails obtidos da tabela CLIENTE devem ser filtrados com base no valor de CLIENTE_SEQ_DB presente em OS, que é a crud table informada.

  • reports: Lista de relatórios associados a OS e que deseja enviar para os emails configurados/adicinados.

Restrição

Esta opção de envio de relatório por e-mail só é visível para usuários com permissão de edição em telas de CRUD.

Scheduler

Criar tabela nfs_core_scheduler

Verificar se existe a tabela nfs_core_scheduler, caso não exista pode ser inserido pelo create table abaixo:

CREATE TABLE `nfs_core_scheduler` (
  `SEQ_DB` bigint(20) NOT NULL AUTO_INCREMENT,
  `NAME` varchar(100) NOT NULL,
  `DESCRIPTION` varchar(300) DEFAULT NULL,
  `CODE_ENTRY_POINT` text,
  `EMPRESA` int(11) DEFAULT NULL,
  `FILIAL` int(11) DEFAULT NULL,
  `LOCAL` int(11) DEFAULT NULL,
  `ATIVO` int(11) DEFAULT NULL,
  `CRON_EXPRESSION` varchar(20) DEFAULT NULL,
  `LAST_EXECUTED` datetime DEFAULT NULL,
  `EXECUTED` int(11) DEFAULT '1'  NOT NULL,
  KEY `nfs_core_scheduler` (`SEQ_DB`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8;

Com a tabela criada a configura se baseia nos seguintes campos:

  • NAME: O nome do agendamento;
  • DESCRIPTION: Um descrição para o agendamento, esse campo é opcional;
  • CODE_ENTRY_POINT: O Código responsável por realizar a ação do agendamento;
  • EMPRESA: A empresa que deseja ser realizar as ações;
  • FILIAL: A filial que deseja ser realizar as ações;
  • LOCAL: A local que deseja ser realizar as ações;
  • ATIVO: Caso não deseje mais usar um agendamento basta marcar o ativo igual a 0 e vice-versa.
  • CRON_EXPRESSION: Expressão com o CRON de quando será executada a ação, recomendamos a utilização do Crontab-Guru ou Crontab-Generator para gerar a expressão cron a ser inserida nesse campo;
  • LAST_EXECUTED: Guarda a data da última execução e usada pelo scheduler verificar se o mesmo foi executado no horário correto.
  • EXECUTED: O seu controle é feito pelo core, representa uma flag para controlar se o entry_point foi executado com sucesso. O valor 0 representa código começou a executar mas não terminou e 1 que foi executado.

Exemplo para enviar um e-mail simples

:::danger ATENÇÃO Para configuração e envio com sucesso para mais de uma filial/local é necessário deixar-los com no mínimo um minuto de diferença para cada agendamento na coluna CRON_EXPRESSION. Exemplo:

SEQ_DBNAMEDESCRIPTIONCODE_ENTRY_POINTEMPRESAFILIALLOCALATIVOCRON_EXPRESSIONLAST_EXECUTEDEXECUTED
1---411127 12 * * *2018-08-16 12:30:231
2---422128 12 * * *2018-08-16 12:30:471
3---433129 12 * * *2018-08-16 12:31:201
4---444130 12 * * *2018-08-16 12:31:561
:::

Colunas com os seguintes valores:

NAME: ENVIAR_EMAIL
DESCRIPTION: Enviar simples email
EMPRESA: 1 
FILIAL: 1
LOCAL: 1
ATIVO: 1
CRON_EXPRESSION: 0 12 * * * // Roda todo dia às 12 horas e 0 minutos

CODE_ENTRY_POINT (Remover comentários ao utilizar o e-mail):

use scheduler\services\EmailHtmlService;
use voclasses\Usuario;
 
$typeSchedule = "email_erros_operacionais";
 
// Consulta para obter os e-mails
$resultEmails = Dao::table('usuario', 'us')
	->select(['us.EMAIL'])
	->innerJoin('us','usuario_tipo_agendamento', 'ut', 'ut.USUARIO_SEQ_DB = us.SEQ_DB')
	->innerJoin('ut', 'tipo_agendamento', 't', ' t.SEQ_DB = ut.TIPO_AGENDAMENTO_SEQ_DB')
	->where(['t.CODIGO' => $typeSchedule, 't.ATIVO' => true, 'ut.ATIVO' => true])
	->get();

	if (!empty($resultEmails)){
		$emails = [];
		foreach ($resultEmails as $value) {
		  $emails[] = $value['EMAIL'];
		}
 
		EmailHtmlService::setContainer($cli->getContainer());
		EmailHtmlService::setUser($user);
 
		$params = [];
		$params['inicio'] = date('d/m/Y');
		$params['format'] = 'html';
 
        /** getPdfReport:
        *    1) Nome do relatório no exemplo: relatorio_inconsistencia
        *    2) Parametros do relatório
        *    3) Sempre $subdomain, é uma variável da scheduler   
        * 
        *  Retorna um html do relatório.
        */
		$html = EmailHtmlService::getHtmlReport("relatorio_inconsistencia", $params, $subdomain);
 
		if($html === false){
			$html = '<i> Não houve inconsistências nesse dia! </i>';
		}
		if (isset($emails) && sizeof($emails) > 0) {
            /**
            * sendEmail:
            *  1) array de emails
            *  2) Assunto do email
            *  3) Html do relatório
            */
			EmailHtmlService::sendEmail($emails, "Relatório de Inconsistência", $html);
		}
}

Arquivos Compartilhados - Shared Files

Recurso para disponibilização de arquivos/documentos de forma pública.

Ativação do recurso

Para habilitar o recurso deve-se ter o seguinte entry point ativo:

INSERT INTO nfs_homol_transpotech_smartos.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', 'SHARED_FILES', NULL, NULL, 9999, 9999, 9999, 'FUNCIONARIO', NULL, '{
	"doc_table":"DOC_FUNCIONARIO",
	"doc_type_table":"DOC_TIPO",
	"doc_relationship_field": "FUNCIONARIO_SEQ_DB",
	"doc_field": "ARQUIVO",
	"title": "Documentos",
	"subtitle": "Download de arquivos",
	"logo": "LOGO_EMPRESA_HEADER",
	"columns": {
 		"name": "enable",
		"type": "enable",
		"validity": "enable"
	}
}', 1, 6, 1, 'JSON', NULL, NULL, '2019-11-22 11:08:25', '2025-03-11 14:30:50', NULL, 'nfs.fabio@189.127.15.246');

O campo CODE é onde setamos a configuração, é um JSON.

{
	"doc_table":"DOC_FUNCIONARIO",
	"doc_type_table":"DOC_TIPO",
	"doc_relationship_field": "FUNCIONARIO_SEQ_DB",
	"doc_field": "ARQUIVO",
	"title": "Documentos",
	"subtitle": "Download de arquivos",
	"logo": "LOGO_EMPRESA_HEADER",
	"columns": {
 		"name": "enable",
		"type": "enable",
		"validity": "enable"
	}
}

doc_table ➡️ Tabela dos documentos.

doc_type_table ➡️ Tabela que classifica o tipo do documento.

doc_relationship_field ➡️ Campo que representa o relacionamento com a entidade.

doc_field ➡️ Campo que guarda a url do documento.

title ➡️ Título que é apresentado no topo da tela de acesso aos documentos.

subtitle ➡️ Subtítulo, aparece abaixo do título.

logo ➡️ Deixar "LOGO_EMPRESA_HEADER" para usar o logo configurado para o cabeçalho.

columns ➡️ Ativa/desativa exibição de colunas.

Shared Files

--

A tabela FUNCIONARIO que é a entidade que agrupa os documentos recebe um novo campo, para armazenar a chave única que será a forma de identificação do funcionário, no caso.

O campo será o SHARED_FILES_KEY.

Exemplo:

insert
    into
    nfs_homol_transpotech_smartos.nfs_core_ds_tabela_campo (TABELA_NOME,
    NOME,
    SEQ,
    SYS,
    SEND_XMOVA,
    GRID,
    GRID_MOBILE,
    DESCRICAO,
    DESCRICAO_RESUMIDA,
    TIPO,
    OBRIGATORIO,
    TAMANHO,
    TAMANHO_DECIMAL,
    MASCARA,
    INSERT_UPDATE,
    DISABLED,
    HINT,
    LINK,
    VALIDACAO,
    OPCOES,
    VALOR_DEFAULT,
    VALIDACAO_VIEW,
    FILTRO_VIEW,
    TOOLTIP_MESSAGE,
    PROPERTIES,
    CONFIG)
values('FUNCIONARIO',
'SHARED_FILES_KEY',
null,
0,
1,
3,
1,
'Chave para acesso a documentos',
'Key',
'TXT',
0,
50,
null,
null,
'IU',
'1',
'',
null,
'UNQ',
null,
null,
null,
null,
null,
null,
null);

Na criação de um novo funcionário essa chave será gerada automaticamente, também será exibido um botão para gerar uma nova chave para o funcionário, e um botão que leva para a tela de documentos compartilhados no formulário do usuário.

Gerador de chave no formulário de funcionários

Na implantação do recurso em uma base de dados com registros já existentes pode-se usar o seguinte modelo de script para fazer a geração manualmente. Ele vai gerar a chave para todos registros que não tem a chave (null), ou os que tem uma chave inválida (vazio).

UPDATE app_funcionario
SET SHARED_FILES_KEY = LOWER(HEX(RANDOM_BYTES(16)))
WHERE SHARED_FILES_KEY IS NULL OR SHARED_FILES_KEY = '';

--

O recurso utiliza o armazenamento no S3, então para o correto funcionamento o campo que guarda a URL do documento precisa ser do tipo FILE.

Utils

Para a geração dessa key também pode ser usado o método Utils::generateSharedFileKey(). Ele vai gerar uma chave única no mesmo padrão das que são geradas automaticamente.

URL Example

https://<domain>/shared/<entity_table>/<SHARED_FILES_KEY> https://transpotech.h.simova.cloud/shared/FUNCIONARIO/6e829564e20c5cfa9c67f36912a9ec89

A partir dessa chave o usuário é localizado e tem seus documentos listados para download.

Cliente

O recurso foi desenvolvido e implementado usando o cliente TranspoTech. transpotech.h.simova.cloud.

TabelaBO

Turno Automático

Hoje no NFS possui duas configurações do Turno Automático, que é o Padrão e Mão de Obra.

Introdução

O Módulo Turno Automático objetiva executar operações automáticas de abertura e fechamento de turnos, assim como migrar / processar informações de (Boletins/Apontamento) recebidos de MOBILE, para uma determinada Entidade. Durante o processo de migração são criados vínculos entre os boletins/apontamentos MOBILE e AUTO;

Pré-requesitos

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

  1. Parâmetro WORKSHIFT_AUTO_ENABLED = 1 em nfs_core_par_parametro;
  2. Parâmetro WORKSHIFT_TABLE_INTERFERENCIA em nfs_core_par_parametro para definir a tabela de Interferências (se não definido, será usada a tabela OPER ou EQP_OPER, nessa ordem;
  3. Definir opções no BOLETIM (->TIPO = 1 e ->MOBILE_TABLE = 2) em nfs_core_ds_tabela -> OPTIONS as opções:
{
  "main_table": "EQP",
  "workshift_auto_enabled": true,
  "id-control": "fixed",
  "id-value": 9999
}
  • main_table: tabela principal do boletim (ex.: EQP ou FUNCIONARIO);
  • workshift_auto_enabled: define que o NFS Core faça o clone dessa tabela;
  • id-control e id-value: necessário sincronização entre as tabelas.
  1. Definir opções no APONTAMENTO (->TIPO = 1 e ->MOBILE_TABLE = 1) em nfs_core_ds_tabela -> OPTIONS as opções:
{
  "workshift_auto_enabled": true,
  "id-control": "fixed",
  "id-value": 9999
}
  • workshift_auto_enabled: define que o NFS Core faça o clone dessa tabela;
  • id-control e id-value: necessário sincronização entre as tabelas.
  1. Criar referências (FK) na Entidade para o Grupo de Turno Automático;
  2. Rodar o DS;
  3. Verificar se foram criadas as Tabelas do Sistema:
  • nfs_work_shift: tabela de configurações do Turno Automático
CREATE TABLE `nfs_work_shift` (
  `SEQ_DB` bigint NOT NULL AUTO_INCREMENT,
  `EMPRESA` int NOT NULL,
  `FILIAL` int NOT NULL,
  `LOCAL` int NOT NULL,
  `TITLE` varchar(100) DEFAULT NULL,
  `CONFIG` text,
  `INS_DH` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `ENABLED` int DEFAULT '1',
  UNIQUE KEY `SEQ_DB_UNIQUE` (`SEQ_DB`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
  • nfs_work_shift_log: tabela de log dos processos de Turno Automático
CREATE TABLE `nfs_work_shift_log` (
  `SEQ_DB` bigint NOT NULL AUTO_INCREMENT,
  `INS_DH` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `ENTIDADE_SEQ_DB` bigint unsigned NOT NULL,
  `TURNO_SEQ_DB` bigint unsigned NOT NULL,
  `BOLETIM_SEQ_DB` bigint unsigned NOT NULL,
  `TURNO_DATA` date DEFAULT NULL,
  `HORA_INI` timestamp NULL DEFAULT NULL,
  `HORA_FIM` timestamp NULL DEFAULT NULL,
  `MINUTOS_ANTES` int DEFAULT '0',
  `MINUTOS_DEPOIS` int DEFAULT '0',
  `PERDAS` varchar(500) DEFAULT NULL,
  `INS_USUARIO_SEQ_DB` bigint unsigned NOT NULL,
  `IND_SIT_PROC` tinyint DEFAULT '0',
  `IND_SIT_PROC_DH` timestamp NULL DEFAULT NULL,
  `IND_SIT_PROC_DESC` varchar(500) DEFAULT NULL,
  `TENTATIVAS` tinyint DEFAULT '1',
  `INCONSISTENCIA` tinyint DEFAULT '0',
  `INCONSISTENCIA_DESC` varchar(500) DEFAULT NULL,
  `BLOQUEADO` tinyint NOT NULL DEFAULT '0',
  UNIQUE KEY `SEQ_DB_UNIQUE` (`SEQ_DB`),
  UNIQUE KEY `nfs_work_shift_log_main_idx` (`ENTIDADE_SEQ_DB`,`TURNO_SEQ_DB`,`TURNO_DATA`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
  1. Verificar se foram criadas as Tabelas APP AUTO_GRUPO, AUTO_TURNO e AUTO_GRUPO_TURNO;

Em alguns casos, para criarmos o vinculo da entidade com o Grupo de Turno Automático, será necessário remover a restrição de FK do campo LINK em core_ds_tabela_campo. {.is-warning}

Sinônimos // Abstração

Entende-se como:

  • Entidade: tabela principal / chave em relação a qual os Turnos Automáticos são criados;
  • Interferência: grupo de dados que classificam apontamentos, podendo ser conhecido também como OPERAÇÕES OU PERDAS;

Demais Definições na estrutura de Tabelas

Colunas de apenas apontamentos automáticos (tabela de Boletins Automáticos - PDA), FK para BOLETIM MOBILE e FK para TURNO AUTOMÁTICO;

Boletim Auto

FK para BOLETIM AUTO e Colunas de Status / Controle do processo de Sincronização (tabela de Boletins Mobile - PD);

Boletim Mobile

FK para APONTAMENTO MOBILE (tabela de Apontamentos Auto);

Apontamento AUTO

FK para APONTAMENTO AUTO e colunas de Controle do processo de Sincronização (tabela de Apontamento Mobile);

Apontamento Mobile

Configuração Inicial

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

1. TITLE: Título do Painel, exibido no topo do Módulo;

TITLE='Gerenciamento de Turnos Automáticos'

2. CONFIG: JSON com as definições do ambiente de Turno Automático;

{
  "ENTIDADE": "EQP",
  "CONTROLE_HORIMETRO": true,
  "SPLIT_APT": true,
  "INTEGRACAO": {
    "COLUNA": "INTEGRA_ST",
    "DESCRICAO": "INTEGRA_DESC",
    "DATA_HORA": "INTEGRA_DH",
    "STATUS_OK": 0,
    "STATUS_ERRO": 5
  },
  "BOLETIM": {
    "TABELA": "EQP_BOLETIM",
    "KEY": "EQP_SEQ_DB",
    "COLUNAS": {
      "HR_INICIAL_MARTE": "HR_FINAL_MARTE",
      "MEDICAO_INICIO_GE": "MEDICAO_FIM_GE"
    },
    "MEDICAO": {
      "INICIAL": "HR_INICIAL_MARTE",
      "FINAL": "HR_FINAL_MARTE"
    },
    "HORIMETRO": {
      "INICIAL": "MEDICAO_INICIO_GE",
      "FINAL": "MEDICAO_FIM_GE"
    },
    "DECIMAIS": 1
  },
  "APONTAMENTO": {
    "TABELA": "EQP_APT",
    "KEY": "EQP_SEQ_DB",
    "CHECK_TURNO": "",
    "COLUNAS": {
      "ENCARREGADO_SEQ_DB": "ENCARREGADO_SEQ_DB",
      "EQP_OS_SEQ_DB": "EQP_OS_SEQ_DB",
      "HR_MARTE": "HR_MARTE_FIM",
      "MEDICAO_GE": "MEDICAO_GE_FINAL"
    },
    "MEDICAO": {
      "INICIAL": "HR_MARTE",
      "FINAL": "HR_MARTE_FIM"
    },
    "HORIMETRO": {
      "INICIAL": "MEDICAO_GE",
      "FINAL": "MEDICAO_GE_FINAL"
    },
    "DECIMAIS": 1
  },
  "PERDA": {
    "TABELA": "EQP_OPER",
    "KEY": "PERDA_SEQ_DB",
    "COLUNAS": ["INTERFERENCIA_PRI", "INTERFERENCIA_SEC", "INTERFERENCIA_TER"]
  },
  "PAINEL": ["EQP", "EQP_AUTO"]
}

Onde:

  • ENTIDADE: Entidade Principal;
  • CONTROLE_HORIMETRO: Se o processo de Turno Automático deve ou não controlar o progresso do valores de horímetro;
  • SPLIT_APT: se os Apontamentos Mobile devem ou não passar por processo de Split (quando estes estiverem entre dois turnos);

    Para que o processo ocorra corretamente é necessário que os horários sejam contínuos (fim de um, início do outro). Por exemplo: Turno A: 00:00:00 -> 12:00:00 | Turno B: 12:00:00 -> 00:00:00 {.is-warning}

  • INTEGRACAO: Se os dados gerados pelo Turno Automático fizerem parte de um processo de Integração é necessário especificar as colunas relacionadas;
  • BOLETIM: Definições do processo de sincronização e controle dos dados de Boletins;
  • APONTAMENTO: Definições do processo de sincronização e controle dos dados de Apontamentos;
  • PERDA: Definições do processo de sincronização e controle dos dados de Perdas / Paradas;
  • PAINEL: Define as Entidades que terão seus paineis exibidos.

As definições obrigatórias são exemplificadas abaixo:

{
  "ENTIDADE": "EQP",
  "BOLETIM": {
    "TABELA": "EQP_BOLETIM",
    "KEY": "EQP_SEQ_DB"
  },
  "APONTAMENTO": {
    "TABELA": "EQP_APT",
    "KEY": "EQP_SEQ_DB"
  }
}

As definições de CONTROLE_HORIMETRO, INTEGRACAO e PERDA são opcionais.

A propriedade CHECK_TURNO do grupo APONTAMENTO deve ser usada em situações onde a identificação do Turno Automático venha diretamente de Mobile, permitindo que apontamentos dentro do mesmo range de hora possam ser inseridos em determinado Turno Automático.

Detalhamento das Propriedades

CONTROLE_HORIMETRO

É utilizada para controlar a progressão contínua do horímetro (maior ou igual ao atual). Nesse processo os valores especificados em MEDICAO, INICIAL e FINAL, são usado para controlar a progressão dos valores salvos em HORIMETRO, INICIAL e FINAL.

Exemplo: Se MEDICAO -> INICIAL > 0, então HORIMETRO -> INICIAL recebe MEDICAO -> INICIAL; do contrário, recebe o último horímetro válido; Se MEDICAO -> FINAL > 0, então HORIMETRO -> FINAL recebe MEDICAO -> FINAL; do contrário, recebe o último horímetro válido;

INTEGRACAO

As propriedades definidas são usadas para sinalizar se os dados são consistentes ou inconsistentes. Os valores de COLUNA, DESCRICAO e DATA_HORA são referências diretas às colunas definidas no Boletim, sinalizando em COLUNA se os dados são Consistente/Inconsistência, assumindo os valores em STATUS_OK e STATUS_ERRO, respectivamente.

BOLETIM / APONTAMENTO

As propriedades definidas em TABELA, KEY definem os vínculos com as tabelas Boletim e Apontamento Mobile e suas respectivas chaves-primárias PK. As propriedades COLUNAS definem os dados de serão migrados (DE/PAPA) do último Boletim / Apontamento para os próximos Boletins / Apontamentos automáticos. Nesses grupos também são definidas as colunas de Controle de Medição, onde MEDICAO e HORIMETRO guardam essas referências (descrito anteriomente). A propriedade DECIMAIS refere-se ao número de casas decimais usadas no processo de controle como fator de arredondadmento.

Ambiente WorkShift / Turno Automático

Log de Processos de Turno Automático

An image

Onde:

  • Carrega logs de agendamentos dos turnos já abertos e/ou fechados de acordo com a data:
Carrega logs de agendamentos dos turnos
  • Força abertura de agendamentos do dia (caso não rode na hora programada, de todos os equipamentos):
Força abertura de agendamentos do dia
  • Filtro rápido dos dados de log carregados:
Filtro rápido dos dados de log carregados
  • Informações:
  • Link de Acesso ao painel de detalhes Link de Acesso ao painel de detalhes
  • Apenas apontamentos automáticos Apenas apontamentos automáticos (WORKSHIFT_ONLY = 1)
  • Boletim com Inconsistência Boletim com Inconsistência
  • Boletim liberado para Integração ou Integrado Boletim liberado para Integração ou Integrado
  • Boletim bloqueado para Integração ou com Erro Boletim bloqueado para Integração ou com Erro
  • Operações:

operations_buttons.png

  • Sincronizar: verifica se existem dados novos vindo de mobile, sincroniza e, se necessário, fecha o turno automático;
  • Reprocessar: excluir todos os dados do Turno Automático atual, sincroniza e, se necessário, fecha o turno automático;
  • Aprovar: ajusta turno automático e sincroniza se houver inconsistência de apontamento fechado fora do horário do turno.

O processo de Aprovação não deve ser usado em conjunto com a propriedade SPLIT_APT. Essa operação será removida automaticamente em futuras atualizações quando a propriedade estiver definida. {.is-warning}

Configurar os Turnos e os Grupos de Turnos

Configurações dos Turnos Automático

turno_agendamento.png

  • Hora Inicial e Final do Turno (hh:mm:ss): horário de abrangência do turno, onde todos os apontamentos da entidade para o range de horário serão migrados para o boletim do Turno Automático;

  • Ajustes de Tolerância (minutos): definições de tolerância (INICIAL e/ou FINAL) para processar / migrar dados de Mobile para o Turnos Automáticos;

  • Dias da Semana por Tipo: comportamento de abertura do Turno Automático de acordo com o dia da semana, onde:

    • Não: não são criado turnos automáticos para o data/hora especificada;
    • AUTOMÁTICO: sempre vai criar o turno automático para a data/hora especificada;
    • POR DEMANDA: só cria o turno automático se receber dados do mobile para data/hora especificada;
  • Interferências: definições relacionadas às perdas / interferências que serão apontadas automáticamente nas seguintes situações:

    • Primárias, Secundárias e Terciárias: são aquelas que geram apontamentos lançados automaticamente na abertura do boletim e no fechamento. A interferência com 0 (zero) minutos será usada para complementar o boletim, seja na abertura ou no fechamento;

turno_perdas.png

As definições seguem a relação interferência => tempo min.

Observações:

  1. Se você especificar uma (1) interferência e tempo = 0 min, será feito um apontamento com FIM_DH nulo (aberto);
  2. Se você especificar uma (1) interferência e tempo > 0 min, será feito um apontamento com FIM_DH = INI_DH + min;
  3. Só pode haver uma (1) interferência com tempo = 0 min;
  4. A interferência com tempo = 0 min também será usada para complementar o turno automático (preencher saltos de horário e complementos no fechamento do turno, se necessário);
  5. Se não houver interferência com tempo = 0 não serão criados apontamentos complementares (que preencherão espaços entre apontamentos).
  • Refeição: essa interferência deverá ser apontada automaticamente quando não for recebida nenhuma comunicação de Mobile (WORKSHIFT_ONLY = 1 no Boletim AUTO). Uma vez definida, o lançamento deve ter horário e duração definidos;
  • Medição: essa interferência deverá ser apontada automaticamente para corrigir possíveis "saltos" de horímetro quando quando o controle de horímetro estiver ativo (CONTROLE_HORIMETRO = true).

Configurações de Grupos de Turnos Automático

Criação do Grupos de turnos automático que serão atribuídos à Entidade do turno Automático.

Cada grupo deve ter ao menos um (1) turno automático atribuídos.

IMPORTANTE! Não se deve atribuir turnos automáticos com horários conflitantes, salvo aqueles onde a propriedade CHECK_TURNO estiver estabelecida! {.is-danger}

Interferências que Perpetuam

São interferência que orientam a abertura dos Turnos Automático, permitindo que ao encerrar um boletim com uma dessas interferências o próximo boletim seja aberto com essa mesma interferência.

perda_perpetua_destacado.png

O campo WORKSHIFT_PERPETUA não é criado automaticamente, logo, é necessário a criação do campo WORKSHIFT_PERPETUA (campo destacado na imagem acima) "Perpetua no turno automático?" para tratar a operação/interferência que perpetua e deve ser criado na tabela APP_OPER através da nfs_core_ds_tabela_campo {.is-warning}

INSERT INTO nfs_core_ds_tabela_campo (TABELA_NOME, NOME, SEQ,
SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA,
TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE,
DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT,
VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES) VALUES('OPER', 'WORKSHIFT_PERPETUA', 12, 0, 1, 1, 1,
'Perpetua no turno automático?', 'Perpetua?', 'BOOL', 0, NULL,
NULL, NULL, 'IU', '0', NULL, NULL, NULL,
'param:CRUD_OPCOES_BOOL;showKey:0', '0', NULL, NULL, NULL, NULL);

Para habilitar o menu de turno automático, será necessária adicionar permissão para o menu de seq_db = 10050 na tabela nfs_acl_grupo_permissao.

menu_auto.png

Este menu virá apenas com os menus para tabelas AUTO_GRUPO, AUTO_TURNO, e para o painel de gerenciamento; caso seja necessário adicionar submenus ou algum outro item, criar o item desejado em nfs__core_menu e atribuir como FATHER o menu de turno automático (SEQ_DB = 10050).

Turno Automático Mão de Obra

Introdução

O Módulo Turno Automático Mão de Obra (TA MO) objetiva executar operações automáticas de abertura e fechamento de turnos, assim como migrar / processar informações de (Boletins/Apontamento) recebidos de MOBILE, para uma determinada Entidade Encarregada (estrutura mestre/detalhes). Durante o processo de migração são criados vínculos entre os boletins/apontamentos MOBILE e AUTO;

Pré-requesitos

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

  1. Parâmetro WORKSHIFT_MO_AUTO_ENABLED = 1 em nfs_core_par_parametro;
  2. Parâmetro WORKSHIFT_MO_TABLE_INTERFERENCIA em nfs_core_par_parametro para definir a tabela de Interferências (se não definido, será usada a tabela OPER ou EQP_OPER, nessa ordem;
  3. Definir opções no BOLETIM (->TIPO = 1 e ->MOBILE_TABLE = 2) em nfs_core_ds_tabela -> OPTIONS as opções:
{
  "main_table": "EQP",
  "workshift_mo_auto_enabled": true,
  "id-control": "fixed",
  "id-value": 9999
}
  • main_table: tabela principal do boletim (ex.: EFETIVO_FUNCIONARIO ou FUNCIONARIO);
  • workshift_mo_auto_enabled: define que o NFS Core faça o clone dessa tabela;
  • id-control e id-value: necessário sincronização entre as tabelas.
  1. Definir opções no APONTAMENTO (->TIPO = 1 e ->MOBILE_TABLE = 1) em nfs_core_ds_tabela -> OPTIONS as opções:
{
  "workshift_mo_auto_enabled": true,
  "id-control": "fixed",
  "id-value": 9999
}
  • workshift_mo_auto_enabled: define que o NFS Core faça o clone dessa tabela;
  • id-control e id-value: necessário sincronização entre as tabelas.
  1. Criar referências (FK) na Entidade para o Grupo de Turno Automático de Mão de Obra;
  2. Rodar o DS;
  3. Verificar se foram criadas as Tabelas do Sistema:
  • nfs_work_shift_mo: tabela de configurações do TA MO:
CREATE TABLE `nfs_work_shift_mo` (
  `SEQ_DB` bigint NOT NULL AUTO_INCREMENT,
  `EMPRESA` int NOT NULL,
  `FILIAL` int NOT NULL,
  `LOCAL` int NOT NULL,
  `TITLE` varchar(100) DEFAULT NULL,
  `CONFIG` text,
  `INS_DH` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `ENABLED` int DEFAULT '1',
  UNIQUE KEY `SEQ_DB_UNIQUE` (`SEQ_DB`)
) ENGINE = InnoDB default CHARSET = utf8 ROW_FORMAT = COMPACT;
  • nfs_work_shift_mo_log: tabela de log dos processos de TA MO:
CREATE TABLE `nfs_work_shift_mo_log` (
  `SEQ_DB` bigint NOT NULL AUTO_INCREMENT,
  `SEQ_DB` bigint NOT null auto_increment,
  `INS_DH` timestamp null default CURRENT_TIMESTAMP,
  `ENTIDADE_SEQ_DB` bigint(20) unsigned not null,
  `TURNO_SEQ_DB` bigint(20) unsigned not null,
  `BOLETIM_SEQ_DB` bigint(20) unsigned not null,
  `TURNO_DATA` date null default null,
  `HORA_INI` timestamp null default null,
  `HORA_FIM` timestamp null default null,
  `MINUTOS_ANTES` int default 0,
  `MINUTOS_DEPOIS` int default 0,
  `OPERADORES` varchar(1000) default null,
  `PERDAS` varchar(500) default null,
  `INS_USUARIO_SEQ_DB` bigint unsigned NOT null,
  `IND_SIT_PROC` tinyint default '0', -- 1 abriu; -1 erro; 2 fechou; -2 erro ao fechar
  `IND_SIT_PROC_DH` timestamp null default null,
  `IND_SIT_PROC_DESC` varchar(500) default null,
  `TENTATIVAS` tinyint default '1',
  `INCONSISTENCIA` tinyint default '0',
  `INCONSISTENCIA_DESC` varchar(500) NULL,
  `BLOQUEADO` TINYINT DEFAULT 0 NOT NULL,
  UNIQUE KEY `SEQ_DB_UNIQUE` (`SEQ_DB`),
  UNIQUE KEY `nfs_work_shift_mo_log_main_idx` (`ENTIDADE_SEQ_DB`,`TURNO_SEQ_DB`,`TURNO_DATA`) USING BTREE,
  KEY `nfs_work_shift_mo_log_entidade_unsyncced` (`ENTIDADE_SEQ_DB`,`IND_SIT_PROC`,`HORA_INI`) USING BTREE,
  KEY `nfs_work_shift_mo_log_entidade_ini_idx` (`ENTIDADE_SEQ_DB`,`HORA_INI`) USING BTREE
) ENGINE = InnoDB default CHARSET = utf8 ROW_FORMAT = COMPACT;";
  1. Verificar se foram criadas as Tabelas APP MO_AUTO_GRUPO, MO_AUTO_TURNO e MO_AUTO_GRUPO_TURNO;

Em alguns casos, para criarmos o vinculo da entidade com o Grupo de Turno Automático, será necessário remover a restrição de FK do campo LINK em core_ds_tabela_campo. {.is-warning}

Sinônimos // Abstração

Entende-se como:

  • Entidade: tabela principal / chave em relação a qual os TA's são criados;
  • Interferência: grupo de dados que classificam apontamentos, podendo ser conhecido também como OPERAÇÕES OU PERDAS;

Demais Definições na estrutura de Tabelas

Colunas de apenas apontamentos automáticos (tabela de Boletins Automáticos Mão de Obra), FK para BOLETIM MOBILE e FK para TURNO AUTOMÁTICO;

app_mo_boletim_auto.png

FK para BOLETIM AUTO e Colunas de Status / Controle do processo de Sincronização (tabela de Boletins Mobile);

app_mo_boletim.png

FK para APONTAMENTO MOBILE (tabela de Apontamentos Auto);

app_mo_apt_auto.png

FK para APONTAMENTO AUTO e colunas de Controle do processo de Sincronização (tabela de Apontamento Mobile);

app_mo_apt.png

Configuração Inicial

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

1. TITLE: Título do Painel, exibido no topo do Módulo;

TITLE='Gerenciamento de Turnos Automáticos Mão de Obra'

2. CONFIG: JSON com as definições do ambiente de TA;

{
  "ENTIDADE": "EFETIVO_FUNCIONARIO",
  "MASTER_KEY": "ENCARREGADO_SEQ_DB",
  "SPLIT_APT": true,
  "INTEGRACAO": {
    "COLUNA": "INTEGRA_ST",
    "DESCRICAO": "INTEGRA_DESC",
    "DATA_HORA": "INTEGRA_DH",
    "STATUS_OK": 0,
    "STATUS_ERRO": 5
  },
  "BOLETIM": {
    "TABELA": "MO_BOLETIM",
    "KEY": "EFETIVO_FUNCIONARIO_SEQ_DB",
    "COLUNAS": {
      "COLUNA_TO": "COLUNA_FROM"
    }
  },
  "APONTAMENTO": {
    "TABELA": "EQP_APT",
    "KEY": "EQP_SEQ_DB",
    "CHECK_TURNO": "",
    "COLUNAS": {
      "COLUNA_TO": "COLUNA_FROM"
    }
  },
  "PERDA": {
    "TABELA": "INTERFERENCIA",
    "KEY": "INTERFERENCIA_SEQ_DB",
    "COLUNAS": ["INTERFERENCIA_PRI", "INTERFERENCIA_SEC", "INTERFERENCIA_TER"]
  },
  "PAINEL": ["EQP", "EQP_AUTO"]
}

Onde:

  • ENTIDADE: Entidade Principal;
  • MASTER_KEY: FK da ENTIDADE Mestre -> detalhes (Ex.: Encarregado -> Operador);
  • SPLIT_APT: se os Apontamentos Mobile devem ou não passar por processo de Split (quando estes estiverem entre dois turnos);

    Para que o processo ocorra corretamente é necessário que os horários sejam contínuos (fim de um, início do outro). Por exemplo: Turno A: 00:00:00 -> 12:00:00 | Turno B: 12:00:00 -> 00:00:00 {.is-warning}

  • INTEGRACAO: Se os dados gerados pelo TA MO fizerem parte de um processo de Integração é necessário especificar as colunas relacionadas;
  • BOLETIM: Definições do processo de sincronização e controle dos dados de Boletins;
  • APONTAMENTO: Definições do processo de sincronização e controle dos dados de Apontamentos;
  • PERDA: Definições do processo de sincronização e controle dos dados de Perdas / Paradas;
  • PAINEL: Define as Entidades que terão seus paineis exibidos.

As definições obrigatórias são exemplificadas abaixo:

{
  "ENTIDADE": "EFETIVO_FUNCIONARIO",
  "MASTER_KEY": "ENCARREGADO_SEQ_DB",
  "BOLETIM": {
    "TABELA": "EQP_BOLETIM",
    "KEY": "EQP_SEQ_DB"
  },
  "APONTAMENTO": {
    "TABELA": "EQP_APT",
    "KEY": "EQP_SEQ_DB"
  }
}

As definições de SPLIT_APT, INTEGRACAO e PERDA são opcionais.

A propriedade CHECK_TURNO do grupo APONTAMENTO deve ser usada em situações onde a identificação do TA venha diretamente de Mobile, permitindo que apontamentos dentro do mesmo range de hora possam ser inseridos em determinado Turno Automático.

Detalhamento das Propriedades

INTEGRACAO

As propriedades definidas são usadas para sinalizar se os dados são consistentes ou inconsistentes. Os valores de COLUNA, DESCRICAO e DATA_HORA são referências diretas às colunas definidas no Boletim, sinalizando em COLUNA se os dados são Consistente/Inconsistência, assumindo os valores em STATUS_OK e STATUS_ERRO, respectivamente.

BOLETIM / APONTAMENTO

As propriedades definidas em TABELA, KEY definem os vínculos com as tabelas Boletim e Apontamento Mobile e suas respectivas chaves-primárias PK. As propriedades COLUNAS definem os dados de serão migrados (DE/PAPA) do último Boletim / Apontamento para os próximos Boletins / Apontamentos automáticos.

Ambiente Turno Automático de Mão de Obra

Log de Processos de Turno Automático

An image

Onde:

  • Carrega logs de agendamentos dos turnos já abertos e/ou fechados de acordo com a data:
Carrega logs de agendamentos dos turnos
  • Força abertura de agendamentos do dia (caso não rode na hora programada, de todos os equipamentos):
Força abertura de agendamentos do dia
  • Filtro rápido dos dados de log carregados:
Filtro rápido dos dados de log carregados
  • Informações:
  • Link de Acesso ao painel de detalhes Link de Acesso ao painel de detalhes
  • Apenas apontamentos automáticos Apenas apontamentos automáticos (WORKSHIFT_MO_ONLY = 1)
  • Boletim com Inconsistência Boletim com Inconsistência
  • Boletim liberado para Integração ou Integrado Boletim liberado para Integração ou Integrado
  • Boletim bloqueado para Integração ou com Erro Boletim bloqueado para Integração ou com Erro
  • Operações:

operations_buttons.png

  • Sincronizar: verifica se existem dados novos vindo de mobile, sincroniza e, se necessário, fecha o turno automático;
  • Reprocessar: excluir todos os dados do Turno Automático atual, sincroniza e, se necessário, fecha o turno automático;
  • Aprovar: ajusta turno automático e sincroniza se houver inconsistência de apontamento fechado fora do horário do turno.

O processo de Aprovação não deve ser usado em conjunto com a propriedade SPLIT_APT. Essa operação será removida automaticamente em futuras atualizações quando a propriedade estiver definida. {.is-warning}

Configurar os Turnos e os Grupos de Turnos

Configurações dos Turnos Automático Mão de Obra

turno_agendamento.png

  • Hora Inicial e Final do Turno (hh:mm:ss): horário de abrangência do turno, onde todos os apontamentos da entidade para o range de horário serão migrados para o boletim do TA;

  • Ajustes de Tolerância (minutos): definições de tolerância (INICIAL e/ou FINAL) para processar / migrar dados de Mobile para o TA's;

  • Dias da Semana por Tipo: comportamento de abertura do Turno Automático de acordo com o dia da semana, onde:

    • Não: não são criado turnos automáticos para o data/hora especificada;
    • AUTOMÁTICO: sempre vai criar o turno automático para a data/hora especificada;
    • POR DEMANDA: só cria o turno automático se receber dados do mobile para data/hora especificada;
  • Interferências: definições relacionadas às perdas / interferências que serão apontadas automáticamente nas seguintes situações:

    • Primárias, Secundárias e Terciárias: são aquelas que geram apontamentos lançados automaticamente na abertura do boletim e no fechamento. A interferência com 0 (zero) minutos será usada para complementar o boletim, seja na abertura ou no fechamento;

turno_perdas.png

As definições seguem a relação interferência => tempo min.

Observações:

  1. Se você especificar uma (1) interferência e tempo = 0 min, será feito um apontamento com FIM_DH nulo (aberto);
  2. Se você especificar uma (1) interferência e tempo > 0 min, será feito um apontamento com FIM_DH = INI_DH + min;
  3. Só pode haver uma (1) interferência com tempo = 0 min;
  4. A interferência com tempo = 0 min também será usada para complementar o turno automático (preencher saltos de horário e complementos no fechamento do turno, se necessário);
  5. Se não houver interferência com tempo = 0 não serão criados apontamentos complementares (que preencherão espaços entre apontamentos).
  • Refeição: essa interferência deverá ser apontada automaticamente quando não for recebida nenhuma comunicação de Mobile (WORKSHIFT_MO_ONLY = 1 no Boletim AUTO). Uma vez definida, o lançamento deve ter horário e duração definidos;
  • Medição: essa interferência deverá ser apontada automaticamente para corrigir possíveis "saltos" de horímetro quando quando o controle de horímetro estiver ativo (CONTROLE_HORIMETRO = true).

Configurações de Grupos de Turnos Automático

Criação do Grupos de turnos automático que serão atribuídos à Entidade do turno Automático.

Cada grupo deve ter ao menos um (1) turno automático atribuídos.

IMPORTANTE! Não se deve atribuir turnos automáticos com horários conflitantes, salvo aqueles onde a propriedade CHECK_TURNO estiver estabelecida! {.is-danger}

Interferências que Perpetuam

São interferência que orientam a abertura dos TA, permitindo que ao encerrar um boletim com uma dessas interferências o próximo boletim seja aberto com essa mesma interferência.

perda_perpetua_destacado.png

O campo WORKSHIFT_MO_PERPETUA não é criado automaticamente, logo, é necessário a criação do campo (campo destacado na imagem acima) "Perpetua no turno automático?" para tratar a operação/interferência que perpetua e deve ser criado na tabela APP_OPER através da nfs_core_ds_tabela_campo {.is-warning}

INSERT INTO nfs_core_ds_tabela_campo (TABELA_NOME, NOME, SEQ,
SYS, SEND_XMOVA, GRID, GRID_MOBILE, DESCRICAO, DESCRICAO_RESUMIDA,
TIPO, OBRIGATORIO, TAMANHO, TAMANHO_DECIMAL, MASCARA, INSERT_UPDATE,
DISABLED, HINT, LINK, VALIDACAO, OPCOES, VALOR_DEFAULT,
VALIDACAO_VIEW, FILTRO_VIEW, TOOLTIP_MESSAGE, PROPERTIES) VALUES('INTERFERENCIA', 'WORKSHIFT_MO_PERPETUA', 12, 0, 1, 1, 1,
'Perpetua no turno automático?', 'Perpetua?', 'BOOL', 0, NULL,
NULL, NULL, 'IU', '0', NULL, NULL, NULL,
'param:CRUD_OPCOES_BOOL;showKey:0', '0', NULL, NULL, NULL, NULL);

Para habilitar o menu de TA, será necessária adicionar permissão para o menu de seq_db = 10060 na tabela nfs_acl_grupo_permissao.

menu_auto.png

Este menu virá apenas com os menus para tabelas MO_AUTO_GRUPO, MO_AUTO_TURNO, e para o painel de gerenciamento; caso seja necessário adicionar submenus ou algum outro item, criar o item desejado em nfs_core_menu e atribuir como FATHER o menu de turno automático (SEQ_DB = 10060).

Utils

A Classe Utils

A classe Utils possui uma série de funções que podem ser utilizadas como açucares sintáticos, para tratar ou gerar dados em formatos específicos.

Métodos Date

getDateNow

Retorna data atual.

  • Entrada:
    • Parâmetros: nenhum;
  • Saida:
    • Tipo: String;
    • Formato: 'd/m/Y';
    • Saída: '14/10/2022'.
Utils::getDateNow();

getTimeNow

Retorna data atual.

  • Entrada:
    • Parâmetros: nenhum;
  • Saida:
    • Tipo de saída: String;
    • Formato: 'H:i:s';
    • Saída: '12:20:19'.
Utils::getDateNow();

getDateTimeNow

Retorna data e hora atual.

  • Entrada:
    • Parâmetros: nenhum;
  • Saida:
    • Tipo de saída: String;
    • Formato: 'd/m/Y H:i:s';
    • Saída: '14/10/2022 12:20:19'.
Utils::getDateTimeNow();

getDateTimeNowToDb

Retorna data e hora atual no formato esperado do Banco.

  • Entrada:
    • Parâmetros: nenhum;
  • Saida:
    • Tipo de saída: String;
    • Formato: 'Y-m-d H:i:s';
    • Saída: '2022-10-14 12:22:46'.
Utils::getDateTimeNowToDb();

convertDateTimeToMysql

Converte uma data hora para o formato MySql

  • Entrada:
    • Parâmetros: $date;
    • Formato: 'd/m/Y H:i';
  • Saida:
    • Tipo de saída: String;
    • Formato: 'Y-m-d H:i:s';
    • Saída: '2022-10-14 12:22:46'.
Utils::convertDateTimeToMysql($dmyhi);

convertDateToMysqlWithoutHour

Converte uma data para o formato MySql

  • Entrada:
    • Parâmetros: $date;
    • Formato: 'd/m/Y';
  • Saida:
    • Tipo de saída: String;
    • Formato: 'Y-m-d H:i:s';
    • Saída: '2022-10-14 12:22:46'.
Utils::convertDateToMysqlWithoutHour($dmy);

convertDateToMysql

Converte uma data para o formato MySql

  • Entrada:
    • Parâmetros: $date;
    • Formato: 'd/m/Y';
  • Saida:
    • Tipo de saída: String;
    • Formato: 'Y-m-d 00:00:00';
    • Saída: '2022-10-14 00:00:00'.
Utils::convertDateToMysqlWithoutHour($dmy);

convertDateToMysqlWithoutHour

Converte uma data para o formato MySql

  • Entrada:
    • Parâmetros: $date;
    • Formato: 'd/m/Y';
  • Saida:
    • Tipo de saída: String;
    • Formato: 'Y-m-d'';
    • Saída: '2022-10-14'.
Utils::convertDateToMysqlWithoutHour($dmy);

Numeric Utils

Classe de métodos estáticos relacionados a números inteiros e decimais.


Métodos


formatDecimalFromMask

Formata um número decimal com base em uma máscara de exibição, semelhante à utilizada nos campos de formulários do sistema.

A função interpreta a máscara fornecida para aplicar:

  • Quantidade de casas decimais (.99 ou ,99).
  • Separador decimal (ponto ou vírgula).
  • Separador de milhar (inferido com base nos caracteres da parte inteira).

Parâmetros

float|string $value: Valor decimal que será formatado (aceita string ou número).

string $mask: Máscara de formatação (ex: 9[9][9],99, 9.999,99, etc).

Retorno

string: Valor formatado como string, de acordo com os separadores e casas decimais da máscara. Retorna string vazia se o valor informado não for numérico.

Exemplos de uso

NumericUtils::formatDecimalFromMask(1234.56, '9[9][9],99');
NumericUtils::formatDecimalFromMask('9876.5', '9[9][9][9].99');
$tabela = xDS::getTable('APONTAMENTO_EQUIPAMENTO');
$tipo = $tabela->CAMPOS['HORIMETRO_ANTERIOR']->TIPO;
$mask = $tabela->CAMPOS['HORIMETRO_ANTERIOR']->MASCARA;

$value = Dao::table('APONTAMENTO_EQUIPAMENTO')->seqDb(89);
echo NumericUtils::formatDecimalFromMask($value['HORIMETRO_ANTERIOR'], $mask);

Notas

Esta função foi criada para reutilização em partes do sistema fora do CRUD, como geração de relatórios, mantendo o mesmo padrão visual de formatação numérica.


NFS xMova Field Server

Como Funciona

O XFS (xMova Field Server) é uma funcionalidade que permite que o smartphone onde é realizado o apontamento que se encontra fixo ou mesmo móvel, mas em um área 100% sem sinal possa receber dados atualizado da Web através de um segundo celular que passar por esse local de tempos em tempos.

Para todas essa estrutura funcionar é necessário primeiro definir quem vai ser o smartphone Server, responsável por coletar (Obtem dados de Apontamentos e quando com sinal transmite-os) e entregar (Obter os dados atualizados da Web e transmitir para o aparelho que está 100% sem sinal) os dados.

O Segundo é definir quem será o Client, que é o smartphone que está em um equipamento que está numa área sem sinal e raramente se desloca para uma área com sinal, porém, é necessário tanto obter seus dados de apontamentos quanto atualizar sua lista de dados com possíves novas informações.

O NFS entra na parte de gerar os dados e enviar eles para todos os possiveis auths, porque você pode alimentar um ou mais equipamentos que estão sem sinal.

Como é muito para gerar os dados de todos os auths possíveis leva um tempo e acontece timeout na comunicação com o xMova, no NFS antes geramos um arquivo prévio e quando é feita a transmissão para o xMova, só lemos esse arquivo e enviamos seus dados, então é importante os dados neles estarem atualizados.

Configuração

Pré-requisito

É necessário ter na par_parametros o PROCESS_BACKGROUND ativo para poder gerar o arquivo de XFS pelo painel.

INSERT INTO nfs_core_par_parametros (EMPRESA, FILIAL, `LOCAL`, NOME, CONTEUDO, TIPO) VALUES(9999, 9999, 9999, 'PROCESS_BACKGROUND', '1', 1);

param

xMova

Primeiro configurar o Server e Client no fluxo, que é possível através do link (colocar aqui link xmova config.)

Montar JSON

O json que é montado é referente ao Client, que é o responsável por receber os dados da WEB.

  • Acessar o banco de dados do cliente e criar o seguinte entry point:
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) VALUES('XFS', 'test_many_install_codes_in_one', 999000011, '9999', 1, 9999, 9999, NULL, NULL, '{
    "auths": {
        "220123350": {
            "fields": [
                "SELECT seq_db seqEquipamento FROM app_eqp WHERE ativo=1"
            ],
            "entities": [
                "Funcionario_Mobile",
                "Equipamento_Mobile",
                "Eqp_Classe_Mobile",
                "Oper_Mobile",
                "Oper_Grupo_Mobile"
            ],
            "empresaFilialLocal": [
                "UPs_Mobile"
            ],
            "shared": []
        }
    },
    "installCodes": [220123350, 187123701]
}', 1, 304, 1, 'JSON', NULL, NULL, '2021-06-08 09:06:44', '2021-06-29 16:20:49', '1');

As colunas mais importantes são: Empresa/Filial/Local e Action, obvio que se não tiver CODE não vai funcionar.

Como foi adicionado Empresa 9999, Filial 9999 e Local 9999 ele vai criar dados para todos as empresas, filiais e locais, porém se você deseja gerar somente para uma filial/local é possível configurar somente ela, logo o XFS será só para filial do entry point configurado.

Action deve ser XFS. {.is-warning}

  • Estrutura da configuração:
ChaveTipoDescrição
fieldsArraySelect que montará o auth do xmova com idEmpresa, idFilial e idLocal junto com o resultado consulta, então no caso insert do entry point que já possui uma configuração, o resulta do auth será todos os equipamentos ativos e seu SEQ_DB será representado por seqEquipamento. Ex.: "auth": {"idEmpresa": 1, "idFilial": 1, "idLocal": 1, "seqEquipamento" : 100} Essa é só uma parte se tiver 500 equipamentos vai ter um auth para cada eles nesse formato.
entitiesArrayCada item desse array são as entidades que são filtradas a partir do dos dados do Auth, então por exemplo se eu for enviar a Classe do Equipamento, é passado o seqEquipamento e seus dados são filtrados.
empresaFilialLocalArraySão as entidade que são iguais para todas as filais, porém, o seu SEQ_DB pode variar de filial/local ou mesmo os dados são diferentes, porém, é o mesmo para todos os auths dela. Exemplo: Funcionário, ele pode ser diferente de uma filial/local para outra, porém, todos os equipamentos de um filial/local deve ter toda a lista de Funcionário da sua filial/local porque de um dia para o outro pode vim o funcionário que estava operando no equipamento A para operar no B.
sharedArraySão declaradas aqui todas as entidades que são iguais e compartilhadas por todas as filiais e locais, são aquelas que são declaradas como 9999 e estão mapeadas como compartilhadas no NFS para filial e local.
installCodesArrayUma chave imporatente e deve conter os installCodes do coletor, ele pode ser o install code de uma outra filial/local, por exemplo, na Suzano temos a empresa Suzano MVI e Suzano SAL, o coletor (XFS Server) fica instalado nele onde ele passa nas gruas da Suzano SAL(XFS Client) coletando e enviados os dados, porém, nessse caso precisa de uma configuração onde será gerado o coletor dizendo quais são os installCodes que existira em seu arquivo para ser obtido futuramente. No caso devo vim na Configuração do Suzano SAL, gerar o Arquivo e depois sincronizar os dados no coletor. Antes de enviar os dados para o xMova são enviados os installCodes que ele deve fazer a requisição, depois eles mesmo são devolvidos em outra requisição para a Web, onde envia todos os auths que o xMova requisitou

Gerar Arquivo

Para Gerar o Arquivo é necessário ter acesso a Admin Console > Acessar XFS e depois de já configurado é só clicar no botão Gerar Arquivo dentro no menu ações.

acoes

Após isso é enviada uma mensagem para uma fila de processamento para que o arquivo seja gerado em background, assim em Log de Geração vai mostrar uma nova linha assim que começar o processamento, para isso hoje ainda é necessário apertar F5, não costuma demorar.

acoes

E quando terminar

acoes

Edição das configurações

Para editar uma configuração existente você pode acessar como admin, ir no menu Admin Console > Acessar XFS nisso vai ter seu entry point, onde tera um editor de código sempre validando o JSON, e também é possível alterar a empresa, filial e local.

acoes

Gerar por EntryPoint

Caso deseje gerar por entry point o código deve ser:

$xfsService = new XMovaFieldServerService();
$saveLogXfsRepo = new SaveLogXFSRepository();
$updateLogXfsRepo = new UpdateLogXFSRepository();
$user = xDS::getUser();

$log = $saveLogXfsRepo->save([
    'name' => 'EntryPoint', // pode ser qualquer nome
    'progress' => BackgroundProgressStatus::DOING,
    'userSeq' => $user->SEQ_DB, 
    'empresa' => $user->EMPRESA_ATUAL,
    'filial' => $user->FILIAL_ATUAL,
    'local' => $user->LOCAL_ATUAL,
    'corporate' => false,// true ou false, se true sobreescreve filial/local com 9999
]);
$result = $xfsService->generateXFSDataFile();
$updateLogXfsRepo->update([
    'seqDb' => $log[2],
    'progress' => BackgroundProgressStatus::DONE, 
    'fileName' => $result['fileName']
]);

Com isso é possível acompanhar pela tela de Log do XFS.

Exemplos

Suzano MVI

{
    "auths": {
        "187123701": {
            "rangeIps": {

            },
            "fields": [
                "SELECT seq_db seqEquipamento, codigo codigoEquipamento FROM app_eqp WHERE ativo=1 "
            ],
            "entities": [
                "Atividade_Programada_Mobile"
            ],
            "empresaFilialLocal": [],
            "share": []
        }
    },
    "installCodes": [220123350, 187123701]
}

Suzano SAL

{
    "auths": {
        "220123350": {
            "fields": [
                "SELECT seq_db seqEquipamento FROM app_eqp WHERE ativo=1"
            ],
            "entities": [
                "Funcionario_Mobile",
                "Equipamento_Mobile",
                "Eqp_Classe_Mobile",
                "Oper_Mobile",
                "Oper_Grupo_Mobile"
            ],
            "empresaFilialLocal": [
                "UPs_Mobile"
            ],
            "shared": []
        }
    },
    "installCodes": [220123350, 187123701]
}

xMova

Hoje para trabalharmos com mensagens entre celular e painel tem a tabela MENSAGEM_MOBILE fixa no processamento, ela deve ser declarada no nfs_core_ds_tabela como tipo 4.

Quando tem um novo apontamento é inserido nessa tabela e enviado de volta ao mobile com o SEQ_DB preenchido. Depois o mobile altera as informações de acordo com o fluxo e às reenvia para a web onde o SEQ_DB é pesquisado pelo ativo e a informação daquela linha é atualizada.

Muito cuidado que ao fazer isso geralmente o apontamento de mensagem é desativo quando é lido no xMova, com isso ao reprocessar pode acontecer de gerar um erro porque no processamento com valor 97, que é erro ao executar um insert.

Exemplo: Modelagem

Exemplo de como modelar um campo do apontamento enviado pelo mobile, para gravar na tabela real de apontamento no banco de dados do cliente

Mobile (xMova)

Na Aba: Model

  • Criar um novo campo: -> valor decimal
Apontamento sync=out cleanupDays=7
	id inc
	
	boletim Boletim
	seqEquipamento SeqEquipamento

	SEQ_DB_DEVICE_MASTER long autofill=boletim.SEQ_DB_DEVICE
	SEQ_DB_DEVICE long uuid
	
	data Now timeTypeField=tipoData
	flagOnline int notFill onlineFlagCreate
	location Location inlineData

	foto Binary picture
	observacao Str
	valor decimal
	server name=EQP_Apt_Foto`

WEB (NFS)

  • Configurar o campo na tabela nfs_core_par_parametros_mobile

    • Definir de onde vem (nome do campo no mobile) e para onde vai (nome do campo na tabela real)
    • Definir o TIPO
    • Definir o código do aplicativo no INSTALLCODE ou utilizar o PARAM_GROUP de acordo com a tabela app_simovaapps clipboard_-_20_de_janeiro_de_2022_às_09_07(1).png
  • Configurar o campo na Modelagem de Campos (nfs_core_ds_tabela_campo) clipboard_-_20_de_janeiro_de_2022_às_09_07.png

  • Acessar o Painel > Admin Console > DS/DDL full

    • Esta ação cria o campo na tabela real clipboard_-_20_de_janeiro_de_2022_às_09_08.png
  • Verificar se o campo foi criado na tabela real clipboard_-_20_de_janeiro_de_2022_às_09_11.png

FAQ - Perguntas frequentes

Soluções para perguntas do grupo Devs

Apontamentos

Como criar uma apontamento a partir de um entrypoint? {.is-info}

Quando for necessário criar um apontamento a partir de um entry point devido à alguma regra de negócio basta utilizar o campo SOURCE da tabela para descrever a origem, exemplo:

$value['SOURCE'] = "ENTRYPOINT";

Controle de acesso

Por que o menu controle de acesso não está sendo exibido? {.is-info}

Para que o usuário tenha acesso ao menu controle de acesso é necessário que o mesmo seja admin ou Super admin e ter permissão no acl *, verifique os grupos a que este usuário pertence e execute as correções necessárias: https://wiki.simova.ws/e/pt-br/controle_acesso/controle_acesso

  • permissão no acl: O usuário precisa ter acesso a uma das seguitnes rotas:
/acl/groups
/acl/users
/acl/permissionRoute
/acl/permissionField

Utils

Como pegar o campo nome da tabela nfs_org_local? {.is-info}

Para obter o seqDb da filial ou do local pode-se usar a função getUser().

$dados_user = xDS::getUser();
$seq_local_atual = $dados_user->LOCAL_ATUAL;
$seq_filial_atual = $dados_user->FILIAL_ATUAL;

Depois utilizar a funcao getNamesByLocalSeqDb para obter os nomes.

$name_local_atual = Utils::getNamesByLocalSeqDb($seq_local_atual);
$name_filial_atual = Utils::getNamesByLocalSeqDb($seq_filial_atual);
echo 'Local: ' . $name_local_atual['local'] . '<br>Filial: ' . $name_filial_atual['filial']; 

Resultado (exemplo):

Local: Cianorte
Filial: Cianorte

Dashboards

Como selecionar a filial ou local para trazer os dados {.is-info}

Pode ser adicionado no período a filial e o local para que possa obter os dados que correspondem apenas a estes, podendo ser inserido os dois ou apenas um ou outro, caso não sejam colocados serão trazidos os dados da filial que o usuario esta autenticado, como no exemplo abaixo:

"periodo": {
    "end": "2020-01-06 17:54:43",
    "begin" : "2020-01-01 17:54:40",
    "filial": "NomeDaFilial",
    "local": "NomeDoLocal"
  }

Exemplo: clique aqui.

Como adicionar uma gráfico em um relatório {.is-info}

É possível gerar uma imagem PNG de um gráfico configurado na tabela nfs_generic_chart ou passar diretamente uma configuração de um gráfico no modelo Highcharts (modelos: https://www.highcharts.com/demo) Exemplo: clique aqui.

Como adicionar uma ordenação padrão para os gráficos dos dashboards? {.is-info}

Foi adicionado um novo campo na tabela nfs_generic_chart chamado CHART_ORDER para controlar a ordenação padrão dos gráficos dentro dos dashboards, para definir essa ordem, basta configurar nesse campo valores inteiros, sendo 1 para exibição do primeiro gráfico, 2 para o segundo gráfico a ser exibido e assim por diante. Para mais detalhes clique aqui.

Folha tarefa

Como configurar a folha tarefa? {.is-info}

As configurações da Folha Tarefa foram atualizadas para deixar de ser uma custom a fim de ser configurada para outros clientes, no link abaixo tem o passo a passo de como realizar a nova configuração. As permissões de usuário e futuras funcionalidades não funcionarão no modelo de configuração antigo. passo a passo

Como configurar tabelas com prefixos na folha tarefa (Ex: MO)? {.is-info}

OBS: as configurações descritas no código segue o modelo padrão de nomenclatura de tabelas, mas existem bases onde as tabelas tem o prefixo MO na nomenclatura, para que funcione é necessário configurar os nomes das tabelas na tabela nfs_core_par_parametros para subtituir na configuração original como no exemplo clique aqui

Painel

Como exibir o painel em modo Kanban? {.is-info}

Para que o painel possar ser aberto diretamente no modo Kanban precisa ser adicionada uma configuração através da tabela nfs_qig_panels adicionando "startMode": "kanban" no OPTIONS fará com que o painel respectivo seja exibido diretamente no modo de exibição Kanban. configuração do painel

Tela de detalhes

Como configurar o Split de Boletim na tela de detalhes? {.is-info}

Para configurar o Split de Boletim é necessário seguir os 3 passos abaixo:

1- Na tabela nfs_core_par_parametros configurar o parâmetro SPLIT_ENABLED = 1

2- Na tabela nfs_core_ds_tabela configurar MOBILE_TABLE = 2

3- Na tabela nfs_acl_grupo_permissao configurar o SIUD deixando o insert = 1

configuração completa

Como configurar cores na tela de detalhes? {.is-info}

Obs: as cores funcionam apenas na nova tela de detalhes.

A configuração das cores foi criada para facilitar a visualização e identificação de boletins integrados e fechados que podem ser configurados no campo SECONDARY_TABLE_OPTIONS da tabela nfs_qig_details e apontamentos com valores especificos que podem ser configurados no campo ADDITIONAL_TABLES da tabela nfs_qig_details. configuração completa

Usuário third party

Como vincular o usuário logado com o registro de efetivo_funcionário do mesmo? {.is-info}

No NFS tem a possíbilidade de vincular um usuário a uma tabela app, onde chamamos ela de terceiro, com isso o cliente tem a possibilidade liberar um usuário externo para consultar relatórios e paineis. Com isso nas funcionalidade que usam o Nfs Query Builder vai ter o parâmetro :NFS_THIRD_PARTY que vai conter o SEQ_DB da tabela de Terceiro. A primeira configuração é definir quais usuários serão private user, depois é só ir na nfs_acl_usuario e na coluna TIPO_USUARIO_SEQ_DB e difinir igual a 5, também é possível realizar isso pelo Controle de Acesso. configuração completa

Validação View

Como vincular o usuário logado com o registro de efetivo_funcionário do mesmo? {.is-info}

A validação view é muito usada para exibir ou não um campo. No caso acima o checkbox FLAG_ROLETE (É Rolete Inferior?) somente será exibido quando a FLAG_FERRAMENTA_MEDICAO (Usa Ferramenta de Medição?) for verdadeira, ou em outra palavras, marcada como SIM.

Logo também é verdade que qualquer VALIDACAO_VIEW que depende da FLAG_ROLETE será ou não exibida baseada na sua exibição, exemplo configuração completa

Controle de Acesso

Como vincular o usuário logado com o registro de efetivo_funcionário do mesmo? {.is-info}

OBS: a tabela CONTROLE_ACESSO deverá ser excluida e utilizar a tabela SIMOVAAPPS para realizar as configurações. Para remover o menu CONTROLE_ACESSO colocar o ignore na tabela nfs_core_menu não funciona. A solução para a tabela CHAVE_ACESSO é excluir a tabela, pois agora será utilizada a tabela SIMOVAAPPS para fazer as configurações, logo pode ser removida a tabela CHAVE_ACESSO, pois já existe a validação no core que não exibirá esse menu quando não houver a tabela. configuração completa

Galeria - UPLOAD de Imagens

Como configurar o UPLOAD de arquivos e imagens? {.is-info}

Na tabela nfs_ds_core_tabela_campo quando este for do tipo UPLOAD e se quer fazer o upload/download de arquivos que não sejam imagens precisa configurar o campo VALIDAÇÃO no formato desejado sem misturar extensões de imagem com extensões de arquivos Ex: **(0:*pdf;1:docx;2:doc;) E para que funcione, as extensões configuradas precisam estar também na tabela nfs_par_parametros nos campos: para arquivos de texto, e arquivos que não sejam imagens CRUD_EXTENSOES_PERMITIDAS_UPLOAD -> 0:txt;1:jpeg;2:png;3:docx para arquivos de imagens-> CRUD_EXTENSOES_UPLOAD_PERMITIDAS_VISUALIZACAO -> png:image/png;jpeg:image/jpeg

configuração completa

Erros de Processamento

Como configurar o UPLOAD de arquivos e imagens? {.is-info}

Verificar se o parametros_mobile esta configurado corretamente, pois se estiver errada a configuração não vai realizar o processaemnto.

status 1 = não processado - ocorre normalmente por erro no parametro_mobile.

outros status serão adicionados

verificar se está configurado corretamente o campo seq_db_device_master ou no mobile está pegando o seq_db_device_master

Manipulação de Imagens

Como rotacionar uma imagem? {.is-info}

Para rotacionar uma imagem em um entryPoint basta utilizarmos a função convertOrientationImage como no exemplo

Para rotacionar uma imagem no CRUD, basta configurar o campo como tipo UPLOAD e utilizar os icones de rotação conforme mostrado nas imagens da galeria simova: clique aqui

Painel de gestão a vista

É possivel adicionar filtros que não tenham relação com a tabela principal? {.is-info}

Os filtros DEVEM ter relacionamento com a tabela principal caso a mesma esteja configurada.

O sistema quando vai enviar os QIIs para serem trabalhados no entryPoint, vai realizar um filtro buscando pela coluna que esta sendo referenciada no filtro, podemos utilizar o seguinte caso como exemplo:

  • Temos o painél baseado na tabela EQP;
  • A tabela EQP possui o campo FK EQP_MODELO;
  • A tabela EQP_MODELO possui o campo FK EQP_TIPO;
  • A tabela EQP não possui nenhum campo relacionado diretamente com EQP_TIPO;
  • Com isso se adicionarmos um filtro para EQP_TIPO no nosso painél de EQPs:
EQP.EQP_TIPO = $valorDoFiltro_eqpTipo

Com isso o sistema não enviará os QIIs para serem trabalhados no entryPoint. Tratativa: adicionar o campo FK EQP_TIPO na tabela EQP.

Cards indicadores

É possivel personalizar a aparência dos cards indicadores? {.is-info}

Sim, é possivel aplicar estilos CSS nos cards, alterando cor, borda, fonte e etc usando o objeto de configuração dos indicadores.

Saiba mais

Relatórios

É possivel editar o template de relatórios pelo NFS? {.is-info}

Sim, o NFS possui o Editor de relatórios que atualmente permite a edição de templates base.twig.

Saiba mais

EntryPoint

Test imagem:

Test Image

Issue Type

Os jiras do NFS Core tem os seguintes tipos:

  • Task: É um tarefa que não ainda definida se é uma New Feature ou Improvement
  • New Feature: É uma nova funcionalidade que não existe no sistema
  • Improvement: É o aprimoramento de uma funcionalidade existente
  • Service: É um atendimento sem alteração no código
  • Bug: É uma correção

image