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

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>