znt/module/pivot/model.php

3065 lines
101 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* The model file of pivot module of ZenTaoPMS.
*
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.cnezsoft.com)
* @license ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
* @author Chunsheng Wang <chunsheng@cnezsoft.com>
* @package pivot
* @version $Id: model.php 4726 2013-05-03 05:51:27Z chencongzhi520@gmail.com $
* @link https://www.zentao.net
*/
class pivotModel extends model
{
/**
* 初始化加载BI相关类。
* Construct,load BI related classes.
*
* @access public
* @return void
*/
public function __construct()
{
parent::__construct();
$this->loadBIDAO();
$this->loadModel('bi');
}
/**
* 判断是否有权限访问。
* Check pivot access.
*
* @param int $pivotID
* @access public
* @return array
*/
public function checkAccess($pivotID, $method = 'preview')
{
$viewableObjects = $this->bi->getViewableObject('pivot');
if(!in_array($pivotID, $viewableObjects))
{
return $this->app->control->sendError($this->lang->pivot->accessDenied, helper::createLink('pivot', $method));
}
}
/**
* 过滤不可见的透视表。
* Filter invisible pivot.
*
* @param array $pivots
* @access public
* @return array
*/
public function filterInvisiblePivot($pivots)
{
$viewableObjects = $this->bi->getViewableObject('pivot');
foreach($pivots as $index => $pivot)
{
if(!in_array($pivot->id, $viewableObjects)) unset($pivots[$index]);
}
return array_values($pivots);
}
/*
* 获取透视表数据。
* Get pivot data by id.
*
* @param int $id
* @access public
* @return object|bool
*/
public function getPivotDataByID($id)
{
$pivot = $this->pivotTao->fetchPivot($id);
if(!$pivot) return false;
return $pivot;
}
/*
* 获取透视表。
* Get pivot.
*
* @param int $pivotID
* @param bool $processDateVar
* @param string $filterStatus
* @param bool $addDrills
* @access public
* @return object|bool
*/
public function getByID(int $pivotID, bool $processDateVar = false, string $filterStatus = 'published', bool $addDrills = true): object|bool
{
$pivot = $this->pivotTao->fetchPivot($pivotID);
if(!$pivot) return false;
$pivot->fieldSettings = array();
if(!empty($pivot->fields) && $pivot->fields != 'null')
{
$pivot->fieldSettings = json_decode($pivot->fields);
$pivot->fields = array_keys(get_object_vars($pivot->fieldSettings));
}
if(!empty($pivot->filters))
{
$filters = json_decode($pivot->filters, true);
$pivot->filters = $this->setFilterDefault($filters, $processDateVar);
}
else
{
$pivot->filters = array();
}
$this->completePivot($pivot);
if($addDrills) $this->addDrills($pivot);
// if(isset($pivot->stage) && $pivot->stage == 'published' && $this->app->methodName == 'preview') $this->processFieldSettings($pivot);
return $pivot;
}
/*
* 获取透视表某版本。
* Get pivot by id and version.
*
* @param int $pivotID
* @param string $version
* @param bool $processDateVar
* @param bool $addDrills
* @access public
* @return object|bool
*/
public function getPivotSpec(int $pivotID, string $version, bool $processDateVar = false, bool $addDrills = true)
{
$pivot = $this->pivotTao->fetchPivot($pivotID, $version);
if(!$pivot) return false;
$pivot->fieldSettings = array();
if(!empty($pivot->fields) && $pivot->fields != 'null')
{
$pivot->fieldSettings = json_decode($pivot->fields);
$pivot->fields = array_keys(get_object_vars($pivot->fieldSettings));
}
if(!empty($pivot->filters))
{
$filters = json_decode($pivot->filters, true);
$pivot->filters = $this->setFilterDefault($filters, $processDateVar);
}
else
{
$pivot->filters = array();
}
$this->completePivot($pivot);
if($addDrills) $this->addDrills($pivot);
return $pivot;
}
/**
* 时间占位符替换为实际的时间。
* Replace time placeholder with actual time.
*
* @param mixed $var
* @param string $type
* @access public
* @return string
*/
public function processDateVar(mixed $var, string $type = 'date'): string
{
if(!is_string($var) || $var === '') return '';
$format = $type == 'datetime' ? 'Y-m-d H:i:s' : 'Y-m-d';
switch($var)
{
case '$MONDAY': $var = date($format, time() - (date('N') - 1) * 24 * 3600); break;
case '$SUNDAY': $var = date($format, time() + (7 - date('N')) * 24 * 3600); break;
case '$MONTHBEGIN': $var = date($format, time() - (date('j') - 1) * 24 * 3600); break;
case '$MONTHEND': $var = date($format, time() + (date('t') - date('j')) * 24 * 3600); break;
}
return $var;
}
/**
* 构建透视表的信息。
* Process pivot information.
*
* @param object|array $pivots
* @param bool $isObject
* @access public
* @return object|array
*/
public function processPivot(object|array $pivots, bool $isObject = true): object|array
{
if($isObject) $pivots = array($pivots);
foreach($pivots as $pivot)
{
$this->completePivot($pivot);
if($isObject) $this->addDrills($pivot);
}
return $isObject ? $pivot : $pivots;
}
/**
* Process name and desc of pivot.
*
* @param object $pivot
* @access private
* @return void
*/
public function processNameDesc(object $pivot): void
{
if(!empty($pivot->type)) return;
$pivot->names = array('zh-cn' => '', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => '');
$pivot->descs = array('zh-cn' => '', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => '');
$clientLang = $this->app->getClientLang();
if(!empty($pivot->name))
{
$pivot->names = json_decode($pivot->name, true);
$langNames = empty($pivot->names) ? array() : array_filter($pivot->names);
$firstName = empty($langNames) ? '' : reset($langNames);
$clientLangName = zget($pivot->names, $clientLang, '');
$pivot->name = empty($clientLangName) ? $firstName : $clientLangName;
}
if(!empty($pivot->desc))
{
$pivot->descs = json_decode($pivot->desc, true);
$langDescs = empty($pivot->descs) ? array() : array_filter($pivot->descs);
$firstDesc = empty($langDescs) ? '' : reset($langDescs);
$clientLangDesc = zget($pivot->descs, $clientLang, '');
$pivot->desc = empty($clientLangDesc) ? $firstDesc : $clientLangDesc;
}
}
/**
* 完善透视表。
* Complete pivot.
*
* @param object $pivot
* @access public
* @return void
*/
private function completePivot(object $pivot): void
{
if(!empty($pivot->settings)) $pivot->settings = json_decode($pivot->settings, true);
$this->processNameDesc($pivot);
}
/**
* 添加下钻信息到透视表。
* Add drills to pivot.
*
* @param object $pivot
* @access public
* @return void
*/
public function addDrills(object $pivot): void
{
$settings = $pivot->settings;
if(!is_array($settings) || !isset($settings['columns'])) return;
$columns = $settings['columns'];
$drillFields = array_column($columns, 'field');
$drills = $this->pivotTao->fetchPivotDrills($pivot->id, $pivot->version, $drillFields);
foreach($columns as $index => $column) $pivot->settings['columns'][$index]['drill'] = zget($drills, $column['field']);
}
/**
* 检测图表是否在使用。
* Check if the Chart is in use.
*
* @param int $chartID
* @param string $type
* @access public
* @return bool
*/
public function checkIFChartInUse(int $chartID, string $type = 'chart', array $screens = array()): bool
{
static $screenList = array();
if($screens) $screenList = $screens;
if(empty($screenList)) $screenList = $this->dao->select('scheme')->from(TABLE_SCREEN)->where('deleted')->eq(0)->andWhere('status')->eq('published')->fetchAll();
foreach($screenList as $screen)
{
$scheme = json_decode($screen->scheme);
if(empty($scheme->componentList)) continue;
foreach($scheme->componentList as $component)
{
$list = !empty($component->isGroup) ? $component->groupList : array($component);
foreach($list as $groupComponent)
{
if(!isset($groupComponent->chartConfig)) continue;
$sourceID = zget($groupComponent->chartConfig, 'sourceID', '');
$sourceType = zget($groupComponent->chartConfig, 'package', '') == 'Tables' ? 'pivot' : 'chart';
if($chartID == $sourceID && $type == $sourceType) return true;
}
}
}
return false;
}
/**
* 构建透视表字段的配置信息类似于dataview/js/basequery.js getFieldSettings()。
* Process pivot field settings, function like dataview/js/basequery.js getFieldSettings().
*
* @param object $pivot
* @access public
* @return void
*/
public function processFieldSettings(object $pivot): void
{
$this->loadModel('dataview');
$fieldSettings = $pivot->fieldSettings;
if(empty($fieldSettings)) return;
$sql = isset($pivot->sql) ? $pivot->sql : '';
$filters = $this->getFieldsFromPivot($pivot, 'filters', array(), !is_array($pivot->filters), true);
if(!empty($filters)) $filters = $this->setFilterDefault($filters);
/* 检测sql是否有效。 */
/* Check if the sql is valid. */
$querySQL = $this->loadModel('bi')->parseSqlVars($sql, $filters);
$this->dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
$stmt = $this->dbh->query($querySQL);
if(!$stmt) return;
$columns = $this->bi->getColumnsType($querySQL);
$columnFields = array();
foreach(array_keys(get_object_vars($columns)) as $type) $columnFields[$type] = $type;
extract($this->bi->getTableAndFields($querySQL));
/* 获取field的键值对以及相关联的对象。 */
/* Get field key value pairs and related objects. */
$this->loadModel('dataview');
if($tables)
{
$statement = $this->bi->sql2Statement($sql);
$moduleNames = $this->dataview->getModuleNames($tables);
$aliasNames = $this->dataview->getAliasNames($statement, $moduleNames);
}
list($fieldPairs, $relatedObjects) = $this->dataview->mergeFields($columnFields, $fields, $moduleNames, $aliasNames);
$objectFields = $this->loadModel('dataview')->getObjectFields();
/* 重建fieldSettings字段。 */
/* Rebuild fieldSettings field. */
$pivot->fieldSettings = $this->bi->rebuildFieldSettings($fieldPairs, $columns, $relatedObjects, $fieldSettings, $objectFields);
}
/**
* 获取执行。
* Get executions.
*
* @param string $begin
* @param string $end
* @access public
* @return array
*/
public function getExecutions(string $begin = '', string $end = ''): array
{
$permission = common::hasPriv('pivot', 'showProject') || $this->app->user->admin;
$IDList = !$permission ? $this->app->user->view->sprints : array();
$executions = $this->pivotTao->getExecutionList($begin, $end, $IDList);
foreach($executions as $execution)
{
$execution->deviation = round($execution->consumed - $execution->estimate, 2);
$execution->deviationRate = $execution->estimate ? round($execution->deviation / $execution->estimate * 100, 2) : 'n/a';
}
return $executions;
}
/**
* 获取产品。
* Get products.
*
* @param string $conditions
* @param string $storyType
* @param array $filters
* @access public
* @return array
*/
public function getProducts(string $conditions, string $storyType = 'story', array $filters = array()): array
{
$permission = common::hasPriv('pivot', 'showProduct') || $this->app->user->admin;
$IDList = !$permission ? $this->app->user->view->products : array();
$products = $this->pivotTao->getProductList($conditions, $IDList, $filters);
/* 为产品生成计划数据和相关的需求数据。 */
/* Generate plan data and related story data for products. */
$this->pivotTao->processPlanStories($products, $storyType, $this->pivotTao->processProductPlan($products, $conditions));
unset($products['']);
return $products;
}
/**
* 获取Bug创建表的数据。
* Get bug related pivot information.
*
* @param string $begin
* @param string $end
* @param int $product
* @param int $execution
* @access public
* @return array
*/
public function getBugs(string $begin, string $end, int $product = 0, int $execution = 0): array
{
$end = date('Y-m-d', strtotime("{$end} +1 day"));
$bugGroups = $this->pivotTao->getBugGroup($begin, $end, $product, $execution);
/* 为bug生成统计数据。 */
/* Generate statistics data for bugs. */
$bugs = array();
foreach($bugGroups as $account => $userBugs)
{
$bug = array();
$bug['openedBy'] = $account;
$bug['unResolved'] = 0;
$bug['validRate'] = 0;
$bug['total'] = 0;
/* Bug已解决状态数据初始化。 */
/* Bug resolved status data initialization. */
foreach(array_keys($this->lang->bug->resolutionList) as $resolution)
{
if($resolution) $bug[$resolution] = 0;
}
/* 获取bug各个状态的统计数据。 */
/* Get statistics data for each status of bugs. */
$resolvedCount = 0;
$validCount = 0;
foreach($userBugs as $userBug)
{
if(!isset($bug[$userBug->resolution])) continue;
$bug[$userBug->resolution]++;
$bug['total']++;
if($userBug->status == 'resolved' || $userBug->status == 'closed') $resolvedCount++;
if($userBug->resolution == 'fixed' || $userBug->resolution == 'postponed') $validCount++;
}
if(!$bug['total']) continue;
/* 获取有效率。*/
/* Get valid rate. */
$bug['validRate'] = $resolvedCount ? round($validCount / $resolvedCount * 100, 2) . '%' : '0%';
$bugs[] = $bug;
}
uasort($bugs, 'sortSummary');
return $bugs;
}
/**
* 获取员工负载表的数据。
* Get workload.
*
* @param int $dept
* @param string $assign assign|noassign
* @param array $users
* @param float $allHour
* @access public
* @return array
*/
public function getWorkload(int $dept, string $assign, array $users, float $allHour): array
{
/* 判断是否需要查询部门用户。 */
/* Check if need to query department users. */
$deptUsers = array();
if($dept)
{
$deptUsers = $this->loadModel('dept')->getDeptUserPairs($dept);
if(!$deptUsers) return array();
}
$canViewExecution = common::hasPriv('execution', 'view');
return $assign == 'noassign' ? $this->getWorkloadNoAssign($deptUsers, $users, $canViewExecution) : $this->getWorkLoadAssign($deptUsers, $users, $canViewExecution, $allHour);
}
/**
* 获取未指派的员工负载表。
* Get workload no assign.
*
* @param array $deptUsers
* @param array $users
* @param bool $canViewExecution
* @access public
* @return array
*/
public function getWorkloadNoAssign(array $deptUsers, array $users, bool $canViewExecution): array
{
$executions = $this->pivotTao->getNoAssignExecution(array_keys($deptUsers));
if(empty($executions)) return array();
/* 构建用户-项目-执行数据结构。 */
/* Build user-project-execution data structure. */
$executionGroups = array();
foreach($executions as $execution)
{
if(!isset($users[$execution->user])) continue;
$executionGroups[$execution->user][$execution->projectID][$execution->executionID] = $execution;
}
/* 计算未指派的执行统计数据。 */
/* Calculate statistics data for no assign execution. */
$workload = array();
foreach($executionGroups as $account => $projects)
{
if(!isset($users[$account])) continue;
$totalExecutions = 0;
foreach($projects as $executions) $totalExecutions += count($executions);
$userFirstRow = true;
foreach($projects as $executions)
{
$projectFirstRow = true;
foreach($executions as $execution)
{
$execution->executionTasks = 0;
$execution->executionHours = 0;
$execution->totalTasks = 0;
$execution->totalHours = 0;
$execution->workload = '0%';
$this->setExecutionName($execution, $canViewExecution);
if($userFirstRow) $execution->userRowspan = $totalExecutions;
if($projectFirstRow) $execution->projectRowspan = count($executions);
$workload[] = $execution;
$userFirstRow = false;
$projectFirstRow = false;
}
}
}
return $workload;
}
/**
* 获取指派的员工负载表。
* Get workload assign.
*
* @param array $deptUsers
* @param array $users
* @param bool $canViewExecution
* @param float $allHour
* @access public
* @return array
*/
public function getWorkLoadAssign(array $deptUsers, array $users, bool $canViewExecution, float $allHour): array
{
$tasks = $this->pivotTao->getAssignTask(array_keys($deptUsers));
if(empty($tasks)) return array();
/* 构建用户-项目-执行-任务数据结构。 */
/* Build user-project-execution-task data structure. */
$taskGroups = array();
foreach($tasks as $task)
{
if(!isset($users[$task->user])) continue;
$user = $task->user;
$project = $task->projectID;
$execution = $task->executionID;
$id = $task->id;
if(isset($taskGroups[$user][$project][$execution][$id]))
{
$taskGroups[$user][$project][$execution][$id]->left = round($taskGroups[$user][$project][$execution][$id]->left + $task->left, 2);
}
else
{
$taskGroups[$user][$project][$execution][$id] = $task;
}
}
/* 获取团队任务的剩余工时。 */
/* Get team task left hours. */
$teamTasks = $this->pivotTao->getTeamTasks(array_keys($deptUsers));
$workload = array();
foreach($taskGroups as $projects)
{
list($totalTasks, $totalHours, $totalExecutions, $userWorkload) = $this->getUserWorkLoad($projects, $teamTasks, $allHour);
/* 计算用户的执行统计数据。 */
/* Calculate user's execution statistics data. */
$userFirstRow = true;
foreach($projects as $executions)
{
$projectFirstRow = true;
foreach($executions as $tasks)
{
$execution = current($tasks);
$execution->executionTasks = count($tasks);
$execution->executionHours = array_sum(array_map(function($task){return $task->isParent ? 0 : $task->left;}, $tasks));
$execution->totalTasks = $totalTasks;
$execution->totalHours = $totalHours;
$execution->workload = $userWorkload;
$this->setExecutionName($execution, $canViewExecution);
if($userFirstRow) $execution->userRowspan = $totalExecutions;
if($projectFirstRow) $execution->projectRowspan = count($executions);
$workload[] = $execution;
$userFirstRow = $projectFirstRow = false;
}
}
}
return $workload;
}
/**
* 设置执行名称。
* Set execution name.
*
* @param object $execution
* @param bool $canViewExecution
* @access public
* @return void
*/
public function setExecutionName(object $execution, bool $canViewExecution): void
{
if($execution->multiple)
{
$execution->executionName = $canViewExecution ? html::a(helper::createLink('execution', 'view', "executionID={$execution->executionID}"), $execution->executionName, '', "title='{$execution->executionName}'") : "<span title='{$execution->executionName}'>{$execution->executionName}</span>";
}
else
{
$execution->executionName = $this->lang->null;
}
}
/**
* 获取用户的工作负载相关信息。
* Get user's workload related information.
*
* @param array $projects
* @param array $teamTasks
* @param float $allHour
* @access private
* @return array
*/
public function getUserWorkLoad(array $projects, array $teamTasks, float $allHour): array
{
/* 计算员工的任务数,剩余工时和总任务数。 */
/* Calculate user's task count, left hours and total task count. */
$totalTasks = $totalHours = $totalExecutions = 0;
foreach($projects as $executions)
{
$totalExecutions += count($executions);
foreach($executions as $tasks)
{
$totalTasks += count($tasks);
foreach($tasks as $task)
{
if($task->isParent) continue;
if(isset($teamTasks[$task->id])) $task->left = $teamTasks[$task->id]->left;
$totalHours = round($totalHours + $task->left, 2);
}
}
}
/* 计算员工的工作负载。 */
/* Calculate user's workload. */
$userWorkload = $allHour ? round($totalHours / $allHour * 100, 2) . '%' : '0%';
return array($totalTasks, $totalHours, $totalExecutions, $userWorkload);
}
/**
* 获取未解决Bug指派表相关数据。
* Get bug assign.
*
* @access public
* @return array
*/
public function getBugAssign(): array
{
$bugGroups = $this->pivotTao->getAssignBugGroup();
$products = $this->pivotTao->getAllProductsIDAndName();
$productProjects = $this->pivotTao->getProductProjects();
$canViewProduct = common::hasPriv('product', 'view');
$canViewProject = common::hasPriv('project', 'view');
$bugs = array();
foreach($bugGroups as $userBugs)
{
$totalBugs = array_sum(array_map(function($bug){return $bug->bugCount;}, $userBugs));
$first = true;
foreach($userBugs as $bug)
{
if(!isset($products[$bug->product])) continue;
$bug->productName = $products[$bug->product];
if($bug->productName)
{
/* 用户有访问权限的情况下,允许用户点击。 */
/* Allow users to click if they have access. */
if($canViewProject && !empty($productProjects[$bug->product]))
{
$bug->productName = html::a(helper::createLink('project', 'view', "projectID={$productProjects[$bug->product]}"), $bug->productName);
}
elseif($canViewProduct)
{
$bug->productName = html::a(helper::createLink('product', 'view', "product={$bug->product}"), $bug->productName);
}
}
$bug->total = $totalBugs;
if($first) $bug->rowspan = count($userBugs);
$bugs[] = $bug;
$first = false;
}
}
return $bugs;
}
/**
* 获取执行的下拉菜单相关数据。
* Get execution dropdown menu related data.
*
* @access public
* @return array
*/
public function getProjectExecutions(): array
{
$executions = $this->pivotTao->getProjectAndExecutionNameQuery();
$pairs = array();
foreach($executions as $execution)
{
if($execution->multiple) $pairs[$execution->id] = $execution->projectname . '/' . $execution->name;
if(!$execution->multiple) $pairs[$execution->id] = $execution->projectname;
}
return $pairs;
}
/**
* 格式化sql和过滤条件。
* Format sql and filter.
*
* @param string $sql
* @param array $filters
* @access public
* @return array
*/
public function getFilterFormat(string $sql, array $filters): array
{
if(empty($filters)) return array($sql, false);
$filters = $this->processQueryFilterDefaults($filters);
$currentFilter = current($filters);
$isQueryFilter = (isset($currentFilter['from']) && $currentFilter['from'] == 'query');
$filterFormat = $isQueryFilter ? array_values($filters) : array();
foreach($filters as $filter)
{
$field = $filter['field'];
if($isQueryFilter)
{
$queryDefault = '';
if(isset($filter['default']))
{
$queryDefault = $filter['default'];
if($filter['type'] == 'date' || $filter['type'] == 'datetime') $queryDefault = $this->processDateVar($filter['default']);
if($filter['type'] == 'datetime') $queryDefault .= ':00.000000000';
if($filter['type'] == 'multipleselect' && is_array($filter['default'])) $queryDefault = implode("','", $filter['default']);
}
if(strpos($sql, $filter['field'] . 'Condition') === false)
{
$sql = str_replace('$' . $filter['field'], "'{$queryDefault}'", $sql);
}
else
{
$sql = str_replace('$' . $filter['field'] . 'Condition', "{$filter['relatedField']}='{$queryDefault}'", $sql);
}
}
else
{
if(!isset($filter['default'])) continue;
$default = $filter['default'];
switch($filter['type'])
{
case 'select':
if(is_string($default) && strpos($default, ',') !== false) $default = explode(',', $default);
if(is_array($default)) $default = implode("', '", array_filter($default, function($val){return trim($val) != '';}));
if(empty($default)) break;
$value = "('" . $default . "')";
$filterFormat[$field] = array('operator' => 'IN', 'value' => $value);
break;
case 'input':
$filterFormat[$field] = array('operator' => 'LIKE', 'value' => "'%$default%'");
break;
case 'date':
case 'datetime':
$begin = $default['begin'];
$end = $default['end'];
if(!empty($begin)) $begin = date('Y-m-d 00:00:00', strtotime($begin));
if(!empty($end)) $end = date('Y-m-d 23:59:59', strtotime($end));
if(!empty($begin) && empty($end)) $filterFormat[$field] = array('operator' => '>=', 'value' => "'{$begin}'");
if( empty($begin) && !empty($end)) $filterFormat[$field] = array('operator' => '<=', 'value' => "'{$end}'");
if(!empty($begin) && !empty($end)) $filterFormat[$field] = array('operator' => 'BETWEEN', 'value' => "'{$begin}' AND '{$end}'");
break;
}
}
}
return array($sql, $filterFormat);
}
/**
* Get groups from settings.
*
* @param array $settings
* @access public
* @return array
*/
public function getGroupsFromSettings(array $settings): array
{
$groups = array();
foreach($settings as $key => $value)
{
if(strpos($key, 'group') !== false && $value) $groups[] = $value;
}
return array_unique($groups);
}
/**
* Trim semicolon of sql.
*
* @param string $sql
* @access public
* @return string
*/
public function trimSemicolon(string $sql): string
{
return trim($sql, " ;");
}
/**
* Append where filter to sql from filters.
*
* @param string $sql
* @param array|false $filters
* @param array $driver
* @access public
* @return string
*/
public function appendWhereFilterToSql(string $sql, array|false $filters, string $driver): string
{
$connectSQL = '';
if(!isset($filters[0]['from']) && $filters !== false)
{
if(!empty($filters))
{
$wheres = array();
foreach($filters as $field => $filter)
{
$fieldSQL = $this->getFilterFieldSQL($filter, $field, $driver);
if($filter['operator'] == 'LIKE' && $filter['value'] == "'%%'")
{
$wheres[] = "1=1"; // if empty filter then ignore this filter.
}
else
{
$wheres[] = "$fieldSQL {$filter['operator']} {$filter['value']}";
}
}
$whereStr = implode(' and ', $wheres);
$connectSQL .= " where $whereStr";
}
else
{
$connectSQL .= " where 1=0";
}
}
if($connectSQL) $sql = "select * from ( $sql ) tt" . $connectSQL;
return $sql;
}
public function getFilterFieldSQL(array $filter, string $field, string $driver)
{
$fieldSql = "tt.`{$field}`";
if($driver == 'duckdb')
{
$type = $filter['type'];
if($type == 'input')
{
$fieldSql = " cast($fieldSql as varchar) ";
}
}
return $fieldSql;
}
/**
* Filter fields with settings.
*
* @param array $fields
* @param array $groups
* @param array $columns
* @access public
* @return array
*/
public function filterFieldsWithSettings(array $fields, array $groups, array $columns): array
{
$filteredFields = array();
$settingFields = $groups;
foreach($columns as $column)
{
$slice = zget($column, 'slice', 'noSlice');
$settingFields[] = $column['field'];
if($slice != 'noSlice') $settingFields[] = $slice;
}
$settingFields = array_unique($settingFields);
foreach($settingFields as $field)
{
if(!isset($filteredFields[$field]) && isset($fields[$field])) $filteredFields[$field] = $fields[$field];
}
return $filteredFields;
}
/**
* Map record value with field options.
*
* @param array $records
* @param array $fields
* @access public
* @return array
*/
public function mapRecordValueWithFieldOptions(array $records, array $fields, string $driver): array
{
$this->app->loadConfig('dataview');
$fieldOptions = $this->getFieldsOptions($fields, $records, $driver);
$records = json_decode(json_encode($records), true);
foreach($records as $index => $record)
{
foreach($record as $field => $value)
{
if(!isset($fields[$field])) continue;
$value = is_string($value) ? str_replace('"', '', htmlspecialchars_decode($value)) : $value;
$record["{$field}_origin"] = $value;
$tableField = !isset($fields[$field]) ? '' : $fields[$field]['object'] . '-' . $fields[$field]['field'];
$withComma = in_array($tableField, $this->config->dataview->multipleMappingFields);
$optionList = isset($fieldOptions[$field]) ? $fieldOptions[$field] : array();
if($withComma)
{
$valueArr = array_filter(explode(',', $value));
$resultArr = array();
foreach($valueArr as $val)
{
$resultArr[] = isset($optionList[$val]) ? $optionList[$val] : $val;
}
$record[$field] = implode(',', $resultArr);
}
else
{
$valueKey = "$value";
$record[$field] = isset($optionList[$valueKey]) ? $optionList[$valueKey] : $value;
}
$record[$field] = is_string($record[$field]) ? str_replace('"', '', htmlspecialchars_decode($record[$field])) : $record[$field];
}
$records[$index] = (object)$record;
}
return $records;
}
/**
* Genereate table cols config.
*
* @param array $fields
* @param array $groups
* @param array $langs
* @access public
* @return array
*/
public function generateTableCols(array $fields, array $groups, array $langs): array
{
$cols = array();
$clientLang = $this->app->getClientLang();
/* Build cols. */
foreach($groups as $group)
{
$fieldObject = $fields[$group]['object'];
$relatedField = $fields[$group]['field'];
$col = new stdclass();
$col->name = $group;
$col->field = $relatedField;
$col->isGroup = true;
$colLabel = $group;
if($fieldObject)
{
$this->app->loadLang($fieldObject);
if(isset($this->lang->$fieldObject->$relatedField)) $colLabel = $this->lang->$fieldObject->$relatedField;
if($this->config->edition != 'open')
{
$workflowFields = $this->loadModel('workflowfield')->getFieldPairs($fieldObject);
if(isset($workflowFields[$relatedField])) $colLabel = $workflowFields[$relatedField];
}
}
if(isset($langs[$group]) && !empty($langs[$group][$clientLang])) $colLabel = $langs[$group][$clientLang];
$col->label = $colLabel;
$cols[0][] = $col;
}
return $cols;
}
/**
* Get show col position.
*
* @param array $settings
* @access public
* @return string noShow | bottom | row | all
*/
public function getShowColPosition(array|object $settings): string
{
$columnTotal = zget($settings, 'columnTotal', 'noShow');
$columnPosition = zget($settings, 'columnPosition', 'bottom');
if($columnTotal == 'noShow') return 'noShow';
return $columnPosition;
}
/**
* Check whether showColPosition should show last row.
*
* @param string $showColPosition
* @access public
* @return bool
*/
public function isShowLastRow(string $showColPosition): bool
{
return in_array($showColPosition, array('bottom', 'all'));
}
/**
* 计算列的统计值。
* Calculate column statistics.
*
* @param array $records
* @param string $statistic
* @param string $field
* @access public
* @return mixed
*/
public function columnStatistics(array $records, string $statistic, string $field): mixed
{
$values = array_column($records, $field);
$numericValues = array_map(function($value)
{
return is_numeric($value) ? floatval($value) : 0;
}, $values);
if($statistic == 'count') return count($numericValues);
if($statistic == 'sum') return round(array_sum($numericValues), 2);
if($statistic == 'avg') return round(array_sum($numericValues) / count($numericValues), 2);
if($statistic == 'min') return min($numericValues);
if($statistic == 'max') return max($numericValues);
if($statistic == 'distinct') return count(array_unique($values));
}
/**
* 行数据转树。
* Convert row data to tree.
*
* @param array $data
* @access public
* @return array|string
*/
public function getGroupTreeWithKey(array $data): array|string
{
$first = reset($data);
if(!isset($first['groups'])) return $first['groupKey'];
$tree = array();
foreach($data as $value)
{
$groups = $value['groups'];
$parentKey = array_shift($groups);
if(!isset($tree[$parentKey])) $tree[$parentKey] = array();
$value['groups'] = $groups;
if(count($groups) == 0) unset($value['groups']);
$tree[$parentKey][] = $value;
}
foreach($tree as $key => $value) $tree[$key] = $this->getGroupTreeWithKey($value);
return $tree;
}
/**
* 获取单元格数据。
* Get cell data.
*
* @param string $key
* @param array $data
* @access public
* @return array
*/
public function formatCellData(string $key, array $data): array
{
if(!isset($data[$key])) return array();
$cellData = $data[$key];
foreach($cellData as $colKey => $colValue)
{
if(is_scalar($colValue))
{
$cellData[$colKey] = array('value' => $colValue);
}
else
{
$value = $colValue['value'];
$colValue['value'] = is_scalar($value) ? $value : '/';
$cellData[$colKey] = $colValue;
}
}
return $cellData;
}
/**
* 计算列的总计值。
* Calculate column total.
*
* @param array $data
* @access public
* @return array
*/
public function getColumnSummary(array $data, string $totalKey): array
{
$summary = array();
foreach($data as $columns)
{
foreach($columns as $colKey => $colValue)
{
if(!isset($summary[$colKey]))
{
$summary[$colKey] = $colValue;
}
else
{
$isGroup = zget($colValue, 'isGroup', 1);
$value = zget($colValue, 'value', '');
$isNumeric = is_numeric($value);
$summary[$colKey]['value'] = !$isGroup && $isNumeric ? $summary[$colKey]['value'] + $value : $value;
}
}
}
$summary[$totalKey] = array('value' => '$total$');
/* 删除汇总行的下钻配置。*/
/* Delete drilldown config of summary row. */
foreach($summary as $key => $value)
{
if(isset($value['value']) && is_numeric($value['value'])) $summary[$key]['value'] = round($summary[$key]['value'], 2);
if(isset($value['drillFields']))
{
unset($summary[$key]['drillFields']);
}
}
return $summary;
}
/**
* 添加行总计到树数据中。
* Add row summary to tree data.
*
* @param array $groupTree
* @param array $data
* @param array $groups
* @param int $currentGroup
* @access public
* @return array
*/
public function addRowSummary(array $groupTree, array $data, array $groups, int $currentGroup = 0): array
{
$first = reset($groupTree);
if(is_scalar($first))
{
$groupData = array();
$rows = array();
foreach($groupTree as $groupKey)
{
$groupData[$groupKey] = $this->formatCellData($groupKey, $data);
$rows[$groupKey] = $data[$groupKey];
}
return array('rows' => $rows, 'summary' => $this->getColumnSummary($groupData, $groups[$currentGroup]));
}
$rows = array();
foreach($groupTree as $key => $children) $rows[$key] = $this->addRowSummary($children, $data, $groups, $currentGroup + 1);
$groupData = array_column($rows, 'summary');
return array('rows' => $rows, 'summary' => $this->getColumnSummary($groupData, $groups[$currentGroup]));
}
/**
* 去除数据中的额外信息,只保留单元格数据。
* Remove extra info from data, only keep cell data.
*
* @param array $records
* @access public
* @return array
*/
public function pureCrystalData(array $records): array
{
$pureData = array();
foreach($records as $key => $record)
{
$columns = $record['columns'];
$groups = $record['groups'];
$pureData[$key] = $groups;
foreach($columns as $colKey => $colValue)
{
$cellData = $colValue['cellData'];
if(isset($colValue['rowTotal'])) $cellData['total'] = $colValue['rowTotal'];
if(isset($cellData['value']))
{
$pureData[$key][$colKey] = $cellData;
}
else
{
foreach($cellData as $sliceKey => $sliceValue) $pureData[$key][$colKey . '_' . $sliceKey] = $sliceValue;
}
}
}
return $pureData;
}
/**
* 拍平切片列数据。
* Flatten slice column data.
*
* @param array $row
* @access public
* @return array
*/
public function flattenRow(array $row): array
{
$record = array();
foreach($row as $colKey => $cell)
{
if(is_scalar($cell))
{
$record[$colKey] = array('value' => $cell);
}
elseif(isset($cell['value']))
{
$record[$colKey] = $cell;
}
}
return $record;
}
/**
* 拍平透视表树结构数据。
* Flatten pivot table tree structure data.
*
* @param array $crystalData
* @param bool $withGroupSummary
* @access public
* @return array
*/
public function flattenCrystalData(array $crystalData, bool $withGroupSummary = false): array
{
$first = reset($crystalData);
if(!isset($first['rows']))
{
$records = array();
foreach($crystalData as $row) $records[] = $this->flattenRow($row);
return $records;
}
$records = array();
foreach($crystalData as $value)
{
$groupRecords = $this->flattenCrystalData($value['rows'], $withGroupSummary);
if($withGroupSummary && isset($value['summary'])) $groupRecords[] = $this->flattenRow($value['summary']);
$records = array_merge($records, $groupRecords);
}
return $records;
}
/**
* 处理行合并单元格。
* Process row span cell.
*
* @param array $records
* @param array $groups
* @access public
* @return array
*/
public function processRowSpan(array $records, array $groups): array
{
$lastGroupValue = array();
foreach($groups as $group) $lastGroupValue[$group] = '';
/* 定义内部函数:获取当前行数据的分组值。*/
/* Define internal function: get current row data's group value. */
$getGroupValue = function($record, $key, $index) use ($groups)
{
$value = array($record[$key]['value']);
$index -= 1;
while($index >= 0)
{
$value[] = $record[$groups[$index]]['value'];
$index -= 1;
}
return $value;
};
$groupsRowSpan = array();
foreach($records as $index => $record)
{
$rowSpan = 1;
foreach($record as $colKey => $cell)
{
if(!isset($cell['value']) || !is_array($cell['value'])) continue;
$rowSpan = max(count($cell['value']), $rowSpan);
}
foreach($record as $colKey => $cell)
{
$record[$colKey]['rowSpan'] = is_scalar($cell['value']) ? $rowSpan : 1;
}
$records[$index] = $record;
foreach($groups as $groupIndex => $group)
{
$groupValue = $getGroupValue($record, $group, $groupIndex);
$groupValueStr = implode('_', $groupValue);
if($groupValue[0] !== '$total$' && $groupValueStr === $lastGroupValue[$group] && isset($groupsRowSpan[$group]))
{
$groupRowSpan = array_pop($groupsRowSpan[$group]);
$groupRowSpan['index'][] = $index;
$groupRowSpan['rowSpan'] += $rowSpan;
$groupsRowSpan[$group][] = $groupRowSpan;
}
else
{
$groupsRowSpan[$group][] = array('index' => array($index), 'rowSpan' => $rowSpan);
}
$lastGroupValue[$group] = $groupValueStr;
}
}
foreach($groupsRowSpan as $group => $groupRowSpans)
{
foreach($groupRowSpans as $groupRowSpan)
{
$indexes = $groupRowSpan['index'];
foreach($indexes as $index)
{
$records[$index][$group]['rowSpan'] = $groupRowSpan['rowSpan'];
}
}
}
return $records;
}
/**
* 计算行汇总值。
* Calculate row total.
*
* @param array $row
* @access public
* @return array
*/
public function getRowTotal(array $row): array
{
$rowTotal = array();
foreach($row as $cell)
{
if(!isset($cell['percentage'])) continue;
list(,,,, $columnKey) = $cell['percentage'];
if(!isset($rowTotal[$columnKey])) $rowTotal[$columnKey] = 0;
$rowTotal[$columnKey] += $cell['value'];
}
return $rowTotal;
}
/**
* 计算百分比值。
* Calculate percentage.
*
* @param array $row
* @param array $rowTotal
* @param array $columnTotal
* @access public
* @return array
*/
public function setPercentage(array $row, array $rowTotal, array $columnTotal): array
{
foreach($row as $key => $cell)
{
if(!isset($cell['percentage'])) continue;
list(,,$showMode,, $columnKey) = $cell['percentage'];
if($showMode == 'row') $cell['percentage'][1] = $rowTotal[$columnKey];
if($showMode == 'column') $cell['percentage'][1] = $columnTotal[$key]['value'];
if($showMode == 'total')
{
$total = 0;
foreach($columnTotal as $column)
{
if(!isset($column['percentage'])) continue;
$percentage = $column['percentage'];
if($percentage[4] === $columnKey) $total += $column['value'];
}
$cell['percentage'][1] = $total;
}
$cell['percentage'][0] = $cell['value'];
$row[$key] = $cell;
}
return $row;
}
/**
* 处理百分比值。
* Process percentage.
*
* @param array $crystalData
* @param array $allSummary
* @access public
* @return array
*/
public function processPercentage(array $crystalData, array $allSummary): array
{
$rows = $crystalData['rows'];
$summary = $crystalData['summary'];
foreach($rows as $key => $row)
{
if(isset($row['rows']))
{
$rows[$key] = $this->processPercentage($row, $allSummary);
}
else
{
$rowTotal = $this->getRowTotal($row);
$rows[$key] = $this->setPercentage($row, $rowTotal, $allSummary);
}
}
$rowTotal = $this->getRowTotal($summary);
$summary = $this->setPercentage($summary, $rowTotal, $allSummary);
return array('rows' => $rows, 'summary' => $summary);
}
/**
* 对数据进行分组。
* Group records.
*
* @param array $records
* @param array $groups
* @access public
* @return array
*/
public function groupRecords(array $records, array $groups): array
{
$groupsData = array();
foreach($records as $record)
{
$key = $this->getGroupsKey($groups, $record);
if(!isset($groupsData[$key])) $groupsData[$key] = array();
$groupsData[$key][] = $record;
}
return $groupsData;
}
/**
* 设置切片列去重后的值。
* Set unique slices.
*
* @param array $records
* @param array $setting
* @access public
* @return array
*/
public function setUniqueSlices(array $records, array $setting): array
{
static $slices = array();
$slice = zget($setting, 'slice', 'noSlice');
if($slice == 'noSlice') return $setting;
if(isset($slices[$slice]))
{
$setting['uniqueSlices'] = $slices[$slice];
return $setting;
}
$uniqueSlices = array();
foreach($records as $record)
{
if(!isset($uniqueSlices[$record->$slice])) $uniqueSlices[$record->$slice] = $record;
}
$slices[$slice] = $uniqueSlices;
$setting['uniqueSlices'] = $uniqueSlices;
return $setting;
}
/**
* 根据字段的值过滤记录。
* Filter records by field value.
*
* @param array $records
* @param string $field
* @access public
* @return array
*/
public function getSliceRecords(array $records, string $field): array
{
$sliceRecords = array();
foreach($records as $record)
{
if(!isset($sliceRecords[$record->$field])) $sliceRecords[$record->$field] = array();
$sliceRecords[$record->$field][] = $record;
}
return $sliceRecords;
}
/**
* 计算单元格数据。
* Calculate cell data.
*
* @param string $columnKey
* @param array $records
* @param array $setting
* @access public
* @return array
*/
public function getCellData(string $columnKey, array $records, array $setting): array
{
$field = zget($setting, 'field', '');
$showOrigin = zget($setting, 'showOrigin', 0);
if($showOrigin) return array('value' => array_column($records, $field), 'isGroup' => false);
$stat = zget($setting, 'stat', 'count');
$slice = zget($setting, 'slice', 'noSlice');
$showMode = zget($setting, 'showMode', 'default');
$showTotal = zget($setting, 'showTotal', 'noShow');
$monopolize = zget($setting, 'monopolize', 0);
$isSlice = $slice != 'noSlice';
if(!$isSlice)
{
$value = $this->columnStatistics($records, $stat, $field);
$cell = array('value' => $value, 'isGroup' => false);
if($showMode == 'default') return $cell;
$cell['percentage'] = array($value, 1, $showMode, $monopolize, $columnKey);
return $cell;
}
/* 处理切片列的情况。 */
/* Handle the slice column situation. */
$uniqueSlices = zget($setting, 'uniqueSlices', array());
$cell = array();
$sliceRecords = $this->getSliceRecords($records, $slice);
foreach($uniqueSlices as $sliceRecord)
{
$sliceValue = $sliceRecord->$slice;
$sliceKey = "{$slice}_{$sliceValue}";
$value = $this->columnStatistics(zget($sliceRecords, $sliceValue, array()), $stat, $field);
$sliceCell = array('value' => $value, 'drillFields' => array($slice => $sliceRecord->{$slice . '_origin'}), 'isGroup' => false);
if($showMode != 'default') $sliceCell['percentage'] = array($value, 1, $showMode, $monopolize, $columnKey);
$cell[$sliceKey] = $sliceCell;
}
if($showTotal != 'noShow')
{
$value = array_sum(array_column($cell, 'value'));
$totalCell = array('value' => $value, 'isGroup' => false);
if($showMode != 'default') $totalCell['percentage'] = array($value, 1, $showMode, $monopolize, "rowTotal_{$columnKey}");
$cell['total'] = $totalCell;
}
return $cell;
}
/**
* 添加下钻字段信息。
* Add drill fields information.
*
* @param array $cell
* @param array $drillFields
* @access public
* @return array
*/
public function addDrillFields(array $cell, array $drillFields): array
{
if(isset($cell['value']))
{
if(!isset($cell['drillFields'])) $cell['drillFields'] = array();
$cell['drillFields'] = array_merge($cell['drillFields'], $drillFields);
return $cell;
}
foreach($cell as $sliceKey => $sliceCell)
{
if($sliceKey == 'total') continue;
$cell[$sliceKey] = $this->addDrillFields($sliceCell, $drillFields);
}
return $cell;
}
/**
* 根据列配置,计算透视表数据。
* Calculate pivot table data.
*
* @param array $groups
* @param array $records
* @param array $settings
* @access public
* @return array
*/
public function processCrystalData(array $groups,array $records, array $settings): array
{
$crystalData = array();
$columnSettings = $settings['columns'];
$groupRecords = $this->groupRecords($records, $groups);
foreach($groupRecords as $key => $data)
{
$record = reset($data);
$groupValues = array();
$groupOriginalValues = array();
foreach($groups as $group)
{
$groupValues[$group] = $record->$group;
$groupOriginalValues[$group] = $record->{$group . '_origin'};
}
$columns = array();
foreach($columnSettings as $colIndex => $setting)
{
$setting = $this->setUniqueSlices($records, $setting);
$field = zget($setting, 'field', '');
$columnKey = "{$field}{$colIndex}";
$cellData = $this->getCellData($columnKey, $data, $setting);
$cellData = $this->addDrillFields($cellData, $groupOriginalValues);
$columns[$columnKey] = array('setting' => $setting, 'cellData' => $cellData);
}
$crystalData[$key] = array('groups' => $groupValues, 'groupKey' => $key, 'columns' => $columns);
}
return $crystalData;
}
/**
* 处理透视表数据为可以显示的格式。
* Process pivot table data for display.
*
* @param array $records
* @access public
* @return array
*/
public function processRecordsForDisplay(array $records): array
{
$roundIfMoreThanTwoDecimals = function($number)
{
if(!is_numeric($number)) return $number;
$number = (float)$number;
if(floor($number) === $number) return $number;
$decimalPart = explode('.', strval($number));
if(isset($decimalPart[1]) && strlen($decimalPart[1]) > 2) return $number;
return $number;
};
$values = array();
foreach($records as $record)
{
$row = array();
$arrayValue = false;
foreach($record as $colKey => $cell)
{
$cellValue = $cell['value'] === '$total$' ? $this->lang->pivot->total : $cell['value'];
if(is_array($cellValue)) $arrayValue = $cellValue;
$cellValue = $roundIfMoreThanTwoDecimals($cellValue);
$row[$colKey] = $cellValue;
if(isset($cell['percentage']))
{
list($number, $total,, $monopolize) = $cell['percentage'];
if($monopolize) $colKey .= '_percentage';
if(!$total) $total = 100;
$row[$colKey] = round($number / $total * 100, 2) . '%';
}
}
if(is_array($arrayValue))
{
foreach(array_keys($arrayValue) as $index)
{
$flattenValue = array();
foreach($row as $key => $value)
{
$value = is_scalar($value) ? $value : $value[$index];
$flattenValue[$key] = $value;
}
$values[] = $flattenValue;
}
}
else
{
$values[] = $row;
}
}
return $values;
}
/**
* 获取合并单元格配置。
* Get row span config.
*
* @param array $records
* @access public
* @return array
*/
public function getRowSpanConfig(array $records): array
{
$configs = array();
foreach($records as $record)
{
$arrayValue = false;
foreach($record as $cell)
{
if(is_array($cell['value'])) $arrayValue = $cell['value'];
}
if(!is_array($arrayValue)) $arrayValue = array(1);
$configs = array_merge($configs, array_fill(0, count($arrayValue), array_column($record, 'rowSpan')));
}
return $configs;
}
/**
* 获取下钻字段配置。
* Get drill fields config.
*
* @param array $records
* @param array $groups
* @access public
* @return array
*/
public function getDrillsFromRecords(array $records, array $groups): array
{
$drills = array();
foreach($records as $record)
{
$groupKey = $this->getGroupsKey($groups, (object)$record);
if(!isset($drills[$groupKey])) $drills[$groupKey] = array('drillFields' => array());
foreach($record as $colKey => $cell)
{
if(isset($cell['drillFields'])) $drills[$groupKey]['drillFields'][$colKey] = $cell['drillFields'];
}
}
return $drills;
}
/**
* 处理查询过滤器的默认值。
* Process query filter defaults.
*
* @param array|false $filters
* @access public
* @return array
*/
public function processQueryFilterDefaults(array|false $filters): array|false
{
if(!is_array($filters)) return $filters;
$options = array();
foreach($filters as $index => $filter)
{
if(empty($filter['default'])) continue;
if(!isset($filter['from']) || $filter['from'] != 'query') continue;
if($filter['type'] !== 'multipleselect') continue;
$type = $filter['type'];
$typeOption = $filter['typeOption'];
if(strpos($type, 'select') !== false && !isset($options[$typeOption])) $options[$typeOption] = $this->getSysOptions($typeOption);
$filters[$index]['default'] = array_intersect($filter['default'], array_keys($options[$typeOption]));
}
return $filters;
}
/**
* Gen sheet.
*
* @param array $fields
* @param array $settings
* @param string $sql
* @param array|false $filters
* @param array $langs
* @access public
* @return array
*/
public function genSheet(array $fields, array $settings, string $sql, array|false $filters, array $langs = array(), string $driver = 'mysql'): array
{
$groups = $this->getGroupsFromSettings($settings);
$cols = $this->generateTableCols($fields, $groups, $langs);
$data = new stdclass();
$data->groups = $groups;
$data->cols = $cols;
$data->array = array();
$data->drills = array();
if(!isset($settings['columns'])) return array($data, array());
$filters = $this->processQueryFilterDefaults($filters);
/* Replace the variable with the default value. */
$sql = $this->bi->processVars($sql, (array)$filters);
$sql = $this->trimSemicolon($sql);
$sql = $this->appendWhereFilterToSql($sql, $filters, $driver);
$records = $this->bi->queryWithDriver($driver, $sql);
$settingFields = $this->filterFieldsWithSettings($fields, $groups, $settings['columns']);
$records = $this->mapRecordValueWithFieldOptions($records, $settingFields, $driver);
if(empty($records)) return array($data, array());
foreach($settings['columns'] as $columnSetting)
{
$cols = $this->getTableHeader($records, $columnSetting, $fields, $cols, $sql, $langs, $driver);
}
/* 根据列配置和分组配置,计算透视表数据。*/
/* Calculate crystal data based on column settings and group settings. */
$crystalData = $this->processCrystalData($groups, $records, $settings);
/* 将扁平的透视表数据转换成树形结构。*/
/* Convert flattened pivot table data to tree structure. */
$groupTree = $this->getGroupTreeWithKey($crystalData);
/* 净化处理透视表数据中的额外信息,只留下与单元格数据相关的信息。*/
/* Clean up the extra information in pivot table data. */
$crystalData = $this->pureCrystalData($crystalData);
/* 基于各级分组,计算每个分组的总计数据行。*/
/* Calculate total data rows based on each group. */
$crystalData = $this->addRowSummary($groupTree, $crystalData, $groups);
/* 计算百分比的值。*/
/* Calculate percentage values. */
$crystalData = $this->processPercentage($crystalData, $crystalData['summary']);
/* 将树形结构转换成扁平的透视表数据。*/
/* Convert tree structure to flattened pivot table data. */
$columnPosition = $this->getShowColPosition($settings);
$showGroupTotal = in_array($columnPosition, array('row', 'all'));
$showAllTotal = in_array($columnPosition, array('bottom', 'all'));
$records = $this->flattenCrystalData($crystalData['rows'], $showGroupTotal);
if($showAllTotal) $records[] = $this->flattenRow($crystalData['summary']);
/* 计算行合并单元格的配置。*/
/* Calculate row span config. */
$records = $this->processRowSpan($records, $groups);
$data->cols = $cols;
$data->array = $this->processRecordsForDisplay($records);
$data->drills = $this->getDrillsFromRecords($records, $groups);
$data->showAllTotal = $showAllTotal;
$configs = $this->getRowSpanConfig($records);
/* $data->groups array 代表分组,最多三个
* $data->cols array thead数据其中对象有三个属性name分组label列的名字isGroup标识是不是分组
* $data->arrays array tbody数据, 其中每一个数组内是一行td的数据
*
* $configs, eg: array(0 => array(0 => 2, 1 => 1), 2 => array(0 => 2))
* 代表在整个tbody中位于[0,0]坐标的td rowspan为2位于[0,1]坐标的td rowspan为1, 位于[2,0]坐标的td rowspan为2
*/
return array($data, $configs);
}
/**
* Check is filters all default empty.
*
* @param array $data
* @access public
* @return void
*/
public function isFiltersAllEmpty($filters)
{
return !empty($filters) && empty(array_filter(array_column($filters, 'default')));
}
/**
* Gen sheet by origin sql.
*
* @param array $fields
* @param array $settings
* @param string $sql
* @param array|false $filters
* @param array $langs
* @access public
* @return string
*/
public function genOriginSheet($fields, $settings, $sql, $filters, $langs = array(), $driver = 'mysql')
{
$sql = $this->bi->processVars($sql, (array)$filters);
$sql = $this->trimSemicolon($sql);
$sql = $this->appendWhereFilterToSql($sql, $filters, $driver);
$dbh = $this->app->loadDriver($driver);
$rows = $dbh->query($sql)->fetchAll();
$rows = $this->filterSpecialChars($rows);
$fieldOptions = $this->getFieldsOptions($fields, $rows);
$rows = json_decode(json_encode($rows), true);
$cols = array();
$drills = zget($settings, 'drills', array());
/* Build cols. */
foreach($fields as $key => $field)
{
$col = new stdclass();
$col->name = $key;
$col->isGroup = true;
$col->label = $this->getColLabel($key, $fields, $langs);
if(isset($drills[$key]))
{
$col->isDrilling = true;
$col->condition = $drills[$key];
$col->drillField = $key;
}
$cols[0][] = $col;
}
$dataDrills = array();
$rowsAfterFields = array();
foreach($rows as $key => $row)
{
$drillFields = array();
$rowAfterFields = array();
foreach($row as $field => $value)
{
if(isset($drills[$field]))
{
$drillField = array();
foreach($drills[$field] as $condition)
{
$queryField = $condition['queryField'];
$drillField[$queryField] = $row[$queryField];
}
$drillFields[$field] = $drillField;
}
$optionList = isset($fieldOptions[$field]) ? $fieldOptions[$field] : array();
$rowAfterFields[$field] = isset($optionList[(string)$value]) ? $optionList[(string)$value] : $value;
}
$dataDrills[$key] = array('drillFields' => $drillFields);
$rowsAfterFields[$key] = $rowAfterFields;
}
$data = new stdclass();
$data->cols = $cols;
$data->array = $rowsAfterFields;
$data->drills = $dataDrills;
$configs = array_fill(0, count($rows), array_fill(0, count($fields), 1));
return array($data, $configs);
}
/**
* 初始化sql。
* Init sql.
*
* @param string $sql
* @param array $filters
* @param string $groupList
* @access public
* @return array
*/
public function initSql(string $sql, array $filters, string $groupList): array
{
$sql = str_replace(';', '', $this->initVarFilter($filters, $sql));
if(preg_match_all("/[\$]+[a-zA-Z0-9]+/", $sql, $out))
{
foreach($out[0] as $match) $sql = str_replace($match, "''", $sql);
}
$connectSQL = $this->getConnectSQL($filters);
$groupSQL = " group by {$groupList}";
$orderSQL = " order by {$groupList}";
return array($sql, $connectSQL, $groupSQL, $orderSQL);
}
/**
* 获取connectSQL。
* Get connectSQL.
*
* @param array $filters
* @access public
* @return string
*/
public function getConnectSQL(array $filters): string
{
$connectSQL = '';
if(!empty($filters) && !isset($filters[0]['from']))
{
$wheres = array();
foreach($filters as $field => $filter) $wheres[] = "tt.`{$field}` {$filter['operator']} {$filter['value']}";
$whereStr = implode(' and ', $wheres);
$connectSQL .= " where {$whereStr}";
}
return $connectSQL;
}
/**
* 通过过滤配置格式化sql。
* Init sql by filters.
*
* @param array $filters
* @param string $sql
* @access public
* @return string
*/
private function initVarFilter(array $filters = array(), string $sql = ''): string
{
if(empty($filters)) return $sql;
foreach($filters as $filter)
{
if(empty($filter['from'])) continue;
$default = isset($filter['default']) ? $filter['default'] : '';
if(is_array($default))
{
$default = array_filter($default, function($val){return !empty($val);});
$default = implode("', '", $default);
}
$sql = str_replace('$' . $filter['field'], "'{$default}'", $sql);
}
if(preg_match_all("/[\$]+[a-zA-Z0-9]+/", $sql, $out))
{
foreach($out[0] as $match) $sql = str_replace($match, "''", $sql);
}
return $sql;
}
/**
* 获取列名称。
* Get col label.
*
* @param string $key
* @param array $fields
* @param array $langs
* @access public
* @return string
*/
public function getColLabel(string $key, array $fields, array $langs): string
{
$clientLang = $this->app->getClientLang();
$fieldLang = zget($fields[$key], $clientLang, '');
if(!empty($fieldLang)) return $fieldLang;
if(isset($langs[$key]))
{
$lang = zget($langs[$key], $clientLang, '');
if(!empty($lang)) return $lang;
}
$object = zget($fields[$key], 'object', '');
if($object)
{
if($this->config->edition != 'open')
{
$workflowFields = $this->loadModel('workflowfield')->getFieldPairs($object);
if(isset($workflowFields[$key])) return $workflowFields[$key];
}
$this->app->loadLang($object);
if(isset($this->lang->{$object}->{$key})) return $this->lang->{$object}->{$key};
}
$name = zget($fields[$key], 'name', '');
if(!empty($name)) return $name;
return $key;
}
/**
* Get the header of the table.
*
* @param array $columnRows
* @param array $column
* @param array $fields
* @param array $cols
* @param array $langs
* @access public
* @return array
*/
public function getTableHeader($columnRows, $column, $fields, $cols, $sql, $langs = array(), $driver = 'mysql')
{
$stat = zget($column, 'stat', '');
$showMode = zget($column, 'showMode', 'default');
$monopolize = $showMode == 'default' ? '' : zget($column, 'monopolize', '');
$showOrigin = (bool)zget($column, 'showOrigin', 0);
$isDrilling = isset($column['drill']) && zget($column['drill'], 'condition', '');
$drillField = $isDrilling ? zget($column['drill'], 'field', '') : '';
$condition = $isDrilling ? zget($column['drill'], 'condition', '') : '';
$col = new stdclass();
$col->name = $column['field'];
$col->isGroup = false;
$col->showOrigin = $showOrigin;
$col->isDrilling = $isDrilling;
$col->drillField = $drillField;
$col->condition = $condition;
$fieldObject = $fields[$column['field']]['object'];
$relatedField = $fields[$column['field']]['field'];
$colLabel = $this->getColLabel($column['field'], $fields, $langs);
if(!$showOrigin)
{
$colLabel = str_replace('{$field}', $colLabel, $this->lang->pivot->colLabel);
$colLabel = str_replace('{$stat}', zget($this->lang->pivot->stepDesign->statList, $stat), $colLabel);
if($showMode != 'default') $colLabel .= sprintf($this->lang->pivot->colShowMode, zget($this->lang->pivot->stepDesign->showModeList, $showMode));
}
$col->label = $colLabel;
$slice = zget($column, 'slice', 'noSlice');
$col->isSlice = $slice != 'noSlice';
if($slice != 'noSlice' && !$showOrigin)
{
if(!isset($cols[1]))
{
foreach($cols[0] as $colData) $colData->rowspan = '2';
$cols[1] = array();
}
$sliceList = array();
foreach($columnRows as $rows) $sliceList[$rows->{$slice}] = $rows->{$slice};
$optionList = $this->getSysOptions($fields[$slice]['type'], $fields[$slice]['object'], $fields[$slice]['field'], $sql, '', $driver);
foreach($sliceList as $field)
{
$childCol = new stdclass();
$childCol->name = $field;
$childCol->isGroup = false;
$childCol->label = isset($optionList[$field]) ? $optionList[$field] : $field;
$childCol->label = empty($childCol->label) ? $this->lang->pivot->empty : $childCol->label;
$childCol->colspan = $monopolize ? 2 : 1;
$childCol->isDrilling = $isDrilling;
$childCol->drillField = $drillField;
$childCol->condition = $condition;
$cols[1][] = $childCol;
}
$col->colspan = count($sliceList);
if($monopolize) $col->colspan *= 2;
if(zget($column, 'showTotal', 'noShow') !== 'noShow')
{
$childCol = new stdclass();
$childCol->name = 'sum';
$childCol->isGroup = false;
$childCol->label = $this->lang->pivot->stepDesign->total;
$childCol->colspan = $monopolize ? 2 : 1;
$childCol->isDrilling = $isDrilling;
$childCol->drillField = $drillField;
$childCol->condition = $condition;
$cols[1][] = $childCol;
$col->colspan += $childCol->colspan;
}
$cols[0][] = $col;
}
else
{
$col->rowspan = !isset($cols[1]) ? '1' : '2';
$col->colspan = $monopolize ? 2 : 1;
$cols[0][] = $col;
}
return $cols;
}
/**
* Implode group keys of record.
*
* @param array $groups
* @param object $record
* @access public
* @return string
*/
public function getGroupsKey(array $groups, object $record): string
{
$groupsKey = array();
foreach($groups as $group) $groupsKey[] = is_scalar($record->$group) ? $record->$group : $record->$group['value'];
return implode('_', $groupsKey);
}
/**
* Get sys options.
*
* @param string $type
* @param string $object
* @param string $field
* @access public
* @return array
*/
public function getSysOptions($type, $object = '', $field = '', $source = '', $saveAs = '', $driver = 'mysql')
{
if(in_array($type, $this->config->pivot->scopeOptionList)) return $this->bi->getScopeOptions($type);
if(!$field) return array();
$options = array();
switch($type)
{
case 'option':
$options = $this->bi->getDataviewOptions($object, $field);
break;
case 'object':
$options = $this->bi->getObjectOptions($object, $field);
break;
case 'string':
case 'number':
if($source)
{
if($this->config->edition != 'open')
{
$this->app->loadConfig('dataview');
static $workflowFields = array();
if(!isset($workflowFields[$object])) $workflowFields[$object] = $this->loadModel('workflowfield')->getList($object);
$originalField = zget($_POST, 'originalField', $field);
$fieldObject = zget($workflowFields[$object], $originalField, null);
if($fieldObject)
{
if($fieldObject->control == 'multi-select') $this->config->dataview->multipleMappingFields[] = $object . '-' . $field;
$options = $this->workflowfield->getFieldOptions($fieldObject);
}
if(!empty(array_filter($options))) break;
}
$options = array();
if(is_array($source))
{
foreach($source as $row) if(isset($row->$field)) $options["{$row->$field}"] = $row->$field;
}
else
{
$keyField = $field;
$valueField = $saveAs ? $saveAs : $field;
$options = $this->bi->getOptionsFromSql($source, $driver, $keyField, $valueField);
}
}
break;
}
if(is_string($source) && $source && $saveAs && in_array($type, array('user', 'product', 'project', 'execution', 'dept', 'option', 'object')))
{
$options = $this->bi->getOptionsFromSql($source, $driver, $field, $saveAs);
}
return array_filter($options);
}
/**
* 替换定义的表名。
* Replace defined table names.
*
* @param string $sql
* @access public
* @return string
*/
public function replaceTableNames(string $sql): string
{
if(preg_match_all("/TABLE_[A-Z]+/", $sql, $out))
{
rsort($out[0]);
foreach($out[0] as $table)
{
if(!defined($table)) continue;
$sql = str_replace($table, trim(constant($table), '`'), $sql);
}
}
return preg_replace("/= *'\!/U", "!='", $sql);
}
/**
* 设置默认的过滤器。
* Set default filter.
*
* @param array $filters
* @access private
* @return array
*/
public function setFilterDefault(array $filters, bool $processDateVar = true): array
{
foreach($filters as &$filter)
{
$isSelect = isset($filter['type']) && $filter['type'] === 'select';
if($isSelect && isset($filter['default']) && $filter['default'] === '0')
{
$filter['default'] = '';
}
if(!isset($filter['default']) || empty($filter['default'])) continue;
if($processDateVar && is_string($filter['default'])) $filter['default']= $this->processDateVar($filter['default']);
}
return $filters;
}
/**
* 根据透视表不同阶段获取不同状态的筛选器。
* Process filters.
*
* @param array $filters
* @param string $filterStatus
* @access public
* @return void
*/
public function processFilters(array $filters, string $filterStatus): array
{
foreach($filters as $index => $filter)
{
if($filterStatus == 'published' && isset($filter['status']) && $filter['status'] == 'design')
{
unset($filters[$index]);
}
if($filterStatus == 'design')
{
if(isset($filter['status']) && $filter['status'] == 'design' && isset($filter['account']) && $filter['account'] == $this->app->user->account) continue;
unset($filters[$index]);
}
}
return array_values($filters);
}
/**
* 将筛选器的值填写到查询条件中。
* Set condition value with filters.
*
* @param array $condition
* @param array $filters
* @access public
* @return string
*/
public function setConditionValueWithFilters(array $condition, array $filters): string
{
$field = $condition['queryField'];
if(!isset($filters[$field])) return '';
$filter = $filters[$field];
extract($filter);
return " $operator $value";
}
/**
* 从透视表对象中获取字段。
* Get fields from pivot object.
*
* @param object $pivot
* @param string $key
* @param mixed $default
* @param bool $jsonDecode
* @param bool $needArray
* @access private
* @return mixed
*/
private function getFieldsFromPivot(object $pivot, string $key, mixed $default, bool $jsonDecode = false, bool $needArray = false): mixed
{
return isset($pivot->{$key}) && !empty($pivot->{$key}) ? ($jsonDecode ? json_decode($pivot->{$key}, $needArray) : $pivot->{$key}) : $default;
}
/**
* Get field options.
*
* @param array $fieldSettings
* @param string $sql
* @access public
* @return array
*
*/
public function getFieldsOptions(array $fieldSettings, array $records, string $driver = 'mysql'): array
{
$options = array();
foreach($fieldSettings as $key => $fieldSetting)
{
$type = $fieldSetting['type'];
$object = $fieldSetting['object'];
$field = $fieldSetting['field'];
$options[$key] = $this->getSysOptions($type, $object, $field, $records, '', $driver);
}
return $options;
}
/**
* Process DTable cols config, let buildPivotTable use.
*
* @param array $cols
* @access public
* @return array
*/
public function processDTableCols($cols)
{
$formatCols = array();
foreach($cols as $colField => $colInfo)
{
$formatCols[] = (object)array('name' => $colField, 'label' => $colInfo['title'], 'colspan' => 1);
}
return array($formatCols);
}
/**
* Process DTable data, let buildPivotTable use.
*
* @param array $cols
* @param array $datas
* @access public
* @return array
*/
public function processDTableData($cols, $datas)
{
return array_map(function($data) use ($cols)
{
$result = [];
$data = (array)$data;
foreach ($cols as $field) $result[] = isset($data[$field]) ? $data[$field] : '';
return $result;
}, $datas);
}
/**
* Build table use data and rowspan.
*
* @param object $data
* @param array $configs
* @access public
* @return string
*
*/
public function buildPivotTable($data, $configs)
{
$width = 128;
$nowSpan = 1;
$inFlow = false;
if(!empty($configs))
{
/* 处理不需要展示的单元格设置为0 */
$columnCount = count(current($configs));
$lineCount = count($configs);
for($i = 0; $i < $columnCount; $i ++)
{
for($j = 0; $j < $lineCount; $j ++)
{
if($configs[$j][$i] > 1 && !$inFlow)
{
$inFlow = true;
$nowSpan = $configs[$j][$i];
continue;
}
if($configs[$j][$i] > 1 && $inFlow)
{
$configs[$j][$i] = 0;
$nowSpan --;
if($nowSpan == 1) $inFlow = false;
}
}
}
}
/* Init table. */
$table = "<div class='reportData'><table class='table table-condensed table-striped table-bordered table-fixed datatable' style='width: auto; min-width: 100%' data-fixed-left-width='400'>";
$showOrigins = array();
$hasShowOrigin = false;
foreach($data->cols[0] as $col)
{
$colspan = zget($col, 'colspan', 1);
$showOrigin = isset($col->showOrigin) ? $col->showOrigin : false;
$colShowOrigin = array_fill(0, $colspan, $showOrigin);
$showOrigins = array_merge($showOrigins, $colShowOrigin);
if($showOrigin) $hasShowOrigin = true;
}
/* Init table thead. */
$table .= "<thead>";
foreach($data->cols as $lineCols)
{
$table .= "<tr>";
foreach($lineCols as $col)
{
$thName = $col->label;
$colspan = zget($col, 'colspan', 1);
$rowspan = zget($col, 'rowspan', 1);
$isGroup = zget($col, 'isGroup', false);
if($isGroup) $thHtml = "<th data-flex='false' rowspan='$rowspan' colspan='$colspan' data-width='auto' class='text-center'>$thName</th>";
else $thHtml = "<th data-flex='true' rowspan='$rowspan' colspan='$colspan' data-type='number' data-width=$width class='text-center'>$thName</th>";
$table .= $thHtml;
}
$table .= "</tr>";
}
$table .= "</thead>";
/* Init table tbody. */
$table .= "<tbody>";
$rowCount = 0;
$showAllTotal = isset($data->showAllTotal) && $data->showAllTotal;
$users = $this->loadModel('user')->getPairs('noletter,noempty,noclosed');
for($i = 0; $i < count($data->array); $i ++)
{
$rowCount ++;
if($showAllTotal && $rowCount == count($data->array)) continue;
$line = array_values($data->array[$i]);
$table .= "<tr class='text-center'>";
for($j = 0; $j < count($line); $j ++)
{
$cols = isset($data->cols[0][$j]) ? $data->cols[0][$j] : array();
$isGroup = !empty($data->cols[0][$j]->isGroup) ? $data->cols[0][$j]->isGroup : false;
$rowspan = isset($configs[$i][$j]) ? $configs[$i][$j] : 1;
$hidden = (isset($configs[$i][$j]) && $configs[$i][$j]) ? false : (bool)$isGroup;
$showOrigin = $showOrigins[$j];
if($hasShowOrigin && !$isGroup && !$showOrigin)
{
$rowspan = isset($configs[$i]) ? end($configs[$i]) : 1;
$hidden = isset($configs[$i]) ? false : true;
}
$lineValue = $line[$j];
if(is_numeric($lineValue)) $lineValue = round($lineValue, 2);
if(isset($cols->name) && in_array($cols->name, $this->config->pivot->userFields)) $lineValue = isset($users[$lineValue]) ? $users[$lineValue] : $lineValue;
if(!$hidden) $table .= "<td rowspan='$rowspan'>$lineValue</td>";
}
$table .= "</tr>";
}
if($showAllTotal && !empty($data->array))
{
$table .= "<tr class='text-center'>";
$table .= "<td colspan='" . count($data->groups) . "'>{$this->lang->pivot->stepDesign->total}</td>";
foreach(end($data->array) as $field => $total)
{
if(in_array($field, $data->groups)) continue;
if(is_numeric($total)) $total = round($total, 2);
$table .= "<td>$total</td>";
}
$table .= "</tr>";
}
$table .= "</tbody>";
$table .= "</table></div>";
return $table;
}
/* Data Drill */
/**
* Get cols for preview data table.
*
* @param string $objectTable
* @access public
* @return array
*/
public function getDrillCols($object)
{
if($object == 'case') $object = 'testcase';
$cols = array();
if(isset($this->config->pivot->drillObjectFields[$object]))
{
$this->loadModel($object);
if(!isset($this->config->$object->dtable->fieldList)) return $this->config->pivot->objectTableFields->$object;
$fieldList = $object == 'product' ? $this->config->product->all->dtable->fieldList : $this->config->$object->dtable->fieldList;
$userTypeCols = $this->config->pivot->userTypeCols;
$nameTypeCols = $this->config->pivot->nameTypeCols;
$reuseDtableFields = $this->config->pivot->reuseDtableFields;
foreach($this->config->pivot->drillObjectFields[$object] as $fieldKey)
{
$fieldSetting = isset($fieldList[$fieldKey]) ? $fieldList[$fieldKey] : $this->config->pivot->objectTableFields->$object[$fieldKey];
$fieldSetting['sortType'] = false;
if(isset($fieldSetting['checkbox']) && $fieldSetting['checkbox']) $fieldSetting['checkbox'] = false;
if(isset($fieldSetting['link']))
{
if(is_string($fieldSetting['link']))
{
$fieldSettingLink = $fieldSetting['link'];
$fieldSetting['link'] = array();
$fieldSetting['link']['url'] = $fieldSettingLink;
}
$fieldSetting['link']['target'] = '_blank';
}
if(isset($fieldSetting['type']) && in_array($fieldSetting['type'], $userTypeCols)) $fieldSetting['type'] = 'user';
foreach(array_keys($fieldSetting) as $settingKey)
{
if(!in_array($settingKey, $reuseDtableFields)) unset($fieldSetting[$settingKey]);
if((!in_array($fieldKey, $nameTypeCols) && $settingKey == 'link') || $object == 'doc') unset($fieldSetting['link']);
if(isset($this->config->pivot->objectTableFields->$object[$fieldKey][$settingKey])) $fieldSetting[$settingKey] = $this->config->pivot->objectTableFields->$object[$fieldKey][$settingKey];
}
$cols[$fieldKey] = $fieldSetting;
}
}
else
{
$this->app->loadLang($object);
$table = isset($this->config->objectTables[$object]) ? $this->config->objectTables[$object] : $this->config->db->prefix . $object;
$table = str_replace('`', '', $table);
$fieldList = $this->loadModel('dev')->getFields($table);
foreach($fieldList as $fieldName => $field)
{
if(empty($field['name'])) continue;
$fieldLabel = $field['name'];
if(isset($this->lang->$object->$fieldName)) $fieldLabel = $this->lang->$object->$fieldName;
$cols[$fieldName] = array('name' => $fieldName, 'title' => $fieldLabel);
}
}
return $cols;
}
/**
* getReferSQL
*
* @param string $object
* @param string $whereSQL
* @param array $fields
* @access public
* @return string
*/
public function getReferSQL(string $object, string $whereSQL = '', array $fields = array()): string
{
$fieldStr = empty($fields) ? '' : (',' . implode(',', $fields));
$table = $this->config->db->prefix . $object;
$referSQL = "SELECT t1.* {$fieldStr} FROM $table AS t1";
return "$referSQL $whereSQL";
}
/**
* Get drill sql.
*
* @param string $objectTable
* @param string $whereSQL
* @param string $conditionsSQL
* @access public
* @return string
*/
public function getDrillSQL($objectTable, $whereSQL = '', $conditions = array())
{
$fieldList = array();
$conditionSQLs = array('1=1');
foreach($conditions as $condition)
{
extract($condition);
if($drillAlias != 't1')
{
$fieldList[] = "{$drillAlias}.{$drillField} AS {$drillAlias}{$drillField}";
$drillField = $drillAlias . $drillField;
}
if(!empty($condition['value']))
{
if(!empty($condition['htmlspecialed'])) $value .= " OR t1.{$drillField} = {$condition['htmlspecialed']}";
$conditionSQLs[] = "(t1.{$drillField}{$value})";
}
}
$referSQL = $this->getReferSQL($objectTable, $whereSQL, $fieldList);
$conditionSQL = 'WHERE ' . implode(' AND ', $conditionSQLs);
return "SELECT t1.* FROM ($referSQL) AS t1 {$conditionSQL}";
}
/**
* Execute drill sql.
*
* @param string $object
* @param string $drillSQL
* @access public
* @return array
*/
public function execDrillSQL($object, $drillSQL, $limit = 10)
{
$limitSQL = "SELECT * FROM ($drillSQL) AS t1 LIMIT $limit";
$queryResult = $this->loadModel('bi')->querySQL($drillSQL, $limitSQL);
$result = array();
if($queryResult['result'] == 'success')
{
$result['data'] = $queryResult['rows'];
$result['cols'] = $this->getDrillCols($object);
}
if($queryResult['result'] == 'fail') $result['error'] = $queryResult['message'];
$result['status'] = $queryResult['result'];
return $result;
}
/**
* Parse query filter, then get drill result.
*
* @param string $object
* @param string $whereSQL
* @param array $filters
* @param array $conditions
* @param bool $emptyFilters
* @param int $limit
* @access public
* @return array
*/
public function getDrillResult($object, $whereSQL, $filters = array(), $conditions = array(), $emptyFilters = true, $limit = 10)
{
$drillSQL = $this->getDrillSQL($object, $whereSQL, $conditions);
if(!empty($filters)) $drillSQL = $this->loadModel('bi')->processVars($drillSQL, $filters, $emptyFilters);
return $this->execDrillSQL($object, $drillSQL, $limit);
}
/**
* Get drill datas.
*
* @param object $pivotState
* @param object $drill
* @param array $conditions
* @param array $filterValues
* @access public
* @return array
*/
public function getDrillDatas(object $pivotState, object $drill, array $conditions, array $filterValues = array()): array
{
$filters = $pivotState->setFiltersDefaultValue($filterValues);
foreach($conditions as $index => $condition)
{
if(isset($condition['value']))
{
$htmlspecialed = htmlspecialchars($condition['value']);
$conditions[$index]['value'] = " = " . $this->dbh->quote($condition['value']);
if($htmlspecialed != $condition['value']) $conditions[$index]['htmlspecialed'] = $this->dbh->quote($htmlspecialed);
}
}
$data = array();
$status = null;
if($pivotState->isQueryFilter())
{
$queryResult = $this->getDrillResult($drill->object, $drill->whereSql, $filters, $conditions, false, 999999);
$data = $queryResult['data'];
$status = $queryResult['status'];
}
else
{
$filters = $pivotState->convertFiltersToWhere($filters);
foreach($conditions as $index => $condition)
{
if(!isset($condition['value'])) $conditions[$index]['value'] = $this->setConditionValueWithFilters($condition, $filters);
}
$drillSQL = $this->getDrillSQL($drill->object, $drill->whereSql, $conditions);
$queryResult = $this->loadModel('bi')->querySQL($drillSQL, $drillSQL);
$data = $queryResult['rows'];
$status = $queryResult['result'];
}
if($status != 'success') return array();
return $data;
}
/**
* Process task datas in Drill modal.
*
* @param array $datas
* @access public
* @return array
*/
public function processKanbanDatas(string $object, array $datas): array
{
$kanbans = $this->dao->select('id')->from(TABLE_PROJECT)->where('type')->eq('kanban')->fetchPairs();
if($object == 'story') $projectStory = $this->dao->select('story, project')->from(TABLE_PROJECTSTORY)->fetchPairs();
foreach($datas as $data)
{
$projectID = 0;
if($object == 'story')
{
$projectID = isset($projectStory[$data->id]) ? $projectStory[$data->id] : 0;
}
else
{
$projectID = zget($data, 'execution', 0);
}
if($projectID && isset($kanbans[$projectID])) $data->isModal = true;
}
return $datas;
}
/**
* Get versions of a pivot.
*
* @param int $pivotID
* @access public
* @return array|bool
*/
public function getPivotVersions(int $pivotID): array|bool
{
$pivot = $this->dao->select('*')->from(TABLE_PIVOT)->where('id')->eq($pivotID)->andWhere('deleted')->eq('0')->fetch();
if(!$pivot) return false;
$pivotSpecList = $this->dao->select('*')->from(TABLE_PIVOTSPEC)->where('pivot')->eq($pivotID)->fetchAll('', false);
if(!$pivotSpecList) return false;
$pivotVersionList = array();
foreach($pivotSpecList as $specData)
{
$pivotVersion = clone $pivot;
foreach($specData as $specKey => $specValue) $pivotVersion->$specKey = $specValue;
$this->processNameDesc($pivotVersion);
$pivotVersionList[] = $pivotVersion;
}
return $pivotVersionList;
}
/**
* Get max version.
*
* @param int $pivotID
* @access public
* @return string
*/
public function getMaxVersion(int $pivotID): string
{
$versions = $this->dao->select('version')->from(TABLE_PIVOTSPEC)->where('pivot')->eq($pivotID)->fetchPairs();
$maxVersion = current($versions);
foreach($versions as $version)
{
if(version_compare($version, $maxVersion, '>')) $maxVersion = $version;
}
return $maxVersion;
}
/**
* Get max version by idList.
*
* @param string|array $pivotIDList
* @access public
* @return string
*/
public function getMaxVersionByIDList(string|array $pivotIDList)
{
$pivotVersions = $this->dao->select('pivot,version')->from(TABLE_PIVOTSPEC)
->where('pivot')->in($pivotIDList)
->fetchGroup('pivot', 'version');
if(empty($pivotVersions)) return array();
$pivotMaxVersion = array();
foreach($pivotVersions as $pivotID => $versions)
{
$versions = array_keys($versions);
$maxVersion = current($versions);
foreach($versions as $version)
{
if(version_compare($version, $maxVersion, '>')) $maxVersion = $version;
}
$pivotMaxVersion[$pivotID] = $maxVersion;
}
return $pivotMaxVersion;
}
public function isVersionChange(array|object $pivots, bool $isObject = true)
{
if($isObject) $pivots = array($pivots);
$pivotMaxVersion = $this->getMaxVersionByIDList(array_column($pivots, 'id'));
foreach($pivots as $index => $pivot)
{
$maxVersion = zget($pivotMaxVersion, $pivot->id, '');
$pivots[$index]->versionChange = $maxVersion != $pivot->version && $pivot->builtin == 1;
}
return $isObject ? current($pivots) : $pivots;
}
/**
* Switch pivot to a new version.
*
* @param int $pivotID
* @param string $version
* @access public
* @return bool
*/
public function switchNewVersion(int $pivotID, string $version): bool
{
$this->dao->update(TABLE_PIVOT)->set('version')->eq($version)->where('id')->eq($pivotID)->exec();
return !dao::isError();
}
/**
* Filter special chars in query data.
*
* @param array $records
* @access public
* @return array
*/
public function filterSpecialChars($records)
{
if(empty($records)) return $records;
foreach($records as $index => $record)
{
foreach($record as $field => $value)
{
$value = is_string($value) ? str_replace('"', '', htmlspecialchars_decode($value)) : $value;
if(is_object($record)) $record->$field = $value;
if(is_array($record)) $record[$field] = $value;
}
$records[$index] = $record;
}
return $records;
}
}
/**
* Sort summary
*
* @param array $pre
* @param array $next
* @access public
* @return mixed
*/
function sortSummary($pre, $next)
{
if($pre['validRate'] == $next['validRate']) return 0;
return $pre['validRate'] > $next['validRate'] ? -1 : 1;
}