fannie/classlib2.0/FannieReportPage.php
<?php
/*******************************************************************************
Copyright 2012 Whole Foods Co-op
This file is part of CORE-POS.
IT CORE is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
IT CORE is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
in the file license.txt along with IT CORE; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*********************************************************************************/
use COREPOS\Fannie\API\data\FileData;
if (!class_exists('FanniePage')) {
include_once(dirname(__FILE__).'/FanniePage.php');
}
/**
@class FanniePage
Class for drawing screens
*/
class FannieReportPage extends FanniePage
{
public $required = True;
/**
Description of the report
*/
public $description = "
Base class for creating reports.
";
public $page_set = 'Reports';
/**
Assign report to a "set" of reports
*/
public $report_set = '';
public $discoverable = true;
/*
*/
public function __construct() {
// To set authentication.
parent::__construct();
}
/**
Function for drawing page content.
form_content and report_content are provided
by default.
*/
protected $content_function = "form_content";
/**
If fields are present in the request, the
form has been submitted and report can be
displayed
*/
protected $required_fields = array();
/**
Define report headers. Headers are necessary if sorting is desired
*/
protected $report_headers = array();
/**
Define report format. Valid values are: html, xls, csv, txt
*/
protected $report_format = 'html';
/**
Enable caching of SQL data. Valid values are: none, day, month
*/
protected $report_cache = 'none';
/**
Allow for reports that contain multiple separate tables of data.
If all the reports are the same width, using META_BLANK and/or
META_REPEAT_HEADERS may be preferrable.
*/
protected $multi_report_mode = False;
protected $multi_counter = 1;
protected $header_index = 0;
/**
Option to enable/disable javascript sorting
*/
protected $sortable = true;
/**
Apply CSS to table but not sorting JS.
May become default behavior if it does
not mess up current unsorted reports
*/
protected $no_sort_but_style = false;
/**
Which column to sort by default
*/
protected $sort_column = 0;
/**
Sort direction. 0 is ascending, 1 is descending
*/
protected $sort_direction = 0;
/**
false => use tablesorter v. 2.0.5b
true => use tablesorter v. 2.22.1 (bootstrap compatible)
*/
protected $new_tablesorter = false;
/**
Disable inclusion of jquery. In rare cases the report
content may be embedded in a page that already included
jquery and multiple copies included can cause problems.
*/
protected $no_jquery = false;
/**
Column containing chart labels.
*/
protected $chart_label_column = 0;
/**
Column(s) containing chart data values.
An empty array signifies means every column
except the label contains data.
*/
protected $chart_data_columns = array();
protected $queueable = false;
protected $form;
/**
Assign meta constant(s) to a row's "meta" field
for special behavior.
Bold is self-explanatory. Blank will insert a blank
line and repeat headers will repeat the report_headers.
The latter two will terminate the current <tbody> and
start a new one. This breaks the report into separately
sortable chunks.
*/
const META_BOLD = 1;
const META_BLANK = 2;
const META_REPEAT_HEADERS = 4;
const META_CHART_DATA = 8;
const META_COLOR = 16;
protected function hasAllFields($fields)
{
foreach($fields as $field) {
if (FormLib::get($field, '') === '') {
return false;
}
}
return true;
}
/**
Handle pre-display tasks such as input processing
@return
- True if the page should be displayed
- False to stop here
The default version will check required_fields
to determine whether the form_content or
report_content method should be called. It
also the value of "excel" for the request and
sets necessary output options.
*/
public function preprocess()
{
if ($this->hasAllFields($this->required_fields)) {
$this->form = new \COREPOS\common\mvc\FormValueContainer();
$this->content_function = 'report_content';
if ($this->config->get('WINDOW_DRESSING')) {
$this->hasMenus(true);
$this->new_tablesorter = true;
} else {
$this->hasMenus(false);
}
$this->formatCheck();
} else {
$this->content_function = 'form_content';
}
return true;
}
/**
Define the data input form
@return An HTML string
*/
public function form_content()
{
}
protected function hasWarehouseSource()
{
$plugins = $this->config->get('PLUGIN_LIST');
if (FormLib::get('data-source') === 'CoreWarehouse' && is_array($plugins) && in_array('CoreWarehouse', $plugins)) {
$reflector = new ReflectionClass($this);
$source = \COREPOS\Fannie\Plugin\CoreWarehouse\CwReportDataSource::getDataSource($reflector->getName());
return $source !== false ? $source : false;
}
return false;
}
function bodyContent()
{
$plugins = $this->config->get('PLUGIN_LIST');
if (($source=$this->hasWarehouseSource()) !== false) {
$source_select = '
<div class="col-sm-12" id="data-source-fields">
<div class="form-group">
<label>Data Source</label> <select name="data-source" class="form-control" id="select-data-source">
<option>Standard</option>
<option>CoreWarehouse</option>
</select>
</div>';
$source_select = array_reduce($source->additionalFields($reflector->getName()),
function ($carry, $field) {
return $carry . '<div class="form-group">' . $field->toHTML() . '</div>';
},
$source_select
);
$source_select .= '</div>';
$source_select = preg_replace('/\s\s+/', '', $source_select);
$this->addOnloadCommand("\$('#primary-content form').prepend('$source_select');\n");
$this->addOnloadCommand("\$('#primary-content .cw-field').attr('disabled', true);\n");
$this->addOnloadCommand("\$('#select-data-source').change(function(){ if ($(this).val()=='CoreWarehouse') { $('#primary-content .cw-field').attr('disabled', false); } else { $('#primary-content .cw-field').attr('disabled', true); } });\n");
}
return $this->form_content();
}
protected function getData()
{
$data = array();
$cached = $this->checkDataCache();
if ($cached !== false) {
$data = $cached;
} else {
/**
Use CoreWarehouse data source if requested & available
Fail back to FannieReportPage::fetch_report_data()
if a data source cannot be found or data source fails
to handle the request
*/
if (($source=$this->hasWarehouseSource()) !== false && FormLib::get('data-source') === 'CoreWarehouse') {
$data = $source->fetchReportData($reflector->getName(), $this->config, $this->connection);
if ($data === false) {
$data = $this->fetch_report_data();
}
} else {
$data = $this->fetch_report_data();
}
$this->freshenCache($data);
}
return $data;
}
protected function multiContent($data)
{
$output = '';
if ($this->report_format != 'xls') {
foreach($data as $report_data) {
$this->assign_headers();
// calculate_footers() here because it can affect headers.
$footers = $this->calculate_footers($report_data);
$headers = $this->report_headers;
$this->header_index = 0;
$output .= $this->render_data($report_data,$headers,
$footers,$this->report_format);
if ($this->report_format == 'html') {
$output .= '<br />';
} elseif ($this->report_format == 'csv' || $this->report_format == 'txt') {
$output .= "\r\n";
}
}
} else {
/**
For XLS multi-report ouput, re-assemble the reports into a single
long array of rows (dataset).
*/
$xlsdata = array();
foreach($data as $report_data) {
$this->assign_headers();
// calculate_footers() here because it can affect headers.
$footers = $this->calculate_footers($report_data);
$this->header_index = 0;
if (!empty($this->report_headers)) {
$headers1 = $this->select_headers(true);
$xlsdata[] = $headers1;
}
$report_data = $this->xlsMeta($report_data);
$xlsdata = array_reduce($report_data,
function($carry, $line) {
$carry[] = $line;
return $carry;
},
$xlsdata
);
if (!empty($footers)) {
// A single footer row
if (!is_array($footers[0])) {
$xlsdata[] = $footers;
// More than one footer row
} else {
$xlsdata = array_reduce($footers,
function($carry, $footer) {
$carry[] = $footer;
return $carry;
},
$xlsdata
);
}
}
$xlsdata[] = array('');
$this->multi_counter++;
}
$output = $this->render_data($xlsdata,array(),array(),'xls');
}
return $output;
}
protected function singleContent($data)
{
// NOT multi_report_mode
$this->assign_headers();
$footers = $this->calculate_footers($data);
/* $data may contain REPEAT_HEADERS calls
* If the 2nd+ headers should be different then report_headers
* has two dimensions.
*/
return $this->render_data($data,$this->report_headers,
$footers,$this->report_format);
}
/**
Define the report display
@return An HTML string
Generally this function is not overriden.
This will first check the cache to see
if data for this report has been saved. If not,
it will look up the data by calling the
fetch_report_data function. That function should
be overriden.
Once the data is retrieved, this will call
the calculate_footers function on the data.
Footers are not required, but it's useful for some
final calculations.
Finally, the render_data function is called. Overriding
that is not recommended.
*/
function report_content()
{
if ($this->queueable && FormLib::get('queued', false)) {
return $this->queueReport();
}
$data = $this->getData();
$output = '';
if ($this->multi_report_mode) {
$output = $this->multiContent($data);
} else {
$output = $this->singleContent($data);
}
return $output;
}
/**
Displays both form_content and report_content
@return html string
*/
public function both_content()
{
$ret = '';
if ($this->report_format == 'html') {
$ret .= $this->form_content();
$ret .= '<hr />';
}
$ret .= $this->report_content();
return $ret;
}
/**
Calculate a footer row
@param $data an two-dimensional array of data
@return array of footer values
Principally, footers are separate from data
so they can be marked in such in HTML rendering
and stay at the bottom when data sorting changes.
This function may also be used to set values
for headers or default sorting. On more elaborate reports,
the number of columns may vary depending on what options
are selected. This function is always called so those values
will be set reliably even if caching is enabled.
*/
public function calculate_footers($data)
{
return array();
}
/**
* Assign new values to $report_headers,
* which is intially assigned in the report,
* usually for 2nd+ reports in multi_report_mode.
*/
public function assign_headers()
{
}
/**
* Return a single-dimension array of headers (column-heads).
@param
@return array of header values
*
* Allow for but not require different headers on each report.
* Input may be one- or two-dimensional.
* If the latter, index is header_index.
* If headers[x] doesn't exist use the last one that does exist
* or empty if none exists.
*/
public function select_headers($incrIndex=False)
{
$headers = array();
$hIndex = $this->header_index;
if (is_array($this->report_headers[0])) {
if (isset($this->report_headers[$hIndex])) {
$headers = $this->report_headers[$hIndex];
} else {
$hIndex = (count($this->report_headers) - 1);
if ($hIndex >= 0) {
$headers = $this->report_headers[$hIndex];
}
}
} else {
$headers = $this->report_headers;
}
if ($incrIndex) {
$this->header_index++;
}
return $headers;
}
/**
Apply formatting to data. This method can be used to
add markup to records - e.g., links to other content.
@param $data two-dimensional array of report data
@return two-dimensional array of report data
*/
protected function format($data)
{
return $data;
}
protected function getCacheKey()
{
$reflector = new ReflectionClass($this);
$qstr = filter_input(INPUT_SERVER, 'QUERY_STRING');
$qstr = str_replace('&excel=xls', '', $qstr);
$qstr = str_replace('&excel=xlsx', '', $qstr);
$qstr = str_replace('&excel=csv', '', $qstr);
$qstr = str_replace('&excel=txt', '', $qstr);
$qstr = str_replace('?excel=xls', '', $qstr);
$qstr = str_replace('?excel=xlsx', '', $qstr);
$qstr = str_replace('?excel=csv', '', $qstr);
$qstr = str_replace('?excel=txt', '', $qstr);
$qstr = str_replace('?no-cache=1', '', $qstr);
$qstr = str_replace('&no-cache=1', '', $qstr);
return $reflector->getName() . $qstr;
}
/**
Look for cached SQL data
Data is stored in the archive database, reportDataCache table.
The key column is an MD5 hash of the current URL (minus the excel
parameter, if present). This means your forms should use type GET
if caching is enabled.
The data is stored as a serialized, gzcompressed string.
*/
protected function checkDataCache()
{
if (isset($_GET['no-cache']) && $_GET['no-cache'] === '1') {
return false;
}
$data = COREPOS\Fannie\API\data\DataCache::getFile
($this->report_cache,
$this->getCacheKey()
);
return $data ? unserialize(gzuncompress($data)) : false;
}
/**
Store SQL data in the cache
@param $data the data
@return True or False based on success
See checkDataCache for details
*/
protected function freshenCache($data)
{
return COREPOS\Fannie\API\data\DataCache::putFile(
$this->report_cache,
gzcompress(serialize($data)),
$this->getCacheKey()
);
}
/**
Extra, non-tabular information prepended to
reports
@return array of strings
*/
public function report_description_content()
{
return array();
}
/**
Standard lines to include above report data
@param $datefields [array] names of one or two date fields
in the GET/POST data. The fields "date", "date1", and
"date2" are detected automatically.
@return array of description lines
*/
protected function defaultDescriptionContent($rowcount, $datefields=array())
{
$ret = array();
$ret[] = '<div class="hidden-print">' . $this->header . '</div>';
$ret[] = '<div class="hidden-print">' . _('Report generated') . ' ' . date('l, F j, Y g:iA') . '</div>';
$ret[] = '<div class="hidden-print">Returned ' . $rowcount . ' rows</div>';
$dt1 = false;
$dt2 = false;
if (count($datefields) == 1) {
$dt1 = strtotime(FormLib::get($datefields[0]));
} elseif (count($datefields) == 2) {
$dt1 = strtotime(FormLib::get($datefields[0]));
$dt2 = strtotime(FormLib::get($datefields[1]));
} elseif (FormLib::get('date') !== '') {
$dt1 = strtotime(FormLib::get('date'));
} elseif (FormLib::get('date1') !== '' && FormLib::get('date2') !== '') {
$dt1 = strtotime(FormLib::get('date1'));
$dt2 = strtotime(FormLib::get('date2'));
}
if ($dt1 && $dt2) {
$ret[] = _('From') . ' '
. date('l, F j, Y', $dt1)
. ' ' . _('to') . ' '
. date('l, F j, Y', $dt2);
} elseif ($dt1 && !$dt2) {
$ret[] = _('For') . ' ' . date('l, F j, Y', $dt1);
}
return $ret;
}
/**
Extra, non-tabular information appended to
reports
@return array of strings
*/
public function report_end_content()
{
return array();
}
/**
Get the report data
@return a two dimensional array
Actual SQL queries go here!
If using multi_report_mode, this should
return an array of two dimensional arrays
where each two dimensional arrays contains
a report's data.
*/
public function fetch_report_data()
{
return array(
array('2000-01-01', '1-1-1', '1234567890123', 'Sample Data'),
);
}
/**
Format data for display
@param $data a two dimensional array of data
@param $headers a header row (optional)
@param $format output format (html | xls | csv | txt)
@return formatted string
*/
public function render_data($data,$headers=array(),$footers=array(),$format='html')
{
$url = $this->config->get('URL');
$ret = "";
switch(strtolower($format)) {
case 'html':
if ($this->multi_counter == 1) {
if (!$this->new_tablesorter) {
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
// windows has trouble with symlinks
$this->addCssFile($url . 'src/javascript/tablesorter-2.0.5b/themes/blue/style.css');
$this->addCssFile($url . 'src/css/print.css');
} else {
$this->addCssFile($url . 'src/javascript/tablesorter/themes/blue/style.css');
$this->addCssFile($url . 'src/css/print.css');
}
} else {
$this->addCssFile($url . 'src/javascript/tablesorter-2.22.1/css/theme.bootstrap.css');
}
if (!$this->window_dressing) {
$ret .= '<!DOCTYPE html><html><head>' .
'<meta http-equiv="Content-Type" ' .
'content="text/html; charset=iso-8859-1">' .
'</head><body>';
$ret .= '<script type="text/javascript">
function highlightCell(e)
{
if ($(this).css(\'background-color\') != \'rgb(255, 255, 0)\') {
$(this).css(\'background-color\',\'yellow\');
} else {
$(this).css(\'background-color\',\'\');
}
}
</script>';
}
$ret .= '<div id="pre-report-content">';
if (empty($_POST)) {
$uri = filter_input(INPUT_SERVER, 'REQUEST_URI');
$ret .= '<div class="hidden-print">';
if (\COREPOS\Fannie\API\data\DataConvert::excelSupport()) {
$ret .= sprintf('<a href="%s%sexcel=xls">Download Excel</a>
',
$uri,
(strstr($uri, '?') === false ? '?' : '&')
);
}
$json = FormLib::queryStringtoJSON(filter_input(INPUT_SERVER, 'QUERY_STRING'));
$ret .= sprintf('<a href="%s%sexcel=csv">Download CSV</a>
<a href="%s%sexcel=txt">Download TXT</a>
<a href="?json=%s">Back</a></div>',
$uri,
(strstr($uri, '?') === false ? '?' : '&'),
$uri,
(strstr($uri, '?') === false ? '?' : '&'),
base64_encode($json)
);
} else {
$ret .= '<form id="downloadForm" method="post">';
foreach ($_POST as $key => $val) {
if (is_array($val)) {
foreach ($val as $v) {
$v = str_replace('"', '', $v);
$ret .= "<input type=\"hidden\" name=\"{$key}[]\" value=\"{$v}\" />";
}
} else {
$val = str_replace('"', '', $val);
$ret .= "<input type=\"hidden\" name=\"{$key}\" value=\"{$val}\" />";
}
}
$ret .= '<input type="hidden" name="excel" id="excelType" /></form>';
if (\COREPOS\Fannie\API\data\DataConvert::excelSupport()) {
$ret .= '<a href="" onclick="$(\'#excelType\').val(\'xls\');$(\'#downloadForm\').submit(); return false;">Download Excel</a>
';
}
$ret .= '<a href="" onclick="$(\'#excelType\').val(\'csv\');$(\'#downloadForm\').submit(); return false;">Download CSV</a>';
$ret .= ' ';
$ret .= '<a href="" onclick="$(\'#excelType\').val(\'txt\');$(\'#downloadForm\').submit(); return false;">Download TXT</a>';
}
$ret = array_reduce($this->defaultDescriptionContent(count($data)),
function ($carry, $line) {
return $carry . (substr($line,0,1)=='<'?'':'<br />').$line;
},
$ret
);
$ret = array_reduce($this->report_description_content(),
function ($carry, $line) {
return $carry . (substr($line,0,1)=='<'?'':'<br />').$line;
},
$ret
);
$ret .= '</div>';
}
$tableID = 'reportTable' . $this->multi_counter;
if ($this->sortable || $this->no_sort_but_style) {
$ret .= '<table class="mySortableTable tablesorter tablesorter-bootstrap" id="' . $tableID . '">';
} else {
$ret .= '<table class="mySortableTable" cellspacing="0"
id="' . $tableID . '" cellpadding="4" border="1">' . "\n";
}
break;
case 'csv':
case 'txt':
if ($this->multi_counter == 1) {
$sep = strtolower($format) == 'txt' ? "\t" : ',';
foreach ($this->defaultDescriptionContent(count($data)) as $line) {
$ret .= $this->csvLine(array(strip_tags($line)), $sep);
}
foreach ($this->report_description_content() as $line) {
$ret .= $this->csvLine(array(strip_tags($line)), $sep);
}
}
case 'xls':
break;
}
if (!empty($headers)) {
$headers1 = $this->select_headers(False);
if (!$this->multi_report_mode && strtolower($format) != 'xls') {
$this->header_index++;
}
switch(strtolower($format)) {
case 'html':
$ret .= '<thead>' . "\n";
$ret .= $this->htmlLine($headers1, True);
$ret .= '</thead>' . "\n";
break;
case 'csv':
$ret .= $this->csvLine($headers1);
break;
case 'txt':
$ret .= $this->csvLine($headers1, "\t");
break;
case 'xls':
break;
}
}
for ($i=0;$i<count($data);$i++) {
switch(strtolower($format)) {
case 'html':
if ($i==0) {
$ret .= "<tbody>\n";
}
$ret .= $this->htmlLine($data[$i]);
if ($i==count($data)-1) {
$ret .= "</tbody>\n";
}
break;
case 'csv':
$ret .= $this->csvLine($data[$i]);
break;
case 'txt':
$ret .= $this->csvLine($data[$i], "\t");
break;
case 'xls':
break;
}
}
if (!empty($footers)) {
switch(strtolower($format)) {
case 'html':
$ret .= '<tfoot>';
// A single footer row
if (!is_array($footers[0])) {
$ret .= $this->htmlLine($footers, True);
// More than one footer row
} else {
foreach ($footers as $footer) {
$ret .= $this->htmlLine($footer, True);
}
}
$ret .= '</tfoot>';
break;
case 'csv':
case 'txt':
$sep = strtolower($format) == 'txt' ? "\t" : ',';
// A single footer row
if (!is_array($footers[0])) {
$ret .= $this->csvLine($footers, $sep);
// More than one footer row
} else {
foreach ($footers as $footer) {
$ret .= $this->csvLine($footer, $sep);
}
}
break;
case 'xls':
break;
}
}
switch(strtolower($format)) {
case 'html':
$ret .= '</table>';
$ret = array_reduce($this->report_end_content(),
function ($carry, $line) {
return $carry . (substr($line,0,1)=='<'?'':'<br />').$line;
},
$ret
);
if (!$this->no_jquery && !$this->new_tablesorter) {
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
// windows has trouble with symlinks
$this->addScript($url . 'src/javascript/jquery-1.11.1/jquery-1.11.1.min.js');
} else {
$this->addScript($url . 'src/javascript/jquery.js');
}
}
if (!$this->new_tablesorter) {
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
// windows has trouble with symlinks
$this->addScript($url . 'src/javascript/tablesorter-2.0.5b/jquery.tablesorter.js');
} else {
$this->addScript($url . 'src/javascript/tablesorter/jquery.tablesorter.js');
}
} else {
$this->addScript($url . 'src/javascript/tablesorter-2.22.1/js/jquery.tablesorter.js');
$this->addScript($url . 'src/javascript/tablesorter-2.22.1/js/jquery.tablesorter.widgets.js');
}
$this->addScript($url . 'src/javascript/jquery.floatThead.min.js?date=20181121');
$sort = sprintf('[[%d,%d]]',$this->sort_column,$this->sort_direction);
if ($this->sortable) {
if (!$this->new_tablesorter && count($data) > 0) {
$this->addOnloadCommand("\$('.mySortableTable').tablesorter({sortList: $sort, widgets: ['zebra']});");
} else {
$this->addOnloadCommand("\$.tablesorter.themes.bootstrap['active'] = 'info';");
$this->addOnloadCommand("\$.tablesorter.themes.bootstrap['table'] += ' tablesorter-core table-condensed small';");
$this->addOnloadCommand("\$.tablesorter.themes.bootstrap['header'] += ' table-condensed small';");
$this->addOnloadCommand("\$('.mySortableTable').tablesorter({sortList: $sort, theme:'bootstrap', headerTemplate: '{content} {icon}', widgets: ['uitheme','zebra']});");
}
// by default this seems to use z-index: 2, but that means it floats *above*
// the datepicker, when that is exposed. datepicker apparently uses z-index:
// 1 but we cannot override that, so here we at least keep them the same.
$this->addOnloadCommand("\$('.mySortableTable').floatThead({zIndex: 1});\n");
} elseif ($this->new_tablesorter) {
/**
New bootstrap-themed tablesorter requires more setup to style correctly
even when sorting isn't used. Simply including a CSS file is not sufficient.
*/
$this->addOnloadCommand("\$.tablesorter.themes.bootstrap['active'] = 'info';");
$this->addOnloadCommand("\$.tablesorter.themes.bootstrap['table'] += ' tablesorter-core table-condensed small';");
$this->addOnloadCommand("\$.tablesorter.themes.bootstrap['header'] += ' table-condensed small';");
$this->addOnloadCommand("\$('.mySortableTable thead th').data('sorter', false);\n");
$this->addOnloadCommand("\$('.mySortableTable').tablesorter({theme:'bootstrap', headerTemplate: '{content} {icon}', widgets: ['uitheme','zebra']});");
}
break;
case 'csv':
if (!headers_sent()) {
header('Content-Type: application/ms-excel');
header('Content-Disposition: attachment; filename="'.$this->header.'.csv"');
}
foreach($this->report_end_content() as $line) {
$ret .= $this->csvLine(array(strip_tags($line)));
}
break;
case 'txt':
if (!headers_sent()) {
header('Content-Type: application/ms-excel');
header('Content-Disposition: attachment; filename="'.$this->header.'.txt"');
}
foreach($this->report_end_content() as $line) {
$ret .= $this->csvLine(array(strip_tags($line)), "\t");
}
break;
case 'xls':
// headers empty in multi-report-mode
if (!empty($headers)) {
$headers1 = $this->select_headers(True);
array_unshift($data,$headers1);
}
if (!$this->multi_report_mode) {
$data = $this->xlsMeta($data);
}
for ($i=0;$i<count($data);$i++) {
for ($j=0;$j<count($data[$i]);$j++) {
if (isset($data[$i][$j])) {
$data[$i][$j] = $this->excelFormat($data[$i][$j]);
}
}
}
$xlsdata = $data;
// footers empty in multi-report-mode
if (!empty($footers)) {
// A single footer row
if (!is_array($footers[0])) {
array_push($xlsdata,$footers);
// More than one footer row
} else {
$xlsdata = array_reduce($footers,
function($carry, $footer) {
$carry[] = $footer;
return $carry;
},
$xlsdata
);
}
}
$xlsdata = array_reduce($this->report_end_content(),
function($carry, $line) {
$carry[] = strip_tags($line);
return $carry;
},
$xlsdata
);
$xlsdata = array_merge(array_reduce($this->report_description_content(),
function($carry, $line) {
$carry[] = array(strip_tags($line));
return $carry;
},
array()
),$xlsdata); // prepend
$xlsdata = array_merge(array_reduce($this->defaultDescriptionContent(count($data)),
function($carry, $line) {
$carry[] = array(strip_tags($line));
return $carry;
},
array()
), $xlsdata); // prepend
$ext = \COREPOS\Fannie\API\data\DataConvert::excelFileExtension();
$ret = \COREPOS\Fannie\API\data\DataConvert::arrayToExcel($xlsdata);
header('Content-Type: application/ms-excel');
header('Content-Disposition: attachment; filename="'
. $this->header . '.' . $ext . '"');
break;
}
$this->multi_counter++;
$this->header_index++;
return $ret;
}
/**
Convert keyed array to numerical
indexes and maintain order
*/
public function dekey_array($arr)
{
$ret = array();
if (!is_array($arr)) {
return $ret;
}
foreach($arr as $outer_key => $row) {
$record = array();
foreach($row as $key => $val) {
$record[] = $val;
}
$ret[] = $record;
}
return $ret;
}
/**
Turn array into HTML table row
@param $row an array of data
@param $header True means <th> tags, False means <td> tags
@return HTML string
Javascript sorting utility requires header rows to be <th> tags
*/
public function htmlLine($row, $header=False)
{
$url = $this->config->get('URL');
$meta = 0;
if (isset($row['meta'])) {
$meta = $row['meta'];
unset($row['meta']);
}
$ret = "\t<tr";
if (($meta & self::META_CHART_DATA) != 0) {
$ret .= ' class="d3ChartData"';
}
$ret .= ">\n";
$tag = $header ? 'th' : 'td';
if (($meta & self::META_BOLD) != 0) {
$tag = 'th';
}
if (($meta & self::META_BLANK) != 0) {
$ret = "</tbody>\n<tbody>\n\t<tr>\n";
$header1 = $this->select_headers(False);
// just using headers as a column count
$row = array_map(function ($item) { return null; }, $header1);
}
$color_styles = '';
if (($meta & self::META_COLOR) != 0) {
if (isset($row['meta_background'])) {
$color_styles .= 'background-color:' . $row['meta_background'] . ';';
unset($row['meta_background']);
}
if (isset($row['meta_foreground'])) {
$color_styles .= 'color:' . $row['meta_foreground'] . ';';
unset($row['meta_foreground']);
}
}
if (($meta & self::META_REPEAT_HEADERS) != 0) {
$ret = "</tbody>\n<tbody>\n\t<tr>\n";
$tag = 'th';
$row = array();
$header1 = $this->select_headers(true);
$row = array_filter($header1, function ($item) { return true; });
}
$date = false;
$trans = false;
/* After removing HTML, the cell will be seen as a number
* and aligned right if it matches this pattern:
* Optional leading $, optionally with space(s) after
* Optional - sign
* A digit
* Possibly more decimal points, commas or digits
* Optionally trailing %, optionally with space(s) before
*/
$numberPattern = '/^(\$ *)?(-)?(\d)([.,\d]*)( *%)?$/';
for($i=0;$i<count($row);$i) {
$span = 1;
while(array_key_exists($i+$span,$row) && $row[$i+$span] === null && ($i+$span)<count($row)) {
$span++;
}
$styles = $color_styles;
if ($row[$i] === "" || $row[$i] === null) {
$row[$i] = ' ';
} elseif (is_numeric($row[$i]) && strlen($row[$i]) == 13) {
// auto-link UPCs to edit tool
$row[$i] = \COREPOS\Fannie\API\lib\FannieUI::itemEditorLink($row[$i]);
} elseif (!$header && preg_match('/^LC\d+$/', $row[$i])) {
$row[$i] = \COREPOS\Fannie\API\lib\FannieUI::likeCodeEditorLink($row[$i]);
} else if (!$header && !$date && preg_match('/^\d\d\d\d-\d\d-\d\d$/', $row[$i])) {
// cell contains a date column
$date = $row[$i];
} elseif (!$header && !$date && preg_match('/^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$/', $row[$i])) {
// cell contains a date column
list($date, $time) = explode(' ', $row[$i]);
} else if ($date && !$trans && preg_match('/^\d+-\d+-\d+$/', $row[$i])) {
// row contains a trans_num column & a date column
// auto-link to reprint receipt
$trans = $row[$i];
$row[$i] = sprintf('<a href="%sadmin/LookupReceipt/RenderReceiptPage.php?date=%s&receipt=%s"
target="_rp_%s_%s">%s</a>',
$url, $date, $row[$i],
$date, $row[$i], $row[$i]);
} else {
if (preg_match($numberPattern, strip_tags($row[$i]))) {
$styles .= 'text-align:right;';
}
}
$class = 'class="reportColumn'.$i;
if (($meta & self::META_CHART_DATA) != 0) {
if ($i == $this->chart_label_column) {
$class .= ' d3Label ';
} else if (is_array($this->chart_data_columns) &&
(count($this->chart_data_columns) == 0 ||
in_array($i, $this->chart_data_columns))
) {
$class .= ' d3Data ';
}
}
if ($header) {
$class .= ' thead ';
}
$class .= '"';
$ret .= "\t\t<" . $tag . ' ' . $class . ' style="' . $styles . '" colspan="' . $span . '">' . "\n"
. "\t\t\t" . $row[$i] . "\n"
. "\t\t</" . $tag . ">\n";
$i += $span;
}
$ret .= "\t</tr>\n";
if (($meta & self::META_REPEAT_HEADERS) != 0) {
$ret .= "</tbody>\n<tbody>\n";
} elseif (($meta & self::META_BLANK) != 0) {
$ret .= "</tbody>\n";
}
return $ret;
}
/**
Turn array into CSV line
@param $row an array of data
@return CSV string
*/
public function csvLine($row, $separator=',')
{
$meta = 0;
if (isset($row['meta'])) {
$meta = $row['meta'];
unset($row['meta']);
}
if (($meta & self::META_BLANK) != 0) {
$header1 = $this->select_headers(False);
// just using headers as a column count
$row = array_map(function ($val) { return null; }, $header1);
}
if (($meta & self::META_REPEAT_HEADERS) != 0) {
$header1 = $this->select_headers(True);
$row = array_map(function ($val) { return strip_tags($val); }, $header1);
}
$ret = "";
foreach($row as $item) {
$item = $this->excelFormat($item);
$ret .= '"'.$item.'"' . $separator;
}
$ret = substr($ret,0,strlen($ret)-1)."\r\n";
return $ret;
}
/**
* Remove formatting from cell contents for Excel formats
*/
public function excelFormat($item, $style='')
{
if ($style == '' && strpos('csv|xls',$this->report_format) !== False) {
$style = $this->report_format;
}
$item = strip_tags($item);
if ($style == 'csv' || $style == 'txt') {
$item = str_replace('"','',$item);
}
// '$ 12.39' -> '12.39' or '$ -12.39' -> '-12.39'
$item = preg_replace('/^\$ *(\d|-)/',"$1",$item);
// '12.39 %' -> '12.39'
// should this divide by 100 when stripping the % sign?
$item = preg_replace("/(\d) *%$/","$1",$item);
// 1,000 -> 1000
$item = preg_replace("/(\d),(\d\d\d)/","$1$2",$item);
return FileData::excelNoFormula($item);
}
/**
Apply meta rules to XLS data
*/
public function xlsMeta($data)
{
$fixup = array();
foreach($data as $row) {
$meta = 0;
if (isset($row['meta'])) {
$meta = $row['meta'];
unset($row['meta']);
}
if (($meta & self::META_BLANK) != 0) {
$row = array();
$header1 = $this->select_headers(False);
// just using headers as a column count
$row = array_map(function($i){ return null; }, $header1);
}
if (($meta & self::META_REPEAT_HEADERS) != 0) {
$header1 = $this->select_headers(True);
$row = array_map(function($i){ return strip_tags($i); }, $header1);
}
$fixup[] = $row;
}
return $fixup;
}
/**
Helper: check default export args
*/
protected function formatCheck()
{
if (FormLib::get('excel') === 'xls') {
$this->report_format = 'xls';
$this->window_dressing = false;
/**
Verify whether Excel support is available. If it is not,
fall back to CSV output. Should probably
generate some kind of log message or notification.
*/
if (!\COREPOS\Fannie\API\data\DataConvert::excelSupport()) {
$this->report_format = 'csv';
}
} elseif (FormLib::get('excel') === 'csv') {
$this->report_format = 'csv';
$this->window_dressing = false;
} elseif (FormLib::get('excel') === 'txt') {
$this->report_format = 'txt';
$this->window_dressing = false;
}
}
/**
Set the form value container
@param [ValueContainer] $f
Accepts a generic ValueContainer instead of a FormValueContainer
so that unit tests can inject preset values
*/
public function setForm(COREPOS\common\mvc\ValueContainer $f)
{
$this->form = $f;
}
public function requiredFields()
{
return $this->required_fields;
}
/**
Check for input and display the page
*/
function drawPage()
{
if (!($this->config instanceof FannieConfig)) {
$this->config = FannieConfig::factory();
}
if (!$this->checkAuth() && $this->must_authenticate) {
$this->loginRedirect();
} elseif ($this->preprocess()) {
/** Use FanniePage::drawPage for the plain old html
version of the page
*/
if ($this->content_function == 'form_content') {
if (FormLib::get('json') !== '') {
$this->addOnloadCommand(FormLib::fieldJSONtoJavascript(base64_decode(FormLib::get('json'))));
}
return parent::drawPage();
}
/**
Global setting overrides default behavior
to force the menu to appear.
Unlike normal pages, the override is only applied
when the output format is HTML.
*/
if (($this->config->get('WINDOW_DRESSING') || $this->new_tablesorter) && $this->report_format == 'html') {
$this->window_dressing = true;
}
if ($this->window_dressing) {
echo $this->getHeader();
}
if ($this->readinessCheck() !== false) {
$func = $this->content_function;
echo $this->$func();
} else {
echo $this->errorContent();
}
if ($this->window_dressing) {
$footer = $this->getFooter();
$footer = str_ireplace('</html>','',$footer);
$footer = str_ireplace('</body>','',$footer);
echo $footer;
}
if ($this->report_format == 'html') {
foreach($this->scripts as $s_url => $s_type) {
printf('<script type="%s" src="%s"></script>',
$s_type, $s_url);
echo "\n";
}
$js_content = $this->javascriptContent();
if (!empty($js_content) || !empty($this->onload_commands)) {
echo '<script type="text/javascript">';
echo $js_content;
echo "\n\$(document).ready(function(){\n";
foreach ($this->onload_commands as $oc) {
if (strstr($oc, 'standardFieldMarkup()')) {
continue;
}
echo $oc."\n";
}
echo "});\n";
echo '</script>';
}
$page_css = $this->cssContent();
if (!empty($page_css)) {
echo '<style type="text/css">';
echo $page_css;
echo '</style>';
}
echo array_reduce($this->css_files,
function ($carry, $css_url) {
return $carry . sprintf('<link rel="stylesheet" type="text/css" href="%s">' . "\n",
$css_url);
},
''
);
}
if ($this->window_dressing || $this->report_format == 'html') {
echo '</body></html>';
}
}
// drawPage()
}
function draw_page()
{
$this->drawPage();
}
public function baseUnitTest($phpunit)
{
$this->bodyContent();
foreach (array('html', 'csv', 'txt') as $format) {
$this->report_format = $format;
$phpunit->assertNotEquals(0, strlen($this->report_content()));
}
}
public function setFormat($f)
{
$this->report_format = $f;
}
private function queueReport()
{
$fileName = realpath($_SERVER['SCRIPT_FILENAME']);
$class = basename($fileName);
$class = substr($class, 0, strlen($class) - 4);
$email = FannieAuth::getEmail(FannieAuth::getUID());
$json = array(
'reportFile' => $fileName,
'reportClass' => $class,
'email' => $email,
'formData' => array(),
);
foreach ($this->form as $k => $v) {
$json['formData'][$k] = $v;
}
$queue = new COREPOS\Fannie\API\jobs\QueueManager();
$added = $queue->add(array(
'class' => 'COREPOS\\Fannie\\API\\jobs\\QueuedReport',
'data' => $json,
));
if ($added) {
return '<div class="alert alert-success">Report queued for ' . $email . '</div>';
}
return '<div class="alert alert-danger">Report could not be queued</div>';
}
}