Odyssey
reporting.i
1 <?php
2 /**
3  * @package reports.i -- common functions for creating a report based on an SQL and column array.
4  * @author SPB
5  *
6  * Configuration is required. The file that calls this needs to have the global variables:
7  * @param String $defaultDateFormat -- for any date columns, this is the default format. If this is empty and it is empty at the lower level of configuration,
8  * then no format will be applied to dates.
9  *
10  * @param Associative Array $reportList -- Each key is a particular report. This value is not used except internally to identify the report.
11  * $reportKey -- The key that is necessary for the report page. It needs to be unique. This will show up on the URL for the report and is referenced in the menu.
12  * $title -- The title of the report. This will show up as the name of the CSV file on download, the title at the top of the page for the report, and the link on the report menu.
13  * If $title is missing, then it uses the key.
14  * $sql -- This is required. It is the base SQL of the report. Aliases are ignored. These come from the $col array. Any sorts are also ignored. Any sorts and filters are added to this
15  * SQL. If there is no sort, then the first column is sorted. Also, the columns are renamed as per the $col array. This is so any sorts and filters can refer to the column name.
16  * $cols -- This is required. This should be in order of the columns mentioned in the SQL. It consists of $colTitle => $colValue pairs.
17  * $colTitle -- This is the title of the column that will appear in the kendo grid. The kendo column name also comes from the camel case version of this string.
18  * If the title starts with a number, the kendo column name will have a "n" prepended. This is because kendo doesn't take kindly to columns starting with a number.
19  * Also, if two columns have the same title, they will still work. If there is a "." in the title, the actual title is everything before the period and the name takes everything.
20  * $colValue -- This is the column type. Column can be one of the following: "number", "decimal", "string", "date", "boolean", "odata", "currency", "ignore", "list", "hidden", "datetime"
21  * "number" -- refers to kendo's "number" format. Text aligns right.
22  * "decimal" -- like number but also restrict the number of digits but no need for a $.
23  * "currency" -- like number except also formats the number to $0.00.
24  * "string" -- refers to kendo's "string" format. Filter is case sensitive and trims first.
25  * "list" -- like string but also adds the "values" attribute to the kendo column. Option needs the $ddl option for the values.
26  * "date" -- refers to kendo's "date" format. This will be formatted according to $defaultDateFormat.
27  * "datetime" -- same as "date" but translate to CU time. This is for the "date with timezone" and other date formats in the database. ("date" works just fine.)
28  * "odata" -- refers to kendo's "odata" format. Sort and filter are not well defined for this format.
29  * "boolean" -- refers to kendo's "string" format. (The boolean format doesn't seem to work.) It has a "values" attribute according to what is true in the dataset and what is false.
30  * Assumed trues are true, "true", "Y", "yes", etc. Assumed falses are false, "false", "N", "no", etc. If both the false and true aren't found, then it gets the opposite.
31  * Regardless of what is true and what is false, it will show as "Y" and "N".
32  * "ignore" -- The column shows up in the dataItem of the grid but doesn't show up in the column array. (So then you can still do stuff with it.)
33  * "hidden" -- The columns shows up in the dataItem and the column array but it has the "hidden" attribute. (I added it for grouping on a column.)
34  * "description" -- Like a "string" type but there is no text wrap and there are ellipses.
35  * "unencoded" -- Sets the kendo's column attribute encoded to false. There is no additional formatting at all.
36  * $groupBy -- This is an optional parameter. It refers to the $colTitle of the column to group. If this is set, then the attributes "serverSideGrouping" and "group" are added to dataSource.
37  * The data for the calls are returned in a format grouped by the column. The CSV becomes grouped as well. The attribute "group" is added to the schema to parse the data correctly.
38  * Sorts sort on the groupBy column first before the actual columns being sorted.
39  * $groupDescription -- This is an optional parameter. It doesn't do anything if $groupBy is not set. It refers to the $colTitle of the description column.
40  * It adds the aggregate max to the description column underneath the group attribute. This allows that column's value to be displayed in the group header instead of the $groupBy column.
41  * $groupForceSortBy -- Forces the group sort to be by the $groupBy when both $groupBy and $groupDescription are defined.
42  * $ddl -- This is an optional parameter. This contains all the DDLs of the "list" columns.
43  * $ddlColumn -- This refers to the $colTitle of the group with a DDL. It needs $value => $text pairs in the DDL.
44  * $value -- Refers to the "value" attribute in the "values" attribute for a column.
45  * $text -- Refers to the "text" attribute in the "values" attribute for a column.
46  * $dateFormat -- This is what any dates should be formatted as for the report. If this is not set, then it gets the default date format for all the reports.
47  * $dateEmpty -- This is what the text will be if the date is null. The default is an empty string.
48  * $href -- Optional. If set, this the URL is different (versus the default $self&report=$report).
49  * $colHeader -- Optional. If set, then columns will have another line for the column header.
50  * $schemaFunc -- Optional. If set, then it refers to another function defined in this script. Function looks like addProfileTemplates($colTitle, $colType, $kendoCol, &$columnRecord).
51  * The function is called once per column in the readReportSchema function.
52  * $dataFunc -- Optional. If set, then it referes to another function defined in this script. Function looks like addLinksToOrphanedRecordsReport($dbh, &$dataRecord).
53  * The function is called once per data row in the getData function.
54  * $dataFuncVariables -- Optional array to send to custom function in this script defined by "dataFunc".
55  * $showOnCSV -- Optional. If not set, then "ignore" and "hidden" columns are not shown on the CSV. Other columns are shown. If set, for the columns specified, true means the column
56  * shows up in the CSV.
57  * nonSqlDataFunc -- Either this option or the SQL option needs to be defined. (If both are defined, SQL takes precedence.) This is set to function name. Function needs to be defined here.
58  * Function has one parameter: $dbh which is the database connection. It must return an array with "data" -- array of what should go into the report, "status" -- "000" if successful,
59  * "error" -- "" if successful; otherwise it is the text of the error message, "sql" -- optional array of SQL used (if the query is too complex for the report engine or if it needs functions.)
60  * defaultSort -- If set, then use this instead of the default sort which is the first visible column ascending.
61  * autoBind -- If set, then set the autoBind kendo property to this value.
62  *
63  * @param Associate Array $reportMenu -- Each key is section of the report menu. Each value is a (Numeric Array) of report keys in that section of the report (corresponds to $reportList).
64  * @param String $self -- refers to the file with the data calls, now $_SERVER["PHP_SELF"] set at the main level.
65  */
66 
67 /**
68  * class PivotCTE
69  * NOTE: Odyssey only. This is in Mammoth to keep this file in sync but it can only be used in Odyssey.
70  * Mammoth uses a prior version of Postgres that doesn't have support for Common Table Expressions (CTEs).
71  *
72  * This class represents the CTE for months. It replaces the culogtrack_pivot which has the current month plus the past 12 months.
73  * There is a column per month and quarter which is zero or one.
74  */
75 class PivotCTE {
76  /**
77  * @var array $pivotArray -- the data in the culogtrack_pivot table.
78  * @var string $cte -- the WITH clause of the SQL. This doesn't change so after it is calculated, it is stored here.
79  * @var array $columnSQLArray -- this maps the column SQL. If the SQL is already created, use it.
80  */
81  private $pivotArray;
82  private $cte;
83  private $columnSQLArray;
84 
85  /**
86  * function __construct()
87  *
88  * This function gets the current timestamp, strips out everything but the year and the month, and then fills in the pivotArray values based on that.
89  * The pivotArray will have 13 records: 12 months ago to the current month.
90  * Each record has the date as varchar Ym, title as "Jan", "Feb", etc., months as an array of zeroes and ones-- the ones will be the month that that date corresponds to,
91  * quarters as an array of zeroes and ones -- the ones will be the quarter that the date corresponds to.
92  */
93  function __construct() {
94  $pivotArray = array();
95  try {
96  $date = DateTime::createFromFormat("Y-m-d H:i:s", date("Y-m")."-1 00:00:00");
97  // NOTE: doing createFromFormat("Ym") is STILL going to create a date with day, hours, minutes, etc.
98 
99  for($i = 0; $i <= 12; $i++) {
100  $row = array("date" => "'" . $date->format("Ym") . "'::varchar",
101  "title" => $date->format("M"),
102  "months" => array_fill(0,13, 0),
103  "quarters" => array_fill(0,4,0));
104  $row["months"][$i] = 1;
105  $row["quarters"][floor($i/4)] = 1;
106  $pivotArray[] = $row;
107  $date->modify("-1 month");
108  }
109  } catch(exception $e) {
110  throw new exception ("Pivot CTE not created correctly: " . $e->getMessage());
111  }
112 
113  $this->pivotArray = $pivotArray;
114  $this->columnSQLArray = array();
115  }
116 
117  /**
118  * public function GetCTE()
119  * This function gets the WITH clause to the SQL. It uses the "values" syntax and gets all the values from the pivotArray. If the CTE is already calculated, then it just returns that.
120  */
121  public function GetCTE() {
122  if (isset($this->cte)) {
123  return $this->cte;
124  }
125 
126  $cteArray = array();
127  foreach($this->pivotArray as $pivotRow) {
128  $cteArray[] = "(" . $pivotRow["date"] . "," . implode(",", $pivotRow["months"]) . "," . implode(",", $pivotRow["quarters"]) . ")";
129  }
130 
131  $this->cte = "with monthPivotCTE(yearmo,m0,m1,m2,m3,m4,m5,m6,m7,m8,m9,m10,m11,m12,q0,q1,q2,q3) as (values " . implode(",", $cteArray) . ")";
132  return $this->cte;
133  }
134 
135  /**
136  * public function GetColumnSQL($toBeSummed)
137  * This function gets the SQL for the columns. Each column looks like "sum($toBeSummed * monthColumn)".
138  *
139  * @param string $toBeSummed -- This is what needs to be summed to be summed by month.
140  * @return string -- Add this to the column section of the SQL.
141  */
142  public function GetColumnSQL($toBeSummed) {
143  if (isset($this->columnSQLArray[$toBeSummed])) {
144  return $this->columnSQLArray[$toBeSummed];
145  }
146 
147  $columnArray = array();
148  for($i = 0; $i <= 12; $i++) {
149  $columnArray[] = "sum($toBeSummed * m$i)";
150  }
151 
152  $this->columnSQLArray[$toBeSummed] = implode(",", $columnArray);
153  return $this->columnSQLArray[$toBeSummed];
154  }
155 
156  /**
157  * public function GetColumns($columnArray)
158  * This function gets the column list.
159  *
160  * @param array $columnArray -- Set the column list as usual here minus the columns for the pivot.
161  * @return array $columnArray -- The starting columns plus "MTD" => "number", "Jan" => "number", "Dec" => "number" etc. (That would be if the current date was in February.)
162  */
163  public function GetColumns($columnArray) {
164  $first = true;
165  foreach($this->pivotArray as $pivotRow) {
166  $col = $first ? "MTD" : $pivotRow["title"];
167  $columnArray[$col] = "number";
168  $first = false;
169  }
170  return $columnArray;
171  }
172 }
173 
174 /**
175  * function GetIsFilterChanged()
176  * This function determines if the filter is changed. If it is, then we will also need the count.
177  *
178  * Javascript parameters:
179  * @param Array oldFilter -- the previous filter
180  * @param Array newFilter -- the new filter
181  */
182 function GetIsFilterChanged() { ?>
183 function IsFilterChanged(oldFilter, newFilter) {
184  <?php // Take care of nulls ?>
185  if (oldFilter == null) {
186  if (newFilter == null) {
187  return false;
188  } else {
189  return true;
190  }
191  } else if (newFilter == null) {
192  return true;
193  }
194 
195  var oldConstructor = oldFilter.constructor;
196  var newConstructor = newFilter.constructor;
197  var oldLogic, oldFilters, newLogic, newFilters;
198  if (oldConstructor === Array) {
199  oldLogic = "and";
200  oldFilters = oldFilter;
201  } else if (oldConstructor === Object) {
202  oldLogic = oldFilter.logic == null ? "and" : oldFilter.logic.trim();
203  oldFilters = oldFilter.filters;
204  } else {
205  return false; <?php // Invalid so in theory the filter won't change. ?>
206  }
207 
208  if (newConstructor === Array) {
209  newLogic = "and";
210  newFilters = newFilter;
211  } else if (newConstructor === Object) {
212  newLogic = newFilter.logic == null ? "and" : newFilter.logic.trim();
213  newFilters = newFilter.filters;
214  } else {
215  return false; <?php // Invalid so in theory the filter won't change. ?>
216  }
217 
218  if (oldLogic != newLogic) {
219  return true;
220  }
221  var oldFiltersEmpty = oldFilters == null || oldFilters.constructor !== Array || oldFilters.length == 0;
222  var newFiltersEmpty = newFilters == null || newFilters.constructor !== Array || newFilters.length == 0;
223  if (oldFiltersEmpty != newFiltersEmpty) {
224  return true;
225  }
226  if (oldFilters.length != newFilters.length) {
227  return true;
228  }
229 
230  var map = {};
231  for(var i = 0; i != oldFilters.length; i++) {
232  var record = oldFilters[i];
233  if (record.field == null || record.operator == null || record.value == null) {
234  return false; <?php // Invalid so in theory the filter won't change ?>
235  }
236  map["" + record.field + "||" + record.operator + "||" + record.value] = true;
237  }
238  for(var i = 0; i != newFilters.length; i++) {
239  var record = newFilters[i];
240  if (record.field == null || record.operator == null || record.value == null) {
241  return false; <?php // Invalid so in theory the filter won't change ?>
242  }
243  if (map["" + record.field + "||" + record.operator + "||" + record.value] == null) {
244  return true;
245  }
246  }
247  return false;
248 }
249 <?php }
250 
251 /**
252  * function GetReportDefinition($schemaData, $pageSize, $pageable, $sort, $filter, $report, $dataUrl, $printerFriendly)
253  * This gets the entire definition for the kendo grid. This is done in PHP rather than javascript because now I have reference javascript functions.
254  *
255  * @param $schemaData -- the data from readReportSchema. This is the kendo's definitions for the model, columns, and group arrays.
256  * @param $pageSize -- the pageSize from a constant at the top of the page.
257  * @param $pageable -- true if the grid should be pageable.
258  * @param $sort -- the sort of the grid.
259  * @param $filter -- the filter of the grid.
260  * @param $report -- the $reportKey in the $reportList.
261  * @param $dataUrl -- the URL of the data calls.
262  * @param $printerFriendly -- {true/false} If true, then the grid is not filterable, sort, etc.
263  *
264  * @return the definition for the report grid.
265  */
266 function GetReportDefinition($schemaData, $pageSize, $pageable, $sort, $filter, $report, $dataUrl, $printerFriendly, $autoBind) {
267  // Ensures that the sort and filter are actually arrays because they can be put in directly through the call to the print page.
268  $sort = isset($sort) ? (is_string($sort) ? HCU_JsonDecode($sort) : $sort) : array();
269  $filter = isset($filter) ? (is_string($filter) ? HCU_JsonDecode($filter) : $filter) : array();
270 
271  $dataSource = array(
272  "serverSorting" => true, "serverFiltering" => true, "serverSorting" => true, "sort" => $sort,
273  "filter" => $filter,
274  "transport" => array(
275  "read" => "*** read ***"
276  ), // End of transport
277  "schema" => array(
278  "model" => $schemaData["modelData"],
279  "data" => "data",
280  "total" => "total"
281  ),
282  "sort" => $sort
283  );
284 
285  if (isset($schemaData["groupData"])) {
286  $dataSource["serverGrouping"] = true;
287  $dataSource["group"] = $schemaData["groupData"];
288  $dataSource["schema"]["groups"] = "data";
289  }
290 
291  $gridDefinition = array(
292  "dataSource" => $dataSource,
293  "columns" => $schemaData["columnData"],
294  "scrollable" => false);
295 
296  if (!$printerFriendly) {
297  $gridDefinition["dataSource"]["serverPaging"] = true;
298  $gridDefinition["dataSource"]["pageSize"] = $pageSize;
299  $gridDefinition["pageable"] = $pageable;
300  $gridDefinition["sortable"] = true;
301  $gridDefinition["filterable"] = array(
302  "extra" => false,
303  "operators" => array(
304  "string" => array(
305  "eq" => "Is equal to",
306  "neq" => "Is not equal to",
307  "startswith" => "Starts with",
308  "endswith" => "Ends with",
309  "contains" => "Contains",
310  "doesnotcontain" => "Does not contain"
311  ),
312  "number" => array(
313  "eq" => "Is equal to",
314  "neq" => "Is not equal to",
315  "gt" => "Greater than",
316  "gte" => "Greater than or equal to",
317  "lt" => "Less than",
318  "lte" => "Less than or equal to"
319  ),
320  "date" => array(
321  "eq" => "Is equal to",
322  "neq" => "Is not equal to",
323  "gt" => "Is after",
324  "gte" => "Is after or equal to",
325  "lt" => "Is before",
326  "lte" => "Is before or equal to"
327  ),
328  "list" => array(
329  "eq" => "Is equal to",
330  "neq" => "Is not equal to",
331  )
332  )
333  );
334  $gridDefinition["dataBound"] = "*** reportDataBound ***";
335  $gridDefinition["pdf"] = array("allPages" => true, "avoidLinks" => true, "paperSize" => "A4", "margin" => array("top" => "2cm", "left" => "1cm", "right" => "1cm", "bottom" => "1cm"),
336  "landscape" => true, "repeatHeaders" => true, "template" => "*** \$(\"#pageTemplate\").html() ***", "fileName" => $report, "scale" => 0.59);
337  $gridDefinition["noRecords"] = array("template" => "<tr><td>No Records Found</td></tr>");
338  $gridDefinition["autoBind"] = $autoBind;
339  }
340 
341  // JSON encode but then remove characters that are meant to be actual javascript code.
342  $gridDefinition = str_replace('"***', "", str_replace('***"', "", HCU_JsonEncode($gridDefinition)));
343  $gridDefinition = str_replace("\\\\", "\\", str_replace("\\/", "/", str_replace('\\"', '"', $gridDefinition))); // Escapes are not necessary anymore.
344  return $gridDefinition;
345 }
346 
347 /**
348  * function InitReport($reportPage, $report, $pageable="true", $pageSize=30, $printerFriendly=false, $sort="", $filter="")
349  * Prints out the init function to create the report grid with the appropriate events. This is server-side. Please refer to the next function for client-side.
350  *
351  * PHP parameters:
352  * @param string $reportPage -- the url of the report page for calling the database functions readReport and readCSV.
353  * @param string $report -- the report id in the report global object for getting the SQL and columns.
354  * @param array $reportList -- the reportList with all the definitions.
355  * @param string $pageable -- "true" or "false." Refers to the kendo attribute pageable.
356  * @param integer $pageSize -- what the size of the page is.
357  * @param boolean $printerFriendly -- if true, then all the controls do not show up on the page.
358  * @param string $sort -- a json_encoded array of the initial sort.
359  * @param string $filter -- a json_encoded array of the initial filter.
360  *
361  * Javascript parameters:
362  * @param Function afterInitFunc -- if defined, then this function will be called after the report grid is initialized.
363  * @param Function dataBoundFunc -- if defined, then this function will be called on the dataBound.
364  * @param Function preInitFunc -- if defined, then this function will be called after model and column data received but before initialization
365  */
366 function InitReport($reportPage, $report, $reportList, $defaultDateFormat, $pageable = true, $pageSize = 30, $printerFriendly = false, $sort = "", $filter = "") {
367 
368  $dataUrl = $reportPage . (false !== strpos($reportPage, "?") ? "&" : "?") . "operation=readReport"; // Ensure that if there is already a query string to append to it.
369  $schemaData = readReportSchema($reportList[$report], $defaultDateFormat);
370 
371  $sort = isset($sort) ? trim($sort) : "";
372  $filter = isset($filter) ? trim($filter) : "";
373  $sort = $sort == "" ? $schemaData["gridConf"]["sort"] : $sort;
374  $filter = $filter == "" ? array() : $filter;
375  $autoBind = HCU_array_key_exists("autoBind", $reportList[$report]) ? $reportList[$report]["autoBind"] === true : true;
376  $gridDefinition = GetReportDefinition($schemaData["gridConf"], $pageSize, $pageable, $sort, $filter, $report, $dataUrl, $printerFriendly, $autoBind);
377 ?>
378 var filterChanged = true;
379 var initial = true;
380 var inInitial = false;
381 var count = -1;
382 var currentFilter = null;
383 var currentSort = null;
384 var funcs = null;
385 function InitReport(funcs) {
386  window.funcs = funcs;
387  var gridDefinition = <?php echo $gridDefinition; ?>;
388  if (funcs != null && typeof(funcs.preInitFunc) == "function") {
389  (funcs.preInitFunc)(gridDefinition);
390  }
391 
392  var grid = $("#reportGrid").kendoGrid(gridDefinition).data("kendoGrid");
393  <?php if (!$printerFriendly) { ?>
394  $(grid.pager.element).hide(); <?php // Hide temporary until databound. ?>
395  <?php } ?>
396 
397  var toolTipProps = homecuTooltip.defaults;
398  toolTipProps.filter = ".showEllipsis:overflown";
399  toolTipProps.content = function(e) {
400  return $(e.target).text().trim();
401  };
402 
403  $("#groupRightsGrid").kendoTooltip(toolTipProps);
404 
405  <?php if (!$printerFriendly) { ?>
406 
407  var warningWindow = $("<div id='warningWindow'></div>").appendTo("body").kendoWindow({
408  modal: true,
409  visible: false,
410  title: "Too Many Records"
411  }).data("kendoWindow");
412 
413  var resultSizeLimit = 85000; <?php // If it is much higher than this, then CSV and print options aren't likely to work. ?>
414  var warningTemplate = kendo.template("<p>You are trying to #: action # with more than #: limit # rows. Please filter more.</p>"
415  + "<a href='\\#' class='k-button k-primary' id='warningClose'>Close</a>");
416 
417  $("#warningWindow").on("click", "#warningClose", function() {
418  warningWindow.close();
419  });
420 
421  $("#printBtn").click(function() {
422  var resultSize = grid.dataSource.total();
423  if (resultSize >= resultSizeLimit) {
424  warningWindow.content(warningTemplate({limit: resultSizeLimit, action: "print"})).open().center();
425  return; <?php // Don't do anything more. ?>
426  }
427 
428  $("#printForm .extraPrintForm").remove();
429  var sort = grid.dataSource.sort();
430  var filter = grid.dataSource.filter();
431  $("#printForm").append("<input class='extraPrintForm' name='sort' value='" + kendo.stringify(sort) + "'>");
432  $("#printForm").append("<input class='extraPrintForm' name='filter' value='" + kendo.stringify(filter) + "'>");
433  $("#printForm").submit();
434  });
435 
436  $("#downloadCSVBtn").click(function () {
437  var resultSize = grid.dataSource.total();
438  if (resultSize >= resultSizeLimit) {
439  warningWindow.content(warningTemplate({limit: resultSizeLimit, action: "print"})).open().center();
440  return; <?php // Don't do anything more. ?>
441  }
442 
443  $("#downloadCSVForm .extraCSVForm").remove();
444  if (currentSort != null) {
445  $("#downloadCSVForm").append("<input class='extraCSVForm' name='sort' value='" + kendo.stringify(currentSort) + "'>");
446  }
447  if (currentFilter != null) {
448  $("#downloadCSVForm").append("<input class='extraCSVForm' name='filter' value='" + kendo.stringify(currentFilter) + "'>");
449  }
450  $("#downloadCSVForm").submit();
451  });
452 
453  $("#downloadPDFBtn").click(function() {
454  var toHide = $(grid.wrapper).find(".k-icon[class^='k-i-sort'], .k-grid-filter");
455  <?php // Otherwise the headers will not look right aligned. Filter and sort are hidden by default but they still take up space in the PDF. ?>
456  $(toHide).hide();
457  grid.saveAsPDF();
458  $(toHide).show();
459  });
460 
461  if (funcs != null && typeof(funcs.afterInitFunc) == "function") {
462  funcs.afterInitFunc();
463  }
464  <?php } // End not printerFriendly. ?>
465 } <?php // End InitReport. ?>
466 
467 <?php
468 /**
469  * var read= function(options)
470  * This function is referenced in the GetReportDefinition function. It is the kendo dataSource > transport > read function. This is a function so that it can redefine the grid based on
471  * the boolean fields. The first data call will see what is the true value and what is the false value of the boolean fields and then create a DDL. Since the kendo grid will do another
472  * read after recreating the grid, this function will prevent that and instead get the data from the first data call. All other data calls are normal.
473  *
474  * @param options -- the options as defined in the kendo datasource api for the read function.
475  */
476 ?>
477 var read = function(options) {
478  if (inInitial) {
479  options.success(window.reportInitialData);
480 
481  inInitial = false;
482  if (funcs != null && typeof(funcs.afterInitFunc) == "function") { <?php // Because grid will have been recreated at this point. ?>
483  (funcs.afterInitFunc)(window.reportInitialData);
484  }
485  delete window.reportInitialData;
486 
487  return;
488  }
489 
490  var filterChanged = IsFilterChanged(currentFilter, options.data.filter);
491  var parameters = {limit: options.data.pageSize, offset: options.data.skip, report: "<?php echo $report; ?>", initial: initial,
492  printerFriendly: <?php echo $printerFriendly ? "true" : "false"; ?>};
493  parameters.newFilter = initial || filterChanged;
494  if (parameters.newFilter) {
495  currentFilter = options.data.filter;
496  }
497  if (options.data.filter != null) {
498  parameters.filter = kendo.stringify(options.data.filter);
499  }
500  if (options.data.sort != null) {
501  parameters.sort = kendo.stringify(options.data.sort);
502  }
503  currentSort = options.data.sort;
504 
505  if (funcs != null && typeof(funcs.parameterMapFunc) == "function") {
506  (funcs.parameterMapFunc) (options.data, parameters, filterChanged);
507  }
508 
509  showWaitWindow();
510  $.post("<?php echo $dataUrl; ?>", parameters, function(data) {
511  hideWaitWindow();
512  if (data.error.length > 0) {
513  $.homecuValidator.displayMessage(data.error, $.homecuValidator.settings.statusError );
514  } else {
515  if (initial) {
516  initial = false;
517  if (data.booleanRedefinition.length > 0) {
518  window.reportInitialData = data.reportData;
519  inInitial = true;
520  var grid = $("#reportGrid").data("kendoGrid");
521  var colMap = {};
522  for(var i = 0, iLength = grid.columns.length; i != iLength; i++) {
523  var iCol = grid.columns[i];
524 
525  if (iCol.columns != null) { <?php // Column headers. ?>
526  for(var j = 0, jLength = iCol.columns.length; j != jLength; j++) {
527  var jCol = iCol.columns[j];
528  colMap[jCol.field] = {i: i, j: j};
529  }
530  } else {
531  colMap[iCol.field] = {i: i, j: -1};
532  }
533  }
534 
535  <?php // Will need to take in account if there is a column header in the boolean redefinition. ?>
536  var length = data.booleanRedefinition.length;
537  for(var i = 0; i != length; i++) {
538  var record = data.booleanRedefinition[i];
539 
540  var mappy = colMap[record.col];
541  if (mappy.j == -1) {
542  grid.columns[mappy.i].values = record.values;
543  } else {
544  grid.columns[mappy.i].columns[mappy.j].values = record.values;
545  }
546  }
547  if (length > 0) {
548  grid.setOptions({columns: grid.columns});
549  }
550  }
551  }
552 
553  parameters.newFilter ? count = data.reportData.total : data.reportData.total = count;
554  <?php // If there is a new filter, then the count will be retrieved. Otherwise use the current count. ?>
555 
556  }
557 
558  options.success(data.reportData);
559  });
560 };
561 
562 <?php
563 /**
564  * var reportDataBound= function(e)
565  * This function accomplishes nothing except provide a hook for a custom function for any script that uses this reporting.i script. It is the kendo dataBound function.
566  *
567  * @param e -- the parameter as defined in kendo grid api for the dataBound function.
568  */
569 ?>
570 var reportDataBound = function(e) {
571 
572  <?php // Only show pager when there is more than one page. ?>
573  var pager = this.pager.element;
574  var pages = this.dataSource.totalPages();
575  pages <= 1 ? $(pager).hide() : $(pager).show();
576 
577  if (funcs != null && typeof(funcs.dataBoundFunc) == "function") {
578  (funcs.dataBoundFunc) (this, e);
579  }
580 };
581 <?php } // End the PHP function to print out init report functions.
582 
583 /**
584  * @todo: This probably doesn't work now with changes to the internal functions. This function is used for adm_reports currently.
585  * function InitReportClientSide($reportPage, $report, $title, $additionalParameters= array(), $scrollable=false, $beforeInitFunc="", $afterInitFunc="")
586  * Prints out the init function to create the report grid with the appropriate events. This is client-side. Please refer to the previous function for server-side.
587  *
588  * PHP parameters:
589  * @param string $reportPage -- the url of the report page for calling the database functions readReport and readCSV.
590  * @param string $report -- the report id in the report global object for getting the SQL and columns.
591  * @param string $title -- the name of the CSV download.
592  * @param array $additionalParameters -- parameters to be added to the default parameters to get all the data.
593  * @param boolean $scrollable -- refers to the kendo scrollable property
594  * @param string $beforeInitFunc -- Javascript function call before the report grid is initialized.
595  * @param string $afterInitFunc -- Javascript function call after the report grid is initialized.
596  */
597 function InitReportClientSide($reportPage, $report, $title, $additionalParameters = array(), $scrollable = false, $beforeInitFunc = "", $afterInitFunc = "") {
598  $dataUrl = $reportPage . (false !== strpos($reportPage, "?") ? "&" : "?") . "operation=readReport"; // Ensure that if there is already a query string to append to it.
599  $parameters = array("report" => $report, "limit" => -1, "offset" => 0, "includeSchema" => "Y");
600  foreach($additionalParameters as $key => $value) {
601  $parameters[$key] = $value;
602  }
603 ?>
604 function InitReport() {
605  var initial = true;
606  var pageSize = 30;
607  var count = 0;
608  var currentFilter = null;
609  showWaitWindow();
610  $.post("<?php echo $dataUrl; ?>", <?php echo HCU_JsonEncode($parameters); ?>,
611  function(topData) {
612  hideWaitWindow();
613  if (topData.error.length > 0) {
614  $.homecuValidator.displayMessage(topData.error, $.homecuValidator.settings.statusError );
615  } else {
616  var sort = {field: topData.columnData[0].field, dir: "asc"};
617  var gridDefinition = {
618  dataSource: {
619  transport: {
620  read: function (options) {
621  options.success(topData.reportData.data);
622  }
623  },
624  schema: {
625  model: topData.modelData
626  },
627  pageSize: pageSize,
628  sort: sort
629  },
630  columns: topData.columnData,
631  pageable: true,
632  sortable: true,
633  filterable: {
634  extra: false
635  },
636  scrollable: <?php echo ($scrollable ? "true" : "false"); ?>
637  };
638 
639  <?php echo ($beforeInitFunc != "" ? "($beforeInitFunc) (gridDefinition);" : ""); ?>
640 
641  var grid = $("#reportGrid").kendoGrid(gridDefinition).data("kendoGrid");
642 
643  $("#downloadCSVBtn").click(function () {
644  downloadCSVClientSide();
645  });
646 
647  <?php echo ($afterInitFunc != "" ? "($afterInitFunc) (grid);" : ""); ?>
648  }
649  });
650 }
651 
652 <?php
653 /**
654  * @todo: This probably doesn't work now with changes to the internal functions. This function is used for adm_reports currently.
655  * function DownloadCSVClientSide()
656  *
657  * Creates the CSV from what is in the grid. Filter and sort are preserved. Includes all the pages and then when done, sets the page back to the current page.
658  * {@link http://stackoverflow.com/questions/14964035/how-to-export-javascript-array-info-to-csv-on-client-side}
659  */
660 ?>
661 function DownloadCSVClientSide() {
662  var grid = $("#reportGrid").data("kendoGrid");
663  var csvData = [];
664  var csvRecord = [];
665  var columns = [];
666  for(var i = 0, length = grid.columns.length; i!= length; i++) {
667  var record = grid.columns[i];
668 
669  if (record.hidden == null || !record.hidden) {
670  columns.push(record.field);
671  csvRecord.push(csvEscape(record.title));
672  }
673  }
674  csvData.push(csvRecord.join());
675 
676  var data = grid.dataSource.data();
677  var filter = grid.dataSource.filter();
678  var sort = grid.dataSource.sort();
679  var query = new kendo.data.Query(data);
680  data = query.filter(filter).sort(sort).data;
681 
682  for(var i = 0; i != data.length; i++) {
683  var record = data[i];
684  csvRecord = [];
685 
686  for(var j = 0; j != grid.columns.length; j++) {
687  if (record.hidden == null || !record.hidden) {
688  var column = grid.columns[j];
689  var kendoCol = column.field;
690  var value = column.template != null ? kendo.template(record[kendoCol]) : (column.format != null ? kendo.format(column.format, record[kendoCol]) : record[kendoCol]);
691  csvRecord.push(csvEscape(value));
692  }
693  }
694 
695  csvData.push(csvRecord.join());
696  }
697 
698  var blob = new Blob([csvData.join("\r\n")], { type: 'text/csv;charset=utf-8;' });
699  var filename = "<?php echo $title; ?>.csv";
700  if (navigator.msSaveBlob) { <?php // IE 10+ ?>
701  navigator.msSaveBlob(blob, filename);
702  } else {
703  var link = document.createElement("a");
704  if (link.download != null) { <?php // feature detection ?>
705  <?php // Browsers that support HTML5 download attribute ?>
706  var url = URL.createObjectURL(blob);
707  link.setAttribute("href", url);
708  link.setAttribute("download", filename);
709  link.style.visibility = 'hidden';
710  document.body.appendChild(link);
711  link.click();
712  document.body.removeChild(link);
713  }
714  }
715 }
716 
717 <?php
718 /**
719  * function CsvEscape(string)
720  * Escapes the string for CSV. That means that if there is a newline or a double quote, it needs to be escaped.
721  *
722  * @param string $string -- the string to convert.
723  * @return string -- the converted string.
724  */
725 ?>
726 function CsvEscape(string) {
727  if (string == null || typeof(string) != "string") {
728  return string;
729  }
730  string = string.indexOf('"') != -1 ? string.replace('"', '""') : string;
731  string = string.indexOf(',') != -1 || string.indexOf('\n') != -1 ? '"' + string + '"' : string;
732  return string;
733 }
734 <?php } // end InitReportClientSide
735 
736 /**
737  * function GetReportTitles()
738  *
739  * Get the titles of the reports.
740  * @return array -- titles of the reports.
741  */
742 function GetReportTitles() {
743  $reportList = GetReportList();
744  $reportTitles = array();
745  foreach($reportList as $key => $record) {
746  $reportTitles[$key] = isset($record["title"]) ? $record["title"] : $key;
747  }
748  return $reportTitles;
749 }
750 
751 /**
752  * function GetCamelCase($string)
753  * Removes the whitespaces and capitalizes the first letter in each word for keys.
754  *
755  * @param string $string -- the string to convert.
756  * @return string -- the converted string.
757  */
758 function GetCamelCase($string) {
759  $returnString = $string;
760  if (!isset($string) || trim($string) == "") {
761  $returnString = "";
762  } else {
763  $camelCase = lcfirst(str_replace(" ", "", ucwords(strtolower(trim(preg_replace("/[^a-z0-9 ]+/i", " ", $string)))))); // the column is camelcase with only letters and numbers.
764  $returnString = is_numeric($camelCase[0]) ? "n$camelCase" : $camelCase; // Kendo seems to complain if the column starts with a number.
765  }
766  return $returnString;
767 }
768 
769 /**
770  * function CompileMenu($conf, $reportList, $reportFrontEndFile, $useRadioButtons=false)
771  * Creates the menu for all the reports.
772  *
773  * @param array $conf -- the page layout configuration.
774  * @param array $reportList -- the report configuration.
775  * @param string $reportFrontEndFile -- the URL.
776  * @param boolean $useRadioButtons -- If true, then to select, you would click on a radio button. Otherwise, the list is shown with links.
777  *
778  */
779 function CompileMenu($conf, $reportList, $reportFrontEndFile, $useRadioButtons = false) {
780  $menu = "";
781  foreach($conf as $sectionTitle => $reports) {
782  $menu .= "<div>\n<h2 class='primary'>$sectionTitle</h2>\n<div class='row'>\n";
783 
784  foreach ($reports as $report) {
785  $reportRecord = HCU_array_key_exists($report, $reportList) ? $reportList[$report] : array();
786  $reportTitle = HCU_array_key_exists("title", $reportRecord) ? $reportRecord["title"] : $report;
787  $href = HCU_array_key_exists("url", $reportRecord) ? $reportRecord["url"] : $reportFrontEndFile . (false !== strpos($reportFrontEndFile, "?") ? "&" : "?") . "report=$report";
788 
789  $menu .= "<div class='col-xs-6'>\n<a href=$href><span class='reportTitle'>$reportTitle</span></a></div>\n";
790  }
791  $menu .= "</div></div>\n";
792  }
793  return $menu;
794 }
795 
796 /**
797  * function UpdateSelectSQL($baseSQL, $colDefs)
798  * updates the select SQL with the keys based on the camelcase of the titles of the columns.
799  *
800  * @param string $baseSQL -- SQL to update.
801  * @param array $colDefs -- the columns in the global report array.
802  */
803 function UpdateSelectSQL($baseSQL, $colDefs) {
804  try {
805  if (!isset($baseSQL) || trim($baseSQL) == "") {
806  throw new exception("BaseSQL needs to be defined.", 1);
807  }
808  if (!isset($colDefs) || !is_array($colDefs)) {
809  throw new exception("ColDefs needs to be defined.", 2);
810  }
811  $cols = array();
812  foreach($colDefs as $colTitle => $colType) { // Gets only the kendo column names.
813  $cols[] = getCamelCase($colTitle);
814  }
815 
816  $stack = array();
817  $word = "";
818  $nonAlphaFound = false;
819  $selectFound = false;
820  $asFound = false;
821  $inExpression = true;
822  $fromIndex = -1;
823  $selectArray = "";
824  $selectCalc = "";
825  $selectName = "";
826  $selectIndex = 0;
827  $whereFound = false;
828  $baseSQL = trim($baseSQL);
829  for($i = 0, $count = strlen($baseSQL); $i != $count; $i++) {
830  $char = $baseSQL[$i];
831 
832  if(ctype_alnum ($char) || $char == "_") {
833  $word .= $char;
834  $nonAlphaFound = false;
835  } else {
836  $willNotBeInExpression = false;
837  $numInStack = count($stack);
838  $stackEmpty = $numInStack == 0;
839  $lastInStack = $stackEmpty ? "" : $stack[$numInStack - 1];
840 
841  if ($selectFound && $word != "" && !$nonAlphaFound && $stackEmpty) {
842  $inExpression = false;
843  }
844 
845  switch(strtolower($word)) {
846  case "select":
847  if (!$selectFound && $stackEmpty) {
848  $selectFound = true;
849  if ($i > strlen($word) - 1) {
850  $word = " select";
851  }
852  }
853  break;
854  case "as":
855  if ($selectFound && $stackEmpty) {
856  $asFound = true;
857  }
858  break;
859  case "from":
860  if ($selectFound && $stackEmpty) {
861  $selectCalc = trim($selectCalc);
862  if (in_array($selectCalc, array("select *", "*"))) {
863  throw new exception("Asterisk is not supported at this time.", 4);
864  }
865  $doesContain = preg_match('/(^| |([a-zA-Z0-9_]+\.))\*$/', $selectCalc); // Either it is ONLY a *, OR there is a * preceded by a space "select *" or ", *", OR {table}.*.
866  if ($doesContain === false) {
867  throw new exception ("Preg_match failed.", 5);
868  }
869  if ($doesContain === 1) {
870  throw new exception("Asterisk is not supported at this time.", 6);
871  }
872  if ($selectCalc[0] != ",") { // Add a space if the first character is not a comma. Otherwise it looks more natural to not have a space.
873  $selectArray .= " ";
874  }
875  $selectArray .= "$selectCalc as " . $cols[$selectIndex++];
876  $fromIndex = $i;
877  }
878  break;
879  case "case":
880  if (!$nonAlphaFound) {
881  array_push($stack, $word);
882  $inExpression = true;
883  }
884  break;
885  case "end":
886  if ($lastInStack == "case" && !$nonAlphaFound) {
887  array_pop($stack);
888  if (count($stack) == 0) {
889  $willNotBeInExpression = true;
890  $stackEmpty = true;
891  } else { // Need to update the lastInStack variable for the case of "end)"
892  $lastInStack = $stack[$numInStack - 2];
893  }
894  }
895  break;
896  case "distinct":
897  $inExpression = true;
898  break;
899  }
900 
901  $nonAlphaFound = true;
902  if ($fromIndex != -1) {
903  break;
904  } else if ($word != "") {
905  if (!$asFound) {
906  $selectCalc .= $word;
907  }
908  }
909 
910  switch ($char) {
911  case "(":
912  case "[":
913  case "{":
914  array_push($stack, $char);
915  $inExpression = true;
916  break;
917  case "'":
918  case '"':
919  if ($lastInStack == $char) {
920  array_pop($stack);
921  if (count($stack) == 0) {
922  $willNotBeInExpression = true;
923  }
924  } else {
925  array_push($stack, $char);
926  }
927 
928  break;
929  case "}":
930  if ($lastInStack == "{") {
931  array_pop($stack);
932  if (count($stack) == 0) {
933  $willNotBeInExpression = true;
934  }
935  }
936  break;
937  case "]":
938  if ($lastInStack == "[") {
939  array_pop($stack);
940  if (count($stack) == 0) {
941  $willNotBeInExpression = true;
942  }
943  }
944  break;
945  case ")":
946  if ($lastInStack == "(") {
947  array_pop($stack);
948  if (count($stack) == 0) {
949  $willNotBeInExpression = true;
950  }
951  }
952 
953  break;
954  case ",":
955  $inExpression = true;
956  if ($selectFound && $stackEmpty) {
957  if (!HCU_array_key_exists($selectIndex, $cols)) {
958  throw new exception("Column mismatch found.", 3);
959  }
960  $selectCalc = trim($selectCalc);
961  $doesContain = preg_match('/(^| |([a-zA-Z0-9_]+\.))\*$/', $selectCalc); // Either it is ONLY a *, OR there is a * preceded by a space "select *" or ", *", OR {table}.*.
962  if ($doesContain === false) {
963  throw new exception ("Preg_match failed.", 5);
964  }
965  if ($doesContain === 1) {
966  throw new exception("Asterisk is not supported at this time.", 6);
967  }
968  if ($selectCalc[0] != ",") { // Add a space if the first character is not a comma. Otherwise it looks more natural to not have a space.
969  $selectArray .= " ";
970  }
971  $selectArray .= "$selectCalc as " . $cols[$selectIndex++];
972  $selectCalc = "";
973  $selectName = "";
974  $asFound = false;
975  }
976  break;
977  case "|":
978  case "+":
979  case "/":
980  case "%":
981  case "-":
982  case "<":
983  case ">":
984  case ".":
985  case "*":
986  case "&":
987  case "^":
988  case "~":
989  case "`":
990  case ":":
991  case "=":
992  if ($stackEmpty) {
993  $inExpression = true;
994  }
995  break;
996  }
997  $word = "";
998 
999  if (!$asFound) {
1000  $selectCalc .= $char;
1001  }
1002  $inExpression = !$willNotBeInExpression;
1003  }
1004 
1005  }
1006 
1007  $restOfSQL = substr($baseSQL, $fromIndex);
1008  $baseSQL = trim($selectArray) . " from " . trim($restOfSQL);
1009 
1010  $returnArray = array("status" => "000", "error" => "", "modifiedSQL" => $baseSQL);
1011  } catch (exception $e) {
1012  $returnArray = array("status" => $e->getCode(), "error" => $e->getMessage());
1013  }
1014 
1015  return $returnArray;
1016 }
1017 
1018 /**
1019  * function FormatReportData($Cu, $dbh, $unprocessedDataArray, $cols, $groupBy, $groupDescription, $dontIncludeId, $initial, &$booleanRedefinition, $dataFunc, $dataFuncVariables)
1020  * Retrieves the data for the report from the database.
1021  *
1022  * @param string $Cu -- the credit union
1023  * @param integer $dbh -- the database connection
1024  * @param array $unprocessedDataArray -- Associative array from either sql or report-specific function. Sort and filter have already taken place at this point.
1025  * @param array $cols -- the column definition from the $reportRecord.
1026  * @param string $groupBy -- the column to group by.
1027  * @param string $groupDescription -- the column to display on the group header row.
1028  * @param boolean $dontIncludeId -- for CSV. If it is not CSV, then there is a special id for kendo.
1029  * @param boolean $initial -- if it is the first time, then also get the DDLs for the boolean columns.
1030  * @param array $booleanRedefinition -- contains all the DDLs for the boolean fields if this is the first time.
1031  * @param string $dataFunc -- if this is defined, then it references the name of the function for the report. The function gets the $dbh and database record.
1032  *
1033  * @return the data for the current sort and filter in the grid.
1034  */
1035 function FormatReportData($Cu, $dbh, $unprocessedDataArray, $cols, $groupBy, $groupDescription, $dontIncludeId, $initial, &$booleanRedefinition, $dataFunc, $dataFuncVariables) {
1036  $reportData = array();
1037  $booleanFields = array();
1038  $index = 0;
1039  $idName = "reportId";
1040  $datetimeFields = array();
1041  $tz = null;
1042  foreach($cols as $colTitle => $colType) {
1043  $value = getCamelCase($colTitle);
1044  $key = strtolower($value);
1045  $colMap[$key] = $value;
1046 
1047  if ($colType == "boolean") {
1048  $booleanFields[$value] = array("trueFound" => false, "falseFound" => false, "trueValue" => "", "falseValue" => "");
1049  } else if ($colType == "datetime") {
1050 
1051  $datetimeFields[$key] = true;
1052  $tz = GetCreditUnionTimezone($dbh, $Cu);
1053  }
1054  }
1055 
1056  $first = true;
1057  $currentGroup = "";
1058  foreach($unprocessedDataArray as $i => $dRecord) { // This will keep the camelcase. The database puts everything as lowercase.
1059  $kendoRecord = array();
1060  if (!$dontIncludeId) {
1061  $kendoRecord[$idName] = $i;
1062  }
1063 
1064  foreach($dRecord as $key => $value) {
1065  $kendoCol = $colMap[strtolower($key)];
1066  $value = trim($value);
1067  $value = isset($booleanFields[$kendoCol]) ? ($value == "t" ? true : ($value == "" || $value == "f" ? false : $value)) : $value;
1068  $kendoRecord[$kendoCol] = $value;
1069 
1070  CaptureBooleanValues($booleanFields, $kendoCol, $value); // Will find what is the true value and what is the false value.
1071 
1072  if (HCU_array_key_exists($key, $datetimeFields) && $value != "") {
1073  // Apply Credit Union timezone.
1074  $dateTime = new DateTime($value);
1075  $dateTime->setTimezone(new DateTimeZone($tz));
1076  $kendoRecord[$kendoCol] = $dateTime->format("Y-m-d H:i:s.u") . "Z";
1077  }
1078 
1079  }
1080 
1081  if ($dataFunc != "") {
1082  $dataFunc($Cu, $dbh, $kendoRecord, $dataFuncVariables);
1083  }
1084 
1085  if ($groupBy != "") {
1086  $groupCol = GetCamelCase($groupBy);
1087  $groupValue = $kendoRecord[$groupCol];
1088 
1089  if ($first || $groupValue != $currentGroup) {
1090  $currentGroup = $groupValue;
1091  $reportData[$groupValue] = array("field" => $groupCol, "value" => $groupValue, "items" => array(), "hasSubgroups" => false, "aggregates" => array());
1092 
1093  if ($groupDescription != "") {
1094  $descCol = GetCamelCase($groupDescription);
1095  $reportData[$groupValue]["aggregates"][$descCol]["max"] = $kendoRecord[$descCol];
1096  }
1097  $first = false;
1098  }
1099  $reportData[$groupValue]["items"][] = $kendoRecord;
1100  } else {
1101  $reportData[] = $kendoRecord;
1102  }
1103 
1104 
1105  }
1106 
1107  if ($initial && count($booleanFields) > 0) { // Need to get the true and false and get the ddl for filtering on booleans.
1108  foreach($booleanFields as $boolCol => $booleanItem) {
1109  $booleanRedefinition[$boolCol] = array("col" => $boolCol,
1110  "values" => array(array("text" => "Y", "value" => $booleanItem["trueValue"]), array("text" => "N", "value" => $booleanItem["falseValue"])));
1111  }
1112  $booleanRedefinition = array_values($booleanRedefinition);
1113  }
1114  return $groupBy != "" ? array_values($reportData) : $reportData;
1115 }
1116 
1117 /**
1118  * function CaptureBooleanValues(&$booleanFields, $kendoCol, $value)
1119  * This function will check the value of boolean field. If the true or false value is not known, then it will assign the current value as true or false for the DDL.
1120  *
1121  * @param $booleanFields -- the array to capture the boolean value.
1122  * @param $kendoCol -- the column currently looked at.
1123  * @param $value -- the value of the column currently looked at.
1124  */
1125 function CaptureBooleanValues(&$booleanFields, $kendoCol, $value) {
1126  if (HCU_array_key_exists($kendoCol, $booleanFields)) {
1127  $falseFound = $booleanFields[$kendoCol]["falseFound"];
1128  $trueFound = $booleanFields[$kendoCol]["trueFound"];
1129  $updateTrue = false;
1130  $updateFalse = false;
1131  if (!$trueFound || !$falseFound) {
1132  switch($value) {
1133  case "Yes":
1134  if (!$falseFound) {
1135  $booleanFields[$kendoCol]["falseValue"] = "No";
1136  }
1137  $updateTrue = true;
1138  break;
1139  case "yes":
1140  if (!$falseFound) {
1141  $booleanFields[$kendoCol]["falseValue"] = "no";
1142  }
1143  $updateTrue = true;
1144  break;
1145  case "Y":
1146  if (!$falseFound) {
1147  $booleanFields[$kendoCol]["falseValue"] = "N";
1148  }
1149  $updateTrue = true;
1150  break;
1151  case "y":
1152  if (!$falseFound) {
1153  $booleanFields[$kendoCol]["falseValue"] = "n";
1154  }
1155  $updateTrue= true;
1156  break;
1157  case "1":
1158  if (!$falseFound) {
1159  $booleanFields[$kendoCol]["falseValue"] = "0";
1160  }
1161  $updateTrue = true;
1162  break;
1163  case "true":
1164  if (!$falseFound) {
1165  $booleanFields[$kendoCol]["falseValue"] = "false";
1166  }
1167  $updateTrue = true;
1168  break;
1169  case "True":
1170  if (!$falseFound) {
1171  $booleanFields[$kendoCol]["falseValue"] = "False";
1172  }
1173  $updateTrue = true;
1174  break;
1175  case "No":
1176  if (!$trueFound) {
1177  $booleanFields[$kendoCol]["trueValue"] = "Yes";
1178  }
1179  $updateFalse = true;
1180  break;
1181  case "no":
1182  if (!$trueFound) {
1183  $booleanFields[$kendoCol]["trueValue"] = "yes";
1184  }
1185  $updateFalse = true;
1186  break;
1187  case "n":
1188  if (!$trueFound) {
1189  $booleanFields[$kendoCol]["trueValue"] = "y";
1190  }
1191  $updateFalse = true;
1192  break;
1193  case "N":
1194  if (!$trueFound) {
1195  $booleanFields[$kendoCol]["trueValue"] = "Y";
1196  }
1197  $updateFalse = true;
1198  break;
1199  case "0":
1200  if (!$trueFound) {
1201  $booleanFields[$kendoCol]["trueValue"] = "1";
1202  }
1203  $updateFalse = true;
1204  break;
1205  case "false":
1206  if (!$trueFound) {
1207  $booleanFields[$kendoCol]["trueValue"] = "true";
1208  }
1209  $updateFalse = true;
1210  break;
1211  case "False":
1212  if (!$trueFound) {
1213  $booleanFields[$kendoCol]["trueValue"] = "True";
1214  }
1215  $updateFalse = true;
1216  break;
1217  default:
1218  $updateFalse = true;
1219  }
1220 
1221  if ($updateTrue) {
1222  $booleanFields[$kendoCol]["trueValue"] = $value;
1223  $booleanFields[$kendoCol]["trueFound"] = true;
1224  $kendoRecord[$kendoCol] = true;
1225  } else if ($updateFalse) {
1226  $booleanFields[$kendoCol]["falseValue"] = $value;
1227  $booleanFields[$kendoCol]["falseFound"] = true;
1228  $kendoRecord[$kendoCol] = false;
1229  }
1230  }
1231  }
1232 }
1233 
1234 /**
1235  * function AddSort($sort, $cols, $groupBy, $groupDescription, $groupForceSortBy, $defaultSort)
1236  * Add the sort clause to the SQL.
1237  *
1238  * @param string $sort -- a JSON encoded string of the current sort in the grid.
1239  * @param array $cols -- the column definition in the $reportRecord.
1240  * @param string $groupBy -- the column to group by (will need to sort by this column first before the actual sort.)
1241  * @param string $groupDescription -- if this exists, then the first sort is this column. Otherwise, it will be the $groupBy column. (If both don't exist, then the sort is normal.)
1242  * @param boolean $groupForceSortBy -- if both $groupBy and $groupDescription exist and this is true, then it sorts by the $groupBy.
1243  * @param array $defaultSort -- if set, then use this sort when there is no actual sort.
1244  *
1245  * @return array("appendToSQL" => the order bys to append to the SQL, "status" => "000" if successful, nonzero number otherwise, "error" => the error encountered)
1246  */
1247 function AddSort($sort, $cols, $groupBy, $groupDescription, $groupForceSortBy, $defaultSort) {
1248  $colTypeMap = array();
1249  $sortSQL = array();
1250  $appendToSQL = "";
1251 
1252  try {
1253  foreach($cols as $colTitle => $colType) {
1254  $colTypeMap[getCamelCase($colTitle)] = $colType;
1255  }
1256 
1257  if ($groupBy != "") { // Need to check if there is a group by first.
1258  $groupCol = getCamelCase($groupDescription != "" && !$groupForceSortBy ? $groupDescription : $groupBy);
1259  $sortSQL[] = "$groupCol asc";
1260  }
1261 
1262  if ($sort != "") {
1263  $sort = HCU_JsonDecode($sort);
1264  if (!is_array($sort)) {
1265  throw new exception("Sort is not a valid array.", 401);
1266  }
1267 
1268  foreach($sort as $sortItem) {
1269  if (!HCU_array_key_exists("field", $sortItem) || !HCU_array_key_exists("dir", $sortItem)) {
1270  throw new exception("Sort is not a valid array.", 402);
1271  }
1272  if (!in_array($sortItem["dir"], array("asc", "desc"))) {
1273  throw new exception("Sort is not a valid array.", 403);
1274  }
1275  $sortSQL[] = getSortCol($sortItem["field"], $colTypeMap[$sortItem["field"]]) . " " . $sortItem["dir"];
1276  }
1277  } else if (isset($defaultSort)) {
1278  if (!is_array($defaultSort)) {
1279  throw new exception("Sort is not a valid array.", 404);
1280  }
1281 
1282  foreach($defaultSort as $sortItem) {
1283  if (!HCU_array_key_exists("field", $sortItem) || !HCU_array_key_exists("dir", $sortItem)) {
1284  throw new exception("Sort is not a valid array.", 402);
1285  }
1286  if (!in_array($sortItem["dir"], array("asc", "desc"))) {
1287  throw new exception("Sort is not a valid array.", 403);
1288  }
1289  $sortSQL[] = getSortCol($sortItem["field"], $colTypeMap[$sortItem["field"]]) . " " . $sortItem["dir"];
1290  }
1291  } else { // There is always a sort. If there isn't one, then it is the first column asc
1292  foreach($cols as $colTitle => $colType) {
1293  if ($colType != "ignore" && $colType != "hidden") {
1294  $sortCol = getSortCol(getCamelCase($colTitle), $colType);
1295  $sortSQL[] = "$sortCol asc";
1296  break;
1297  }
1298  }
1299  }
1300 
1301  // Need to take it out of if/else. Even though there has to be a sort by and if there isn't then there is a default for the first column. Now, groups as well.
1302  if (isset($sortSQL) && count($sortSQL) != 0) {
1303  $appendToSQL .= " order by " . implode(", ", $sortSQL);
1304  }
1305 
1306  $returnArray = array("appendToSQL" => $appendToSQL, "status" => "000", "error" => "");
1307  } catch (exception $e) {
1308  $returnArray = array("appendToSQL" => "", "status" => $e->getCode(), "error" => $e->getMessage());
1309  }
1310 
1311  return $returnArray;
1312 }
1313 
1314 /**
1315  * function AddNonSqlSort(&$unprocessedDataArray, $sort, $cols, $groupBy, $groupDescription, $groupForceSortBy)
1316  * Takes an array and sorts it according to the sorts from the KendoGrid.
1317  *
1318  * @param $unprocessedDataArray -- the inputted array.
1319  * @param $sort -- the JSON encoded string representation of the sort object within the KendoGrid.
1320  * @param $cols -- the columns array from the report definition array.
1321  * @param $groupBy -- the groupBy attribute in the report definition array.
1322  * @param $groupDescription -- the groupDescription attribute in the report definition array.
1323  * @param $groupForceSortBy -- the groupForceSortBy attribute in the report definition array.
1324  *
1325  * @return array("data" => data sorted, "status" => "000" if successful, non-zero if not successful, "error" => empty string if successful, otherwise the error message.)
1326  */
1327 function AddNonSqlSort($unprocessedDataArray, $sort, $cols, $groupBy, $groupDescription, $groupForceSortBy) {
1328  $colTypeMap = array();
1329  $compArray = array();
1330  $returnArray = array("data" => $unprocessedDataArray, "status" => "000", "error" => "");
1331  try {
1332  foreach($cols as $colTitle => $colType) {
1333  $colTypeMap [GetCamelCase($colTitle)] = $colType;
1334  }
1335 
1336  if ($groupBy != "") { // Need to check if there is a group by first.
1337  $groupCol = GetCamelCase($groupDescription != "" && !$groupForceSortBy ? $groupDescription : $groupBy);
1338  $compArray [] = array("field" => $groupCol, "type" => "string", "dir" => "asc"); // Possibly the "string" needs to not be hardcoded.
1339  }
1340 
1341  if ($sort != "") {
1342  $sort = HCU_JsonDecode($sort);
1343  if (!is_array($sort)) {
1344  throw new exception("Sort is not a valid array.", 401);
1345  }
1346 
1347  foreach($sort as $sortItem) {
1348 
1349  if (!HCU_array_key_exists("field", $sortItem) || !HCU_array_key_exists("dir", $sortItem)) {
1350  throw new exception("Sort is not a valid array.", 402);
1351  }
1352 
1353  if (!in_array($sortItem["dir"], array("asc", "desc"))) {
1354  throw new exception("Sort is not a valid array.", 403);
1355  }
1356 
1357  $compArray [] = array("field" => $sortItem["field"], "type" => $colTypeMap[$sortItem["field"]], "dir" => $sortItem["dir"]);
1358  }
1359 
1360  // Natural sort is whatever order the function returns data in.
1361  if (count($sort) > 0) {
1362  usort($unprocessedDataArray, function($a, $b) use ($compArray) {
1363  return CmpSort($a, $b, $compArray);
1364  });
1365  }
1366 
1367  $returnArray = array("data" => $unprocessedDataArray, "status" => "000", "error" => "");
1368  }
1369  } catch (exception $e) {
1370  $returnArray = array("data" => array(), "status" => $e->getCode(), "error" => $e->getMessage());
1371  }
1372 
1373  return $returnArray;
1374 }
1375 
1376 /**
1377  * function GetSortCol($kendoCol, $type)
1378  * Gets the particular sort of the column based on the column type. So far, there is just the string compare and the number compare.
1379  *
1380  * @param $kendoCol -- the kendo column name.
1381  * @param $type -- the type of the kendo column.
1382  * @param $isFilter -- if this function is used for filtering dates are cast to the date. For sorting dates are cast to timestamp. The "equal" operator seems to be more for the date.
1383  * @return the string of the comparison SQL clause.
1384  */
1385 function GetSortCol($sortCol, $type, $isFilter = false) {
1386  $sortCol = '"' . strtolower($sortCol) . '"';
1387  switch($type) {
1388  case "string":
1389  case "list":
1390  case "boolean":
1391  case "description":
1392  return "lower(trim(coalesce($sortCol::varchar, '')))";
1393  case "number":
1394  case "currency":
1395  case "decimal":
1396  return "$sortCol::decimal"; // Ensure that it sorts as a number not as ascii.
1397  case "date":
1398  case "datetime":
1399  return "(case when trim(coalesce($sortCol::varchar, '')) = '' then null else $sortCol end)::" . ($isFilter ? "date" : "timestamp");
1400  default:
1401  return $sortCol;
1402  }
1403 }
1404 
1405 /**
1406  * function CmpSort($a, $b, $sortArray)
1407  * This is a custom sort function for if the sort needed to happen after it was retrieved from the database. In the case of getting data from www3, www5, www6, conbining and then sorting.
1408  *
1409  * @param string $a -- the first object to check
1410  * @param string $b -- the second object to check
1411  * @param array $sortArray -- equivalent to the sort array in kendo.
1412  */
1413 function CmpSort($a, $b, $sortArray) {
1414  $cmp = 0; // $a > $b is positive; $a < $b is negative; 0 means that $a is equal to $b.
1415  foreach($sortArray as $sortRow) {
1416  switch($sortRow["type"]) {
1417  case "string":
1418  case "list":
1419  case "odata":
1420  case "hidden":
1421  case "ignore":
1422  case "description":
1423  case "unencoded":
1424  case "unencoded":
1425  $aCmp = trim($a[$sortRow["field"]]);
1426  $bCmp = trim($b[$sortRow["field"]]);
1427  $cmp = $sortRow["dir"] == "asc" ? strcmp($aCmp, $bCmp) : strcmp($bCmp, $aCmp);
1428  break;
1429  case "boolean":
1430  $aCmp = $a[$sortRow["field"]];
1431  $bCmp = $b[$sortRow["field"]];
1432  $aCmp = $aCmp === true ? 1 : ($aCmp === false ? 0 : (in_array(strtolower(trim(strval($aCmp))), array("true", "t", "y", "yes", "1")) ? 1 : 0));
1433  $bCmp = $bCmp === true ? 1 : ($bCmp === false ? 0 : (in_array(strtolower(trim(strval($bCmp))), array("true", "t", "y", "yes", "1")) ? 1 : 0));
1434  $cmp = $sortRow["dir"] == "asc" ? $aCmp - $bCmp : $bCmp - $aCmp;
1435  break;
1436  case "number":
1437  case "decimal":
1438  case "currency":
1439  $aCmp = floatval($a[$sortRow["field"]]);
1440  $bCmp = floatval($b[$sortRow["field"]]);
1441  $cmp = $sortRow["dir"] == "asc" ? $aCmp - $bCmp : $bCmp - $aCmp;
1442  break;
1443  case "date":
1444  case "datetime":
1445  $aCmp = strtotime($a[$sortRow["field"]]);
1446  $bCmp = strtotime($b[$sortRow["field"]]);
1447  $cmp = $sortRow["dir"] == "asc" ? $aCmp - $bCmp : $bCmp - $aCmp;
1448  break;
1449  }
1450 
1451  if ($cmp != 0) {
1452  break;
1453  }
1454  }
1455 
1456  return $cmp;
1457 }
1458 
1459 /**
1460  * function AddFilter($filter, $cols)
1461  * This will add the filter to current report SQL.
1462  *
1463  * @param array $filter -- the current filter of the grid.
1464  * @param array $cols -- the column definition from the $reportRecord.
1465  * @return array("appendToSQL" => the filter where clause to add to SQL, "status" => "000" if successful, nonzero number otherwise, "error" => the error encountered)
1466  */
1467 function AddFilter($filter, $cols) {
1468 
1469  $appendToSQL = array();
1470 
1471  try {
1472  if ($filter != "") {
1473  $filter = HCU_JsonDecode($filter);
1474  if (!is_array($filter)) {
1475  throw new exception("Filter is not a valid array.", 501);
1476  }
1477  if (!HCU_array_key_exists("logic", $filter) || !HCU_array_key_exists("filters", $filter)) {
1478  throw new exception("Filter is not a valid array.", 502);
1479  }
1480  if (!in_array($filter["logic"], array("and", "or"))) {
1481  throw new exception("Filter is not a valid array.", 503);
1482  }
1483 
1484  $opStartMap = array("eq" => "= '", "neq" => "<> '", "lt" => "< '", "lte" => "<= '", "gt" => "> '", "gte" => ">= '", "startswith" => "like '", "contains" => "like '%",
1485  "endswith" => "like '%", "doesnotcontain" => "not like '%");
1486  $opEndMap = array("eq" => "'", "neq" => "'", "lt" => "'", "lte" => "'", "gt" => "'", "gte" => "'", "startswith" => "%'", "contains" => "%'", "endswith" => "'", "doesnotcontain" => "%'");
1487  $colMap = array();
1488 
1489  foreach($cols as $colTitle => $colValue) {
1490  $key = GetCamelCase($colTitle);
1491  $colMap[$key] = $colTitle;
1492  }
1493 
1494  foreach($filter["filters"] as $filterItem) {
1495  if (HCU_array_key_exists("logic", $filterItem) || HCU_array_key_exists("filters", $filterItem)) {
1496  throw new exception("At this time, multiple levels of filters are not allowed.", 504);
1497  }
1498  if (!HCU_array_key_exists("field", $filterItem) || !HCU_array_key_exists("operator", $filterItem) || !HCU_array_key_exists("value", $filterItem)) {
1499  throw new exception("Filter is not a valid array.", 505);
1500  }
1501  $databaseCol = trim($filterItem["field"]);
1502  $op = strtolower(trim($filterItem["operator"]));
1503  $value = prep_save(strtolower(trim($filterItem["value"])));
1504 
1505  // Need to configure is null and is not null.
1506  $fullOp = $op == "isnull" ? "\"" . strtolower($databaseCol) . "\" is null" : ($op == "isnotnull" ? "\"" . strtolower($databaseCol) . "\" is not null" :
1507  getSortCol($databaseCol, $cols[$colMap[$databaseCol]], true) . " " . $opStartMap[$op] . trim($value) . $opEndMap[$op]);
1508 
1509  $appendToSQL[] = $fullOp;
1510  }
1511  }
1512 
1513  $appendToSQL = count($appendToSQL) == 0 ? "" : " where (" . implode(") " . $filter["logic"] . " (", $appendToSQL) . ")";
1514 
1515  $returnArray = array("appendToSQL" => $appendToSQL, "status" => "000", "error" => "");
1516  } catch (exception $e) {
1517  $returnArray = array("appendToSQL" => "", "status" => $e->getCode(), "error" => $e->getMessage());
1518  }
1519 
1520  return $returnArray;
1521 }
1522 
1523 /**
1524  * function AddNonSqlFilter(&$unprocessedDataArray, $filter, $cols)
1525  * This will add a filter to an array based on the columns types and the filter array sent from the KendoGrid.
1526  *
1527  * @param $unprocessedDataArray -- the inputted array.
1528  * @param $sort -- the JSON encoded string representation of the sort object within the KendoGrid.
1529  * @param $cols -- the columns array from the report definition array.
1530  * @param $groupBy -- the groupBy attribute in the report definition array.
1531  * @param $groupDescription -- the groupDescription attribute in the report definition array.
1532  * @param $groupForceSortBy -- the groupForceSortBy attribute in the report definition array.
1533  *
1534  * @return array("data" => the filtered array, "status" => "000" if successful, nonzero if not, "error" => empty string if successful, otherwise error message)
1535  */
1536 function AddNonSqlFilter($unprocessedDataArray, $filter, $cols) {
1537  $returnArray = array("data" => $unprocessedDataArray, "status" => "000", "error" => "");
1538  try {
1539  if ($filter != "") {
1540  $filter = HCU_JsonDecode($filter);
1541  if (!is_array($filter)) {
1542  throw new exception("Filter is not a valid array.", 501);
1543  }
1544  if (!HCU_array_key_exists("logic", $filter) || !HCU_array_key_exists("filters", $filter)) {
1545  throw new exception("Filter is not a valid array.", 502);
1546  }
1547  if (!in_array($filter["logic"], array("and", "or"))) {
1548  throw new exception("Filter is not a valid array.", 503);
1549  }
1550 
1551  $colMap = array();
1552 
1553  foreach($cols as $colTitle => $colValue) {
1554  $key = getCamelCase($colTitle);
1555  $colMap[$key] = $colTitle;
1556  }
1557 
1558  $simplifiedTypeMap = array("number" => "number", "decimal" => "number", "currency" => "number", "string" => "string", "list" => "string", "date" => "date", "odata" => "string",
1559  "boolean" => "boolean", "ignore" => "string", "hidden" => "string", "description" => "string", "unencoded" => "string", "datetime" => "date");
1560 
1561  $filterArray = array();
1562  $logic = $filter["logic"];
1563  foreach($filter["filters"] as $filterItem) {
1564  if (HCU_array_key_exists("logic", $filterItem) || HCU_array_key_exists("filters", $filterItem)) {
1565  throw new exception("At this time, multiple levels of filters are not allowed.", 504);
1566  }
1567  if (!HCU_array_key_exists("field", $filterItem) || !HCU_array_key_exists("operator", $filterItem) || !HCU_array_key_exists("value", $filterItem)) {
1568  throw new exception("Filter is not a valid array.", 505);
1569  }
1570 
1571  $field = trim($filterItem["field"]);
1572  $type = $cols[$colMap[$field]];
1573  $type = $simplifiedTypeMap[$type];
1574 
1575  $filterArray [] = array("field" => $field, "operator" => strtolower(trim($filterItem["operator"])), "value" => strtolower(trim($filterItem["value"])),
1576  "type" => $type);
1577  }
1578 
1579  $unprocessedDataArray = array_filter($unprocessedDataArray, function($value) use ($filterArray, $logic) {
1580  return CmpFilter($value, $filterArray, $logic);
1581  });
1582 
1583  /* From array_filter documentation: Because array_filter() preserves keys, you should consider the resulting array to be an associative array even if the original array had integer keys
1584  for there may be holes in your sequence of keys. This means that, for example, json_encode() will convert your result array into an object instead of an array.
1585  Call array_values() on the result array to guarantee json_encode() gives you an array. */
1586  $returnArray = array("data" => array_values($unprocessedDataArray), "status" => "000", "error" => "");
1587  }
1588  } catch (exception $e) {
1589  $returnArray = array("data" => array(), "status" => $e->getCode(), "error" => $e->getMessage());
1590  }
1591 
1592  return $returnArray;
1593 }
1594 
1595 /**
1596  * function CmpFilter($dataRow, $filterArray, $logic)
1597  * Callback function to see if to keep row based on the filtering.
1598  *
1599  * @param array $dataRow -- the row to check if it fits.
1600  * @param array $filterArray -- the filters from the KendoGrid.
1601  * @param string $logic -- {and/or} If "and", then results of comparisons are &&ed together. If "or", then results of comparisons are ||ed together.
1602  */
1603 function CmpFilter($dataRow, $filterArray, $logic) {
1604 
1605  $keepRow = true;
1606  foreach($filterArray as $filterRow) {
1607  $dataValue = $dataRow[$filterRow["field"]];
1608  $filterValue = $filterRow["value"];
1609  $filterValueLength = strlen($filterValue);
1610  $cmp = true;
1611 
1612  $dataDate = null;
1613  $filterDate = null;
1614  $dataDateFormat = "";
1615  $filterDateFormat = "";
1616  $filterBoolean = null;
1617  $dataBoolean = null;
1618  if ($filterRow["type"] == "date") {
1619  $dataDate = DateTime::createFromFormat("Y-m-d H:i:s+|", $dataValue);
1620  $dataDate = !$dataDate ? DateTime::createFromFormat("Y-m-d+|", $dataValue) : $dataDate;
1621  $filterDate = DateTime::createFromFormat("Y-m-d H:i:s+|", $filterValue);
1622  $filterDate = !$filterDate ? DateTime::createFromFormat("Y-m-d+|", $filterValue) : $filterDate;
1623  $dataDateFormat = $dataDate->format("Y-m-d");
1624  $filterDateFormat = $filterDate->format("Y-m-d");
1625  } else if ($filterRow["type"] == "boolean") {
1626  $dataBoolean = $dataValue === true ? 1 : ($dataValue === false ? 0 : (in_array(strtolower(trim(strval($dataValue))), array("true", "t", "y", "yes", "1")) ? 1 : 0));
1627  $filterBoolean = $filterValue === true ? 1 : ($filterValue === false ? 0 : (in_array(strtolower(trim(strval($filterValue))), array("true", "t", "y", "yes", "1")) ? 1 : 0));
1628  } else if ($filterRow["type"] == "string") {
1629  $dataValue = trim(strtolower($dataValue));
1630  $filterValue = trim(strtolower($filterValue));
1631  }
1632 
1633  switch ($filterRow["type"] . " | " . $filterRow["operator"]) {
1634  case "number | eq": $cmp = floatval($dataValue) == floatval($filterValue); break;
1635  case "number | neq": $cmp = floatval($dataValue) != floatval($filterValue); break;
1636  case "number | lt": $cmp = floatval($dataValue) < floatval($filterValue); break;
1637  case "number | lte": $cmp = floatval($dataValue) <= floatval($filterValue); break;
1638  case "number | gt": $cmp = floatval($dataValue) > floatval($filterValue); break;
1639  case "number | gte": $cmp = floatval($dataValue) >= floatval($filterValue); break;
1640  case "string | eq": $cmp = strcmp($dataValue, $filterValue) == 0; break;
1641  case "string | neq": $cmp = strcmp($dataValue, $filterValue) != 0; break;
1642  case "string | startswith": $cmp = substr($dataValue, 0, $filterValueLength) == $filterValue; break;
1643  case "string | endswith": $cmp = substr($dataValue, -$filterValueLength) == $filterValue; break;
1644  case "string | contains": $cmp = strpos($dataValue, $filterValue) !== false; break;
1645  case "string | doesnotcontain": $cmp = strpos($dataValue, $filterValue) === false; break;
1646  case "date | eq": $cmp = strcmp($dataDateFormat, $filterDateFormat) == 0; break;
1647  case "date | neq": $cmp = strcmp($dataDateFormat, $filterDateFormat) != 0; break;
1648  case "date | gt": $cmp = $dataDate > $filterDate; break;
1649  case "date | gte": $cmp = $dataDate >= $filterDate; break;
1650  case "date | lt": $cmp = $dataDate < $filterDate; break;
1651  case "date | lte": $cmp = $dataDate <= $filterDate; break;
1652  case "boolean | eq": $cmp = intval($dataBoolean) == intval($filterBoolean); break;
1653  case "boolean | neq": $cmp = intval($dataBoolean) != intval($filterBoolean); break;
1654  case "boolean | isnull": $cmp = !isset($dataValue); break;
1655  case "boolean | isnotnull": $cmp = isset($dataValue); break;
1656  }
1657 
1658  $keepRow = $logic == "and" ? $keepRow && $cmp : $keepRow || $cmp;
1659 
1660  if (($logic == "and" && !$keepRow) || ($logic == "or" && $keepRow)) { // Shortcut. If there is one false, then AND is false. If there is one true, then OR is true.
1661  break;
1662  }
1663  }
1664 
1665  return $keepRow;
1666 }
1667 
1668 /**
1669  * function ReadReportData($reportRecord, $dbh, $limit, $offset, $includeCount, $dontIncludeId, $initial, $excludeSort, $sort, $filter, $report, $cookieName)
1670  * Will get the data for the report. This is the main function to call for the main read function and the CSV read function.
1671  *
1672  * @param $Cu -- the credit union
1673  * @param $reportRecord -- the record in the $reportList for the particular $report.
1674  * @param $dbh -- the database connection.
1675  * @param $limit -- how many records to show.
1676  * @param $offset -- where to start the record count.
1677  * @param $includeCount -- where to run the SQL to see how many records there are. This happens on the initial data call and when the filter changes. It doesn't happen when getting CSV.
1678  * @param $dontIncludeId -- if this is false, then a kendoId column is added for the dataSource > schema > model > id attribute in the kendo grid.
1679  * @param $initial -- if this is true, then get the true/false values for the boolean fields.
1680  * @param $excludeSort -- by default, a sort is added to the first column. If a sort is defined, then it uses that sort. This is for when the sort happens later.
1681  * @param $sort -- the current sort of the grid.
1682  * @param $filter -- the current filter of the grid.
1683  * @param $report -- the $reportKey in the $reportList.
1684  * @param $cookieName -- the name of the cookie. This is only relevant if info needs to be cached or whatnot.
1685  *
1686  * @return array("error" => $errors, "status" => $status, "reportData" => $reportData, "sql" => $sqls, "booleanRedefinition" => $booleanRedefinition);
1687  * $errors -- the first error encountered or an empty array
1688  * $status -- "000" if no errors; nonzero otherwise
1689  * $sql -- all SQLs used. (Usually the SQL to get the data only. If $includeCount, then also the SQL to get the count.)
1690  * $booleanRedefinition -- an array of DDLs to update the grid with. This happens on the first data call.
1691  */
1692 function ReadReportData($Cu, $reportRecord, $dbh, $limit, $offset, $includeCount, $dontIncludeId, $initial, $excludeSort, $sort, $filter, $report, $cookieName) {
1693  try {
1694 
1695  validateReportRecord($reportRecord);
1696 
1697  $baseSQL = HCU_array_key_exists("sql", $reportRecord) ? $reportRecord["sql"] : "";
1698  $nonSqlDataFunc = HCU_array_key_exists("nonSqlDataFunc", $reportRecord) ? $reportRecord["nonSqlDataFunc"] : "";
1699 
1700  $cols = HCU_array_key_exists("cols", $reportRecord) ? $reportRecord["cols"] : array();
1701  $groupBy = HCU_array_key_exists("groupBy", $reportRecord) ? trim($reportRecord["groupBy"]) : "";
1702  $groupDescription = HCU_array_key_exists("groupDescription", $reportRecord) ? trim($reportRecord["groupDescription"]) : "";
1703  $groupForceSortBy = HCU_array_key_exists("groupForceSortBy", $reportRecord) ? $reportRecord["groupForceSortBy"] : false;
1704  $dataFunc = HCU_array_key_exists("dataFunc", $reportRecord) ? trim($reportRecord["dataFunc"]) : "";
1705  $dataFuncVariables = HCU_array_key_exists("dataFuncVariables", $reportRecord) ? $reportRecord["dataFuncVariables"] : array();
1706  $defaultSort = HCU_array_key_exists("defaultSort", $reportRecord) ? $reportRecord["defaultSort"] : null;
1707 
1708  // Likely this syntax can be replaced with nonSqlDataFunc as it should be a more wholistic approach to the two reports in adm_reports.prg that use the replaceString.
1709  if (isset($replaceString) && isset($replaceCode)) {
1710  $cookie = array();
1711  $replaceCodeString = "";
1712  $cookieExists = false;
1713  if (isset($_COOKIE[$cookieName])) {
1714  parse_str($_COOKIE[$cookieName], $cookie);
1715  $cookieExists = true;
1716  }
1717 
1718  if ($cookieExists && isset($cookie[$report]) && trim($cookie[$report]) != "") {
1719  $replaceCodeString = trim($cookie[$report]);
1720  } else {
1721  $replaceCodeString = eval($replaceCode);
1722  $cookie[$report] = $replaceCodeString;
1723  $expiresInDay = time() + 86400;
1724 
1725  setcookie($cookieName, http_build_query($cookie), $expiresInDay, "/", $_SERVER['SERVER_NAME']);
1726  }
1727  $baseSQL = str_replace($replaceString, $replaceCodeString, $baseSQL);
1728  }
1729 
1730  // Now, we have two options: use the SQL or use the sqlReplaceFunc. If we use the sqlReplaceFunc, then we will have to sort and filter ourselves.
1731  if ($baseSQL != "") {
1732  $results = UpdateSelectSQL($baseSQL, $cols);
1733  if ($results["status"] !== "000") {
1734  throw new exception("Update Select SQL failed.", 211);
1735  }
1736  $baseSQL = $results["modifiedSQL"];
1737 
1738  $fullSQL = "select * from ($baseSQL) t";
1739  $countSQL = "select count(*) from ($baseSQL) t";
1740 
1741  // If there are multiple servers, then we do still want to add the filter to the query.
1742  $results = AddFilter($filter, $cols);
1743  if ($results["status"] != "000") {
1744  throw new exception("SQL didn't filter correctly.", 210);
1745  }
1746 
1747  $fullSQL .= $results["appendToSQL"];
1748  $countSQL .= $results["appendToSQL"];
1749 
1750  if (!$excludeSort) { // If there are multiple servers, then this function is unnecessary.
1751  $results = AddSort($sort, $cols, $groupBy, $groupDescription, $groupForceSortBy, $defaultSort);
1752  if ($results["status"] != "000") {
1753  throw new exception("SQL didn't sort correctly.", 209);
1754  }
1755 
1756  $fullSQL .= $results["appendToSQL"];
1757  }
1758 
1759  if ($limit != -1)
1760  $fullSQL .= " limit $limit offset $offset";
1761 
1762  $sqls[] = $fullSQL;
1763 
1764  $sth = db_query($fullSQL, $dbh);
1765 
1766  $unprocessedDataArray = db_fetch_all($sth);
1767 
1768  // FALSE is returned if there are no rows in the result, or on any other error.
1769  $unprocessedDataArray = !$unprocessedDataArray ? array() : $unprocessedDataArray;
1770 
1771  $sql = $countSQL;
1772  $sqls[] = $countSQL;
1773  $sth = db_query($sql,$dbh);
1774  if (!$sth) {
1775  throw new exception("Count SQL failed.", 201);
1776  }
1777 
1778  list($count) = db_fetch_array($sth,0);
1779 
1780  } else if ($nonSqlDataFunc) {
1781 
1782  $sqls = array(); // No sqls but prevent an E_NOTICE.
1783  $results = $nonSqlDataFunc($Cu, $dbh);
1784 
1785  if (!isset($results)) {
1786  throw new exception("Data function is not formatted correctly.", 206);
1787  } else if (!HCU_array_key_exists("status", $results) || !HCU_array_key_exists("error", $results) || !HCU_array_key_exists("data", $results)) {
1788  throw new exception("Data function is not formatted correctly.", 207);
1789  } else if ($results["status"] !== "000") {
1790  throw new exception ($results["error"], 208);
1791  }
1792 
1793  if (HCU_array_key_exists("sql", $results)) {
1794  $sqls = $results["sql"];
1795  }
1796 
1797  $rawData = $results["data"];
1798  $unprocessedDataArray = array();
1799 
1800  // Add columns to rawData.
1801  $colCount = count($cols);
1802  $colnames = array();
1803  foreach($cols as $colTitle => $colType) { // Gets only the kendo column names.
1804  $colnames[] = getCamelCase($colTitle);
1805  }
1806  foreach($rawData as $dataRow) {
1807  if (count($dataRow) != $colCount) {
1808  throw new exception ("Data from function doesn't return columns necessary for the report.", 203);
1809  }
1810 
1811  $unprocessedDataArray [] = array_combine ($colnames, $dataRow);
1812  }
1813 
1814  // Filter
1815  $results = AddNonSqlFilter($unprocessedDataArray, $filter, $cols);
1816 
1817  if ($results ["status"] != "000") {
1818  throw new exception ("Data did not filter correctly.", 204);
1819  }
1820 
1821  $unprocessedDataArray = $results ["data"];
1822 
1823  $count = count($unprocessedDataArray);
1824 
1825  // Sort
1826  $results = AddNonSqlSort($unprocessedDataArray, $sort, $cols, $groupBy, $groupDescription, $groupForceSortBy);
1827 
1828  if ($results ["status"] != "000") {
1829  throw new exception ("Data did not filter correctly.", 205);
1830  }
1831 
1832  $unprocessedDataArray = $results ["data"];
1833 
1834  // Page
1835  if ($limit != -1) {
1836  $unprocessedDataArray = array_slice($unprocessedDataArray, $offset, $limit);
1837  }
1838 
1839  } else {
1840  throw new exception ("Either the 'sql' or the 'nonSqlDataFunc' need to be defined.", 202);
1841  }
1842 
1843  $booleanRedefinition = array();
1844  $reportData = FormatReportData($Cu, $dbh, $unprocessedDataArray, $cols, $groupBy, $groupDescription, $dontIncludeId, $initial, $booleanRedefinition, $dataFunc, $dataFuncVariables);
1845 
1846  $reportData = array("total" => -1, "data" => $reportData);
1847  if ($includeCount) {
1848  $reportData["total"] = $count;
1849  }
1850 
1851  $returnArray = array("error" => array(), "status" => "000", "reportData" => $reportData, "sql" => $sqls, "booleanRedefinition" => $booleanRedefinition);
1852  } catch(exception $e) {
1853  $returnArray = array("error" => array($e->getMessage()), "status" => $e->getCode(), "reportData" => array(), "sql" => $sqls);
1854  }
1855  return $returnArray;
1856 }
1857 
1858 /**
1859  * function ReadReportSchema($reportRecord, $defaultDateFormat)
1860  * Gets the schema for the kendo grid. That includes the column array, the model array, and the group array.
1861  *
1862  * @param $reportRecord -- the record from the $reportList according to the $report.
1863  * @param $defaultDateFormat -- the default date format. If the date format is not defined for the report, then it uses this value.
1864  *
1865  * @return array("error" => $errors, "status" => $status, "gridConf" => array("modelData" => $modelData, "columnData" => $columnData, "groupData" => $groupData, "sort" => $sort));
1866  * $errors -- the first error encountered or an empty array
1867  * $status -- "000" if no errors; nonzero otherwise
1868  * $gridConf -- contains all the configuration for the kendoGrid.
1869  * $modelData -- contains the configuration for the kendoGrid: dataSource > schema > model.
1870  * $columnData -- contains the configuration for the kendoGrid: columns.
1871  * $groupData -- contains the configuration for the kendoGrid: dataSource > group.
1872  * $sort -- contains the default sort: first non-ignore column.
1873  */
1874 function ReadReportSchema($reportRecord, $defaultDateFormat) {
1875  try {
1876  ValidateReportRecord($reportRecord);
1877 
1878  $modelData = array();
1879  $columnData = array();
1880  $groupData = null;
1881 
1882  $dateFormat = HCU_array_key_exists("dateFormat", $reportRecord) ? trim($reportRecord["dateFormat"]) : null;
1883  $format = isset($dateFormat) ? trim($dateFormat) : (isset($defaultDateFormat) ? trim($defaultDateFormat) : "");
1884  $dateIfNull = HCU_array_key_exists("dateEmpty", $reportRecord) ? trim($reportRecord["dateEmpty"]) : null;
1885  $replaceString = HCU_array_key_exists("replaceString", $reportRecord) ? trim($reportRecord["replaceString"]) : null;
1886  $replaceCode = HCU_array_key_exists("replaceCode", $reportRecord) ? trim($reportRecord["replaceCode"]) : null;
1887  $values = HCU_array_key_exists("ddl", $reportRecord) ? $reportRecord["ddl"] : null;
1888  $groupBy = HCU_array_key_exists("groupBy", $reportRecord) ? trim($reportRecord["groupBy"]) : null;
1889  $groupDescription = HCU_array_key_exists("groupDescription", $reportRecord) ? trim($reportRecord["groupDescription"]) : null;
1890  $cols = HCU_array_key_exists("cols", $reportRecord) ? $reportRecord["cols"] : array();
1891  $colHeader = HCU_array_key_exists("colHeader", $reportRecord) ? $reportRecord["colHeader"] : array();
1892  $schemaFunc = HCU_array_key_exists("schemaFunc", $reportRecord) ? $reportRecord["schemaFunc"] : "";
1893  $defaultSort = HCU_array_key_exists("defaultSort", $reportRecord) ? $reportRecord["defaultSort"] : null;
1894 
1895  $fieldData = array();
1896 
1897  if (count($colHeader) > 0) {
1898  foreach($colHeader as $header => $lastColumn) {
1899  $title = explode(".", $header)[0];
1900  $columnData[] = array("title" => $title, "columns" => array(), "lastColumn" => $lastColumn);
1901  }
1902  reset($columnData);
1903  }
1904 
1905  $sort = array("dir" => "asc");
1906  $first = true;
1907  foreach($cols as $colTitle => $colType) {
1908  $kendoCol = getCamelCase($colTitle);
1909  $fieldRecord = array();
1910  $newTitle = explode(".", $colTitle);
1911  $newTitle = $newTitle[0];
1912  $columnRecord = array("field" => $kendoCol, "title" => $newTitle);
1913  if ($colType == "ignore" || $colType == "boolean") {
1914  $fieldRecord["type"] = "string";
1915  } else if ($colType == "unencoded") {
1916  $fieldRecord["type"] = "string";
1917  $columnRecord["encoded"] = false;
1918  } else if ($colType == "hidden") {
1919  $fieldRecord["type"] = "string";
1920  $columnRecord["hidden"] = true;
1921  } else if ($colType == "date" || $colType == "datetime") {
1922  $fieldRecord["type"] = "date";
1923  if ($format != "") {
1924  $columnRecord["template"] = "# if ($kendoCol == null || $kendoCol == '') { # $dateIfNull # } else { # #: kendo.toString(new Date($kendoCol), '$format') # # } #";
1925  }
1926  } else if ($colType == "number") {
1927  $fieldRecord["type"] = "number";
1928  $columnRecord["attributes"] = array("class" => "textAlignRight");
1929  $columnRecord["headerAttributes"] = array("class" => "textAlignRight");
1930  } else if ($colType == "decimal") {
1931  $fieldRecord["type"] = "number";
1932  $columnRecord["attributes"] = array("class" => "textAlignRight");
1933  $columnRecord["headerAttributes"] = array("class" => "textAlignRight");
1934  $columnRecord["format"] = "{0:n}";
1935  } else if ($colType == "currency") {
1936  $fieldRecord["type"] = "number";
1937  $columnRecord["attributes"] = array("class" => "textAlignRight");
1938  $columnRecord["headerAttributes"] = array("class" => "textAlignRight");
1939  $columnRecord["format"] = "{0:c2}";
1940  } else if ($colType == "list") {
1941  $fieldRecord["type"] = "string";
1942 
1943  $ddl = array();
1944  foreach($values[$colTitle] as $value => $text) {
1945  $ddl[] = array("text" => $text, "value" => $value);
1946  }
1947  $columnRecord["values"] = $ddl;
1948  } else if ($colType == "description") {
1949  $fieldRecord["type"] = "string";
1950  $columnRecord["attributes"] = array("class" => "showEllipsis");
1951  $columnRecord["width"] = 100;
1952  } else {
1953  $fieldRecord["type"] = $colType;
1954  }
1955 
1956  if (isset($groupBy) && $groupBy == $colTitle) {
1957  $groupData = array("field" => $kendoCol);
1958 
1959  if (isset($groupDescription)) {
1960  $groupDescCol = getCamelCase($groupDescription);
1961  $groupData["aggregates"] = array(array("field" => $groupDescCol, "aggregate" => "max"));
1962  $columnRecord["groupHeaderTemplate"] = "#: aggregates.$groupDescCol.max #";
1963  } else {
1964  $columnRecord["groupHeaderTemplate"] = "#: value #";
1965  }
1966  }
1967 
1968  $fieldData[$kendoCol] = $fieldRecord;
1969 
1970  if ($schemaFunc != "") {
1971  $schemaFunc($Cu, $colTitle, $colType, $kendoCol, $columnRecord);
1972  }
1973 
1974  if ($colType != "ignore") {
1975  if (count($colHeader) > 0) {
1976  $key = key($columnData);
1977  $columnData[$key]["columns"][] = $columnRecord;
1978  if ($colTitle == $columnData[$key]["lastColumn"]) {
1979  unset($columnData[$key]["lastColumn"]);
1980  next($columnData);
1981  }
1982  } else {
1983  $columnData[] = $columnRecord;
1984  }
1985 
1986  if ($first && $colType != "hidden") { // Don't sort on hidden fields either
1987  if (isset($defaultSort)) {
1988  $sort = $defaultSort;
1989  } else {
1990  $sort["field"] = $kendoCol;
1991  }
1992  $first = false;
1993  }
1994 
1995  }
1996  }
1997 
1998  $idName = "reportId";
1999  $fieldData[$idName] = array("type" => "number");
2000  $modelData["id"] = $idName;
2001  $modelData["fields"] = $fieldData;
2002  } catch(exception $e) {
2003  return array("error" => array($e->getMessage()), "status" => $e->getCode(), "gridConf" => array());
2004  }
2005  return array("error" => array(), "status" => "000", "gridConf" => array("modelData" => $modelData, "columnData" => $columnData, "groupData" => $groupData, "sort" => $sort));
2006 }
2007 
2008 /**
2009  * function RunCurl($server, $parameters, $cmd, $cookieName)
2010  * Runs a curl call
2011  *
2012  * @param array $parameters -- additional parameters.
2013  * @param string $cmd -- the URL to run.
2014  * @param string $cookieName -- cookieName is relevant if one report has the replaceString and replaceCode attributes set.
2015  *
2016  * @return string -- whatever was returned by the call.
2017  */
2018 function RunCurl($parameters, $cmd, $cookieName) {
2019  $ch = curl_init($cmd);
2020 
2021  $cookieLine = "HCUTicket=" . urlencode($_COOKIE['HCUTicket']) . ($cookieName != "" ? ";$cookieName=" . urlencode($_COOKIE[$cookieName]) : "");
2022 
2023  curl_setopt($ch, CURLOPT_COOKIE, $cookieLine);
2024  curl_setopt($ch, CURLOPT_USERPWD,"nobody:no1home");
2025  curl_setopt($ch, CURLOPT_RETURNTRANSFER,1); # get response as string
2026  curl_setopt($ch, CURLOPT_POST, true);
2027  curl_setopt($ch, CURLOPT_POSTFIELDS, $parameters);
2028  curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
2029  curl_setopt($ch, CURLOPT_COOKIESESSION, true );
2030  curl_setopt($ch, CURLOPT_COOKIEJAR, "/home/homecu/tmp/reportCookie");
2031  curl_setopt($ch, CURLOPT_COOKIEFILE, "/home/homecu/tmp/reportCookie");
2032 
2033  $rawresp = curl_exec($ch);
2034 
2035  return $rawresp;
2036 }
2037 
2038 /**
2039  * function PrintCSVData($reportValues, $report, $reportTitle, $reportList, $defaultDateFormat, $delimiter = ",")
2040  * Downloads a CSV from the page.
2041  *
2042  * @param array $reportData -- the data of the report.
2043  * @param string $report -- the key of the report which is in the report definition array.
2044  * @param string $reportTitle -- what to save the CSV as.
2045  * @param array $reportList -- the report definition array.
2046  * @param string $defaultDateFormat -- the default date format. Now, in the configuration, this is intrepreted as the PHP date format so this is translated into javascript date format.
2047  * @param string $delimiter -- what should separate the columns and column values? A Comma is assumed.
2048  */
2049 function PrintCSVData($reportData, $report, $reportTitle, $reportList, $defaultDateFormat, $delimiter = ",") {
2050  $reportRecord = $reportList[$report];
2051  $reportName = $reportTitle != "" ? $reportTitle : (HCU_array_key_exists("title", $reportRecord) ? $reportRecord["title"] : trim($report));
2052  $kendoDateFormat = HCU_array_key_exists("dateFormat", $reportRecord) ? $reportRecord["dateFormat"] : (isset($defaultDateFormat) ? $defaultDateFormat : "MM/dd/yyyy");
2053  $showOnCSV = HCU_array_key_exists("showOnCSV", $reportRecord) ? $reportRecord["showOnCSV"] : array();
2054 
2055  $groupBy = HCU_array_key_exists("groupBy", $reportRecord) ? trim($reportRecord["groupBy"]) : "";
2056  $groupDescription = HCU_array_key_exists("groupDescription", $reportRecord) ? trim($reportRecord["groupDescription"]) : "";
2057 
2058  $csvData = array();
2059  $csvRow = array();
2060 
2061  $phpDateFormatArray = array("yyyy" => "Y", "dd" => "d", "HH" => "H", "MM" => "m", "mm" => "i", "ss" => "s");
2062  $dateArray = array();
2063  $ignoreMap = array();
2064  $currencyMap = array();
2065  $listMap = array();
2066  $unencodedMap = array();
2067 
2068  $phpDateFormat = $kendoDateFormat;
2069  foreach($phpDateFormatArray as $from => $to) {
2070  $phpDateFormat = str_replace($from, $to, $phpDateFormat);
2071  }
2072 
2073  if ($groupBy != "") {
2074  $csvRow[] = ""; // Indent report
2075  }
2076  foreach($reportRecord["cols"] as $colTitle => $colType) {
2077  $kendoCol = strtolower(GetCamelCase($colTitle));
2078 
2079  switch($colType) {
2080  case "date":
2081  case "datetime":
2082  $dateArray[$kendoCol] = true;
2083  break;
2084  case "currency":
2085  $currencyMap[$kendoCol] = true;
2086  break;
2087  case "list":
2088  $listMap[$kendoCol] = $reportRecord["ddl"][$colTitle];
2089  break;
2090  case "unencoded":
2091  $unencodedMap[$kendoCol] = true;
2092  break;
2093  }
2094 
2095  // Want the group description to show by default if it exist. Otherwise want the group id to show by default.
2096  $showColumnByGroup = $groupBy != "" ? ($groupDescription != "" ? $groupDescription == $colTitle : $groupBy == $colTitle) : false;
2097 
2098  // If the showOnCSV array sets this column to show false or the column is either ignore or hidden then don't show it on the CSV.
2099  $ignoreColumn = HCU_array_key_exists($colTitle, $showOnCSV) ? !$showOnCSV[$colTitle] : ($showColumnByGroup ? false : in_array($colType, array("ignore", "hidden")));
2100  $ignoreMap[$kendoCol] = $ignoreColumn;
2101 
2102  // Don't show it if it is part of the group because that will be in a different place in the CSV or if it is ignored.
2103  if ($colTitle == $groupBy || $colTitle == $groupDescription || $ignoreColumn) {
2104  continue;
2105  }
2106 
2107  $csvRow[] = getCsvValue(explode(".", $colTitle)[0], $delimiter);
2108  }
2109 
2110  $csvData[] = implode("$delimiter", $csvRow);
2111 
2112  if ($groupBy == "") {
2113  foreach($reportData as $row) {
2114  $csvRow = array();
2115  foreach($row as $col => $value) {
2116  $lcol = strtolower($col);
2117  if ($ignoreMap[$lcol]) {
2118  continue;
2119  }
2120  if (HCU_array_key_exists($lcol, $dateArray)) {
2121  try {
2122  if (isset($value) && trim($value) != "") {
2123  $dateTime = new DateTime($value);
2124  $value = $dateTime->format($phpDateFormat);
2125  }
2126  } catch (exception $e) {
2127  $value = "(Invalid Date Format)";
2128  }
2129  } else if (HCU_array_key_exists($lcol, $currencyMap)) {
2130  $value = intval($value) == 0 ? "" : "\$" . number_format(floatval($value), 2);
2131  } else if (HCU_array_key_exists($lcol, $listMap)) {
2132  $value = $listMap[$lcol][trim($value)];
2133  } else if (HCU_array_key_exists($lcol, $unencodedMap)) {
2134  $value = strip_tags($value);
2135  } else {
2136  $value = strval($value);
2137  }
2138 
2139  $csvRow[] = GetCsvValue($value, $delimiter);
2140  }
2141  $csvData[] = implode("$delimiter", $csvRow);
2142  }
2143  } else {
2144  $groupByCol = strtolower(GetCamelCase($groupBy));
2145  $groupDescriptionCol = strtolower(GetCamelCase($groupDescription));
2146 
2147  foreach($reportData as $row) {
2148  $csvData[] = "";
2149  $csvData[] = "";
2150 
2151  // Row telling the group by value.
2152  if (!$ignoreMap[$groupByCol]) {
2153  $csvData[] = implode("$delimiter", array(GetCsvValue($groupBy, $delimiter), GetCsvValue($row["value"], $delimiter)));
2154  }
2155 
2156  // Row telling the group description value.
2157  if ($groupDescription != "" && !$ignoreMap[$groupDescription]) {
2158  $descriptionValue = $row["aggregates"][GetCamelCase($groupDescription)]["max"];
2159 
2160  if (!$ignoreMap[$groupDescriptionCol]) {
2161  $csvData[] = implode("$delimiter", array(GetCsvValue($groupDescription, $delimiter), GetCsvValue($descriptionValue, $delimiter)));
2162  }
2163  }
2164 
2165  $csvData[] = "";
2166 
2167  foreach($row["items"] as $rowRow) {
2168 
2169  $csvRow = array(""); // Indent the report.
2170  foreach($rowRow as $col => $value) {
2171  $lower = strtolower($col);
2172  if ($lower == $groupByCol || $lower == $groupDescriptionCol || $ignoreMap[$lower]) {
2173  continue;
2174  }
2175 
2176  if (HCU_array_key_exists($lower, $dateArray)) {
2177  try {
2178  if (isset($value) && trim($value) != "") {
2179  $dateTime = new DateTime($value);
2180  $value = $dateTime->format($phpDateFormat);
2181  }
2182 
2183  } catch (exception $e) {
2184  $value = "(Invalid Date Format)";
2185  }
2186  } else if (HCU_array_key_exists($lower, $currencyMap)) {
2187  $value = intval($value) == 0 ? "" : "\$" . number_format(floatval($value), 2);
2188  } else if (HCU_array_key_exists($lower, $listMap)) {
2189  $value = $listMap[$lower][trim($value)];
2190  } else if (HCU_array_key_exists($lower, $unencodedMap)) {
2191  $value = strip_tags($value);
2192  } else {
2193  $value = strval($value);
2194  }
2195 
2196  $csvRow[] = GetCsvValue($value, $delimiter);
2197  }
2198  $csvData[] = implode("$delimiter", $csvRow);
2199  }
2200  }
2201  }
2202 
2203  $csvData = implode("\r\n", $csvData);
2204 
2205  header("Content-length: " . strlen($csvData) );
2206  header("Content-type: application/octetstream");
2207  header("Content-disposition: inline; filename=\"$reportName.csv\"");
2208  print ($csvData);
2209 
2210 }
2211 
2212 /**
2213  * function GetCsvValue($value, $delimiter)
2214  * This will get the CSV value from the regular value. It essentially escapes the delimiter and new lines.
2215  *
2216  * @param $value -- the value
2217  * @param $delimiter -- the delimiter (most likely a comma)
2218  * @return the escaped version of the value.
2219  */
2220 function GetCsvValue($value, $delimiter) {
2221  $value = str_replace("&nbsp;", " ", trim($value));
2222  if (strpos($value, "$delimiter") !== false || strpos($value, "\n") !== false) {
2223  $value = "\"$value\"";
2224  }
2225  return $value;
2226 }
2227 
2228 /**
2229  * function ValidateReportRecord($reportRecord, &$errors)
2230  * Validates the $reportRecord to spot some obvious problems before actually parsing the SQL and adding sort and filter and whatnot.
2231  *
2232  * @param array $reportRecord -- the array that has all the information to create a report.
2233  */
2234 function ValidateReportRecord($reportRecord) {
2235  if (!HCU_array_key_exists("sql", $reportRecord) && !HCU_array_key_exists("nonSqlDataFunc", $reportRecord)) {
2236  throw new exception("Either SQL or another way of getting the data is required.", 101);
2237  }
2238  if (!HCU_array_key_exists("cols", $reportRecord)) {
2239  throw new exception("Cols are required.", 102);
2240  }
2241  foreach($reportRecord["cols"] as $colName => $colType) {
2242  if (!in_array($colType, array("number", "decimal", "string", "date", "boolean", "odata", "currency", "ignore", "list", "hidden", "description", "unencoded", "datetime"))) {
2243  throw new exception("Column type is not known.", 103);
2244  }
2245  if ($colType == "list") {
2246  if (!HCU_array_key_exists("ddl", $reportRecord)) {
2247  throw new exception ("DDLs are not defined.", 104);
2248  }
2249  if (!HCU_array_key_exists($colName, $reportRecord["ddl"])) {
2250  throw new exception ("DDL is not defined for $colName.", 105);
2251  }
2252  }
2253  }
2254 }
GetCTE()
Definition: reporting.i:121
GetColumnSQL($toBeSummed)
Definition: reporting.i:142
GetColumns($columnArray)
Definition: reporting.i:163
__construct()
Definition: reporting.i:93