* @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'); } /* * 获取透视表。 * Get pivot. * * @param int $pivotID * @param bool $processDateVar * @access public * @return object|bool */ public function getByID(int $pivotID, bool $processDateVar = false): object|bool { $pivot = $this->dao->select('*')->from(TABLE_PIVOT)->where('id')->eq($pivotID)->fetch(); 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(); } return $this->processPivot($pivot); } /** * 时间占位符替换为实际的时间。 * Replace time placeholder with actual time. * * @param string $var * @param string $type * @access public * @return string */ public function processDateVar(string $var, string $type = 'date'): string { if(empty($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); $screenList = $this->dao->select('scheme')->from(TABLE_SCREEN)->where('deleted')->eq(0)->andWhere('status')->eq('published')->fetchAll(); foreach($pivots as $pivot) $this->completePivot($pivot, $screenList); if($isObject && isset($pivot->stage) && $pivot->stage == 'published') $this->processFieldSettings($pivot); return $isObject ? $pivot : $pivots; } /** * 完善透视表。 * Complete pivot. * * @param object $pivot * @param array $screenList * @access public * @return void */ private function completePivot(object $pivot, array $screenList): void { if(!empty($pivot->sql)) $pivot->sql = trim(str_replace(';', '', $pivot->sql)); if(!empty($pivot->settings)) $pivot->settings = json_decode($pivot->settings, true); if(empty($pivot->type)) { $pivot->names = array('zh-cn' => '', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''); $pivot->descs = array('zh-cn' => '', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''); if(!empty($pivot->name)) { $pivotNames = json_decode($pivot->name, true); $pivot->name = zget($pivotNames, $this->app->getClientLang(), '') ? : reset($pivotNames); $pivot->names = $pivotNames; } if(!empty($pivot->desc)) { $pivotDescs = json_decode($pivot->desc, true); $pivot->desc = zget($pivotDescs, $this->app->getClientLang(), ''); $pivot->descs = $pivotDescs; } $pivot->used = $this->checkIFChartInUse($pivot->id, 'pivot', $screenList); } } /** * 检测图表是否在使用。 * 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 { $fieldSettings = $pivot->fieldSettings ?? $this->getFieldsFromPivot($pivot, 'fields', array(), true); 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('chart')->parseSqlVars($sql, $filters); $this->dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); $stmt = $this->dbh->query($querySQL); if(!$stmt) return; $columns = $this->loadModel('dataview')->getColumns($querySQL); $columnFields = array(); foreach(array_keys(get_object_vars($columns)) as $type) $columnFields[$type] = $type; extract($this->chart->getTables($querySQL)); /* 获取field的键值对以及相关联的对象。 */ /* Get field key value pairs and related objects. */ $moduleNames = $tables ? $this->dataview->getModuleNames($tables) : array(); list($fieldPairs, $relatedObject) = $this->dataview->mergeFields($columnFields, $fields, $moduleNames); $objectFields = array(); foreach(array_keys($this->lang->dataview->objects) as $object) $objectFields[$object] = $this->dataview->getTypeOptions($object); /* 重建fieldSettings字段。 */ /* Rebuild fieldSettings field. */ $pivot->fieldSettings = $this->rebuildFieldSettings($fieldPairs, $columns, $relatedObject, $fieldSettings, $objectFields); } /** * 重建透视表filedSettings字段 * Rebuild fieldSettings field of pivot. * * @param object $pivot * @param array $fieldPairs * @param object $columns * @param array $relatedObject * @param object $fieldSettings * @param array $objectFields * @access private * @return object */ public function rebuildFieldSettings(array $fieldPairs, object $columns, array $relatedObject, object $fieldSettings, array $objectFields): object { $fieldSettingsNew = new stdclass(); foreach($fieldPairs as $index => $field) { $defaultType = $columns->{$index}; $defaultObject = $relatedObject[$index]; if(isset($objectFields[$defaultObject][$index])) $defaultType = $objectFields[$defaultObject][$index]['type'] == 'object' ? 'string' : $objectFields[$defaultObject][$index]['type']; if(!isset($fieldSettings->{$index})) { /* 如果字段设置中没有该字段,则使用默认的配置。 */ /* If the field is not set in the field settings, use the default value. */ $fieldItem = new stdclass(); $fieldItem->name = $field; $fieldItem->object = $defaultObject; $fieldItem->field = $index; $fieldItem->type = $defaultType; $fieldSettingsNew->{$index} = $fieldItem; } else { /* 兼容旧版本的字段设置,当为空或者为布尔值时,使用默认值 */ /* Compatible with old version of field settings, use default value when empty or boolean. */ if(!isset($fieldSettings->{$index}->object) || is_bool($fieldSettings->{$index}->object) || strlen($fieldSettings->{$index}->object) == 0) $fieldSettings->{$index}->object = $defaultObject; /* 当字段设置中没有字段名时,使用默认的字段名配置。 */ /* When there is no field name in the field settings, use the default field name configuration. */ if(!isset($fieldSettings->{$index}->field) || strlen($fieldSettings->{$index}->field) == 0) { $fieldSettings->{$index}->field = $index; $fieldSettings->{$index}->object = $defaultObject; $fieldSettings->{$index}->type = 'string'; } $object = $fieldSettings->{$index}->object; $type = $fieldSettings->{$index}->type; if($object == $defaultObject && $type != $defaultType) $fieldSettings->{$index}->type = $defaultType; $fieldSettingsNew->{$index} = $fieldSettings->{$index}; } } return $fieldSettingsNew; } /** * 获取执行。 * 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 * @access public * @return array */ public function getProducts(string $conditions, string $storyType = 'story'): array { $permission = common::hasPriv('pivot', 'showProduct') || $this->app->user->admin; $IDList = !$permission ? $this->app->user->view->products : array(); $products = $this->pivotTao->getProductList($conditions, $IDList); /* 为产品生成计划数据和相关的需求数据。 */ /* 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; $taskGroups[$task->user][$task->projectID][$task->executionID][$task->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->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}") : "executionName}>{$execution->executionName}"; } 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(isset($teamTasks[$task->id])) $task->left = $teamTasks[$task->id]->left; $totalHours += $task->left; } } } /* 计算员工的工作负载。 */ /* 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 { $filterFormat = array(); if(empty($filters)) return array($sql, $filterFormat); foreach($filters as $filter) { $field = $filter['field']; if(isset($filter['from']) && $filter['from'] == 'query') { $queryDefault = isset($filter['default']) ? $this->processDateVar($filter['default']) : ''; $sql = str_replace('$' . $filter['field'], "'{$queryDefault}'", $sql); } else { if(!isset($filter['default'])) continue; $default = $filter['default']; switch($filter['type']) { case 'select': if(empty($default)) break; if(is_array($default)) $default = implode("', '", array_filter($default, function($val){return trim($val) != '';})); $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 str_replace(';', '', $sql); } /** * Append where filter to sql from filters. * * @param string $sql * @param array $filters * @access public * @return string */ public function appendWhereFilterToSql(string $sql, 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"; } $sql = "select * from ($sql) tt" . $connectSQL; return $sql; } /** * Map record value with field options. * * @param array $records * @param array $fields * @param string $sql * @access public * @return array */ public function mapRecordValueWithFieldOptions(array $records, array $fields, string $sql): array { $this->app->loadConfig('dataview'); $records = json_decode(json_encode($records), true); $fieldOptions = $this->getFieldsOptions($fields, $sql); foreach($records as $index => $record) { foreach($record as $field => $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; } } $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(isset($langs[$group]) and !empty($langs[$group][$clientLang])) $colLabel = $langs[$group][$clientLang]; $col->label = $colLabel; $cols[0][] = $col; } return $cols; } /** * Process column original. * * @param int $index * @param string $field * @param array $groups * @param array $records * @access public * @return array */ public function processColumnOriginal(int $index, string $field, array $groups, array $records): array { $columnRecords = array(); foreach($records as $record) { $columnRecord = new stdclass(); foreach($groups as $group) $columnRecord->$group = $record->$group; $columnRecord->{$field . $index} = $record->$field; $columnRecords[] = $columnRecord; } return $columnRecords; } /** * Get slice field key of record. * * @param int $index * @param string $slice * @param string $field * @param object $record * @access public * @return string */ public function getSliceFieldKey(int $index, string $slice, string $field, object $record): string { if($slice == 'noSlice') return $field . $index; return $record->$slice . '_slice_' . $field . $index; } /** * Init statistic column with slice. * * @param int $index * @param string $field * @param string $slice * @param array $groups * @param array $records * @access public * @return array */ public function initSliceColumnRecords(int $index, string $field, string $slice, array $groups, array $records): array { $columnRecords = array(); $groupUnique = array(); $sliceUnique = array(); foreach($records as $record) { $groupUnionKey = $this->getGroupsKey($groups, $record); $fieldKey = $this->getSliceFieldKey($index, $slice, $field, $record); $sliceUnique[$fieldKey] = 1; $groupUnique[$groupUnionKey] = $record; } $sliceKeys = array_keys($sliceUnique); $groupUnionKeys = array_keys($groupUnique); foreach($groupUnionKeys as $groupUnionKey) { $columnRecord = new stdclass(); foreach($groups as $group) $columnRecord->$group = $groupUnique[$groupUnionKey]->$group; foreach($sliceKeys as $sliceKey) { $columnRecord->$sliceKey = array('count' => 0, 'distinct' => array(), 'sum' => 0, 'avg' => array(), 'max' => array(), 'min' => array()); } $columnRecords[$groupUnionKey] = $columnRecord; } return $columnRecords; } /** * Process column stat with slice. * * @param int $index * @param string $field * @param string $slice * @param string $stat * @param array $groups * @param array $records * @access public * @return array */ public function processColumnStat(int $index, string $field, string $slice, string $stat, array $groups, array $records): array { $sliceRecords = $this->initSliceColumnRecords($index, $field, $slice, $groups, $records); foreach($records as $record) { $groupUnionKey = $this->getGroupsKey($groups, $record); $fieldKey = $this->getSliceFieldKey($index, $slice, $field, $record); $sliceGroupRecord = $sliceRecords[$groupUnionKey]; $value = $record->$field; $floatValue = is_numeric($value) ? (float)$value : 0; switch($stat) { case 'sum': $sliceGroupRecord->{$fieldKey}[$stat] += $floatValue; break; case 'avg': case 'max': case 'min': $sliceGroupRecord->{$fieldKey}[$stat][] = $floatValue; break; case 'count': $sliceGroupRecord->{$fieldKey}[$stat] += 1; break; case 'distinct': $sliceGroupRecord->{$fieldKey}[$stat][] = $value; break; } } foreach($sliceRecords as $groupUnionKey => $sliceRecord) { $sliceFields = array_keys((array)$sliceRecord); foreach($sliceFields as $sliceField) { /* 分组字段直接跳过。*/ /* Skip the group field directly. */ if(in_array($sliceField, $groups)) continue; $sliceStat = $sliceRecord->{$sliceField}[$stat]; switch($stat) { case 'sum': $sliceRecord->$sliceField = round($sliceStat, 2); break; case 'avg': $sum = array_sum($sliceStat); $count = count($sliceStat); $sliceRecord->$sliceField = $count == 0 ? 0 : round($sum / $count, 2); break; case 'max': $sliceRecord->$sliceField = $sliceStat ? round(max($sliceStat), 2) : 0; break; case 'min': $sliceRecord->$sliceField = $sliceStat ? round(min($sliceStat), 2) : 0; break; case 'count': $sliceRecord->$sliceField = $sliceStat; break; case 'distinct': $sliceRecord->$sliceField = count(array_unique($sliceStat)); break; } } } return $sliceRecords; } /** * Merge origin records. * * @param array $originColumns * @param array $mergeRecords * @access public * @return array */ public function mergeOriginRecords(array $originColumns, array $mergeRecords): array { if(empty($originColumns)) return $mergeRecords; $originColumns = $this->sortWithItemLength($originColumns); foreach($originColumns as $columnIndex => $columnSetting) { if(empty($mergeRecords)) { $mergeRecords = $columnSetting['records']; continue; } $columnRecords = $columnSetting['records']; foreach($mergeRecords as $index => $mergeRecord) { $mergeRecords[$index] = (object)array_merge((array)$mergeRecord, (array)$columnRecords[$index]); } } return $mergeRecords; } /** * Merge statistic records. * * @param array $statColumns * @param array $groups * @param array $mergeRecords * @access public * @return array */ public function mergeStatRecords(array $statColumns, array $groups, array $mergeRecords): array { if(empty($statColumns)) return $mergeRecords; $statColumns = $this->sortWithItemLength($statColumns); foreach($statColumns as $columnIndex => $columnSetting) { if(empty($mergeRecords)) { $mergeRecords = $columnSetting['records']; continue; } $columnRecords = $columnSetting['records']; foreach($mergeRecords as $key => $mergeRecord) { $groupUnionKey = $this->getGroupsKey($groups, $mergeRecord); $mergeRecords[$key] = (object)array_merge((array)$mergeRecord, (array)$columnRecords[$groupUnionKey]); } } return $mergeRecords; } /** * Sort array with item length. * * @param array $array * @access public * @return array */ public function sortWithItemLength(array $array): array { usort($array, function($a, $b) { $lengthA = count($a); $lengthB = count($b); if ($lengthA == $lengthB) { return 0; } return ($lengthA > $lengthB) ? -1 : 1; }); return $array; } /** * Sort merge records with group field. * * @param array $records * @param array $mergeRecords * @param array $groups * @access public * @return array */ public function orderByRecordsGroups(array $records, array $mergeRecords, array $groups): array { $groupTree = $this->generateGroupTree($groups, $records); $groupRecords = $this->flattenGroupTree($groups, $groupTree); foreach($mergeRecords as $mergeRecord) { $groupUnionKey = $this->getGroupsKey($groups, $mergeRecord); $groupRecords[$groupUnionKey][] = $mergeRecord; } $orderRecords = array(); foreach($groupRecords as $groupRecord) { $orderRecords = array_merge($orderRecords, $groupRecord); } return $orderRecords; } /** * Generate group tree. * * @param array $groups * @param array $records * @access public * @return array */ public function generateGroupTree(array $groups, array $records): array { $groupTree = array(); foreach($records as $record) { $currentGroupTree = &$groupTree; foreach($groups as $group) { $groupValue = $record->$group; if(!isset($currentGroupTree[$groupValue])) $currentGroupTree[$groupValue] = array(); $currentGroupTree = &$currentGroupTree[$groupValue]; } } return $groupTree; } /** * Flatten group tree. * * @param array $groups * @param array $groupTree * @access public * @return void */ public function flattenGroupTree($groups, $groupTree) { $flattenGroup = array_map(function($key, $value) { return array($key, $value); }, array_keys($groupTree), $groupTree); $groupLevels = count($groups); $currentLevel = 1; $flattenGroupRecords = array(); while($currentLevel < count($groups)) { $nextFlatten = array(); foreach($flattenGroup as $flatten) { $lastValue = array_pop($flatten); foreach($lastValue as $key => $value) { /* 如果value为空,那么说明到了最后一级,直接构建分组键值的数组。*/ /* If the value is empty, it means that the last level has been reached, and the array of the group key value is constructed directly. */ if(empty($value)) { $unionKey = implode('_', $flatten) . '_' . $key; $flattenGroupRecords[$unionKey] = array(); continue; } $nextFlatten[] = array_merge($flatten, array($key, $value)); } } $flattenGroup = $nextFlatten; $currentLevel ++; } return $flattenGroupRecords; } /** * Calculate merge cell config. * * @param array $groups * @param array $records * @access public * @return array */ public function calculateMergeCellConfig(array $groups, array $records) { $configs = array(); foreach($groups as $index => $group) { if($index == 0) { $groupRecords = array(); foreach($records as $record) $groupRecords[$record->$group][] = $record; } $haveNext = isset($groups[$index + 1]); $this->getColumnConfig($groupRecords, $configs, $groups, $index, $haveNext); } return $configs; } /** * Get column config to merge table cell. * * @param array $groupRows * @param array $configs * @param array $groups * @param int $index * @param bool $haveNext * @access public * @return void * * Init $groupRows like this: array('people1' => [0 => ['create' => 'people1', 'product' => 'product1']], 'people2' => ['create' => 'people2', 'product' => 'product2']]) * The second time the function is executed, groupRows is passed in array('people1_product1' => [0 => ['create' => 'people1', 'product' => 'product1']], 'people2_product2' => ['create' => 'people2', 'product' => 'product2']]) * * The key value of this array is unique; */ public function getColumnConfig(array &$groupRows, array &$configs, array $groups, int $index, bool $haveNext): void { $newRows = array(); $start = 1; $next = $index + 1; foreach($groupRows as $key => $datas) { $number = count($datas); $configs[$start - 1][$next - 1] = $number; $start += $number; if($haveNext) { $nextGroup = $groups[$next]; foreach($datas as $data) { $newKey = $key . '_' . $data->$nextGroup; $newRows[$newKey][] = $data; } } } if($haveNext) $groupRows = $newRows; } /** * Gen sheet. * * @param array $fields * @param array $settings * @param string $sql * @param array $filters * @param array $langs * @access public * @return array */ public function genSheet(array $fields, array $settings, string $sql, array $filters, array $langs = array()): array { $groups = $this->getGroupsFromSettings($settings); $cols = $this->generateTableCols($fields, $groups, $langs); /* Replace the variable with the default value. */ $sql = $this->initVarFilter($filters, $sql); $sql = $this->trimSemicolon($sql); $sql = $this->appendWhereFilterToSql($sql, $filters); $records = $this->dao->query($sql)->fetchAll(); $records = $this->mapRecordValueWithFieldOptions($records, $fields, $sql); $showColTotal = zget($settings, 'columnTotal', 'noShow'); $mergeRecords = array(); if(isset($settings['columns'])) { $columnSettings = $settings['columns']; foreach($columnSettings as $columnIndex => $columnSetting) { $columnShowOrigin = isset($columnSetting['showOrigin']) ? $columnSetting['showOrigin'] : false; $columnStat = $columnSetting['stat']; $columnField = $columnSetting['field']; $columnSlice = zget($columnSetting, 'slice', 'noSlice'); $cols = $this->getTableHeader($records, $columnSetting, $fields, $cols, $sql, $langs, $columnShowOrigin); if($columnShowOrigin) { $columnRecords = $this->processColumnOriginal($columnIndex, $columnField, $groups, $records); if($columnRecords) $columnRecords = $this->processShowData($columnRecords, $groups, $columnSetting, $showColTotal, $columnField . $columnIndex); $columnSetting['records'] = $columnRecords; $mergeRecords = $this->mergeOriginRecords(array($columnSetting), $mergeRecords); } elseif(!empty($columnStat)) { $columnRecords = $this->processColumnStat($columnIndex, $columnField, $columnSlice, $columnStat, $groups, $records); if($columnRecords) $columnRecords = $this->processShowData($columnRecords, $groups, $columnSetting, $showColTotal, $columnField . $columnIndex); $columnSetting['records'] = $columnRecords; $mergeRecords = $this->mergeStatRecords(array($columnSetting), $groups, $mergeRecords); } } } $mergeRecords = $mergeRecords ? array_values($mergeRecords) : array(); $mergeRecords = $this->orderByRecordsGroups($records, $mergeRecords, $groups); $data = new stdclass(); $data->groups = $groups; $data->cols = $cols; $data->array = json_decode(json_encode($mergeRecords), true); if($showColTotal == 'sum' && count($data->array)) $this->processLastRow($data->array[count($data->array) - 1]); $data->columnTotal = isset($settings['columnTotal']) ? $settings['columnTotal'] : ''; $configs = $this->calculateMergeCellConfig($groups, $mergeRecords); /* $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); } /** * Process last column data. * * @param array $data * @access public * @return void */ public function processLastRow(array &$data) { foreach($data as $key => $value) { if($value == '$totalGroup$') $data[$key] = $this->lang->pivot->step2->total; } } /** * 通过合并单元格的数据对透视表进行分页 * Page pivot by configs merge cell data. * * @param array $configs * @param int $page * @param bool $useColumnTotal * @static * @access public * @return bool */ public function pagePivot($configs, $page, $useColumnTotal) { $configs = array_values($configs); // 当前在第几页 $nowPage = 1; // 一共多少行 $pageCount = 0; // 当前页目前多少行 $nowCount = 0; // 记录当前第几个分组(项),共有多少行 $itemRow = array(0 => 0); // 目标分页的起始行和结束行 $start = $end = -1; foreach($configs as $key => $config) { if($nowPage == $page and $start == -1) $start = $pageCount; if($nowPage == $page + 1 and $end == -1) $end = $pageCount; $pageCount += $config[0]; $nowCount += $config[0]; $itemRow[] = $pageCount; // 如果当前页超过了50行,换一页 if($nowCount >= $this->config->pivot->recPerPage) { $nowCount = 0; if(isset($configs[$key + 1])) $nowPage += 1; } } if($start == -1) $start = 0; if($end == -1) $end = $pageCount; // 获得当前页面有多少"项" $endKey = array_search($end, $itemRow); $startKey = array_search($start, $itemRow); $itemCount = $endKey - $startKey; // 如果是最后一页且使用了显示列的汇总,项数-1, if($page == $nowPage and $useColumnTotal) $itemCount --; return array($start, $end - 1, $itemCount, $nowPage); } /** * Gen sheet by origin sql. * * @param array $fields * @param array $settings * @param string $sql * @param array $filters * @param array $langs * @access public * @return string */ public function genOriginSheet($fields, $settings, $sql, $filters, $langs = array()) { $sql = $this->initVarFilter($filters, $sql); /* Process rows. */ $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"; } $this->app->loadClass('sqlparser', true); $parser = new sqlparser($sql); $statement = $parser->statements[0]; if(!$statement->limit) { $statement->limit = new stdclass(); $statement->limit->offset = 0; $statement->limit->rowCount = 99999999; } $sql = $statement->build(); $columnSQL = "select * from ($sql) tt" . $connectSQL; $rows = $this->dao->query($columnSQL)->fetchAll(); $rows = json_decode(json_encode($rows), true); $cols = array(); $clientLang = $this->app->getClientLang(); /* Build cols. */ foreach($fields as $field) { $key = $field['field']; $col = new stdclass(); $col->name = $key; $col->isGroup = true; $fieldObject = $field['object']; $relatedField = $field['field']; $colLabel = $key; if($fieldObject) { $this->app->loadLang($fieldObject); if(isset($this->lang->$fieldObject->$relatedField)) $colLabel = $this->lang->$fieldObject->$relatedField; } if(isset($langs[$key]) and !empty($langs[$key][$clientLang])) $colLabel = $langs[$key][$clientLang]; $col->label = $colLabel; $cols[0][] = $col; } $fieldOptions = $this->getFieldsOptions($fields, $sql); foreach($rows as $key => $row) { foreach($row as $field => $value) { $optionList = isset($fieldOptions[$field]) ? $fieldOptions[$field] : array(); $row[$field] = isset($optionList[$value]) ? $optionList[$value] : $value; } $rows[$key] = $row; } $data = new stdclass(); $data->cols = $cols; $data->array = $rows; $configs = array_fill(0, count($rows), array_fill(0, count($fields), 1)); return array($data, $configs); } /** * 初始化分组信息。 * Init groups info. * * @param array $fields * @param array $settings * @param array $langs * @access public * @return array */ public function initGroups(array $fields, array $settings, array $langs): array { $groups = $sqlGroups = array(); $condition = !empty($settings['filterType']) && $settings['filterType'] == 'query'; $clientLang = $this->app->getClientLang(); foreach($settings as $key => $value) { if(strpos($key, 'group') !== false && $value) $groups[] = $value; } $groups = array_unique($groups); foreach($groups as $group) $sqlGroups[] = $condition ? "`$group`" : "tt.`$group`"; $groupList = implode(',', $sqlGroups); $groupCol = array(); foreach($groups as $group) { $col = new stdclass(); $col->name = $group; $col->isGroup = true; $fieldObject = $fields[$group]['object']; $relatedField = $fields[$group]['field']; /* 如果有自定义的语言包,则使用自定义的语言包,否则使用系统默认的语言包。 */ /* If there is a custom language pack, use the custom language pack, otherwise use the system default language pack. */ $col->label = $this->getColLabelValue($group, $fieldObject, $relatedField, $clientLang, $langs); $groupCol[] = $col; } return array($groups, $groupList, $groupCol); } /** * 根据语言适配对应的列名。 * Adapt the corresponding column name according to the language. * * @param string $field * @param string $fieldObject * @param string $relatedField * @param string $clientLang * @param array $langs * @access private * @return string */ public function getColLabelValue(string $field, string $fieldObject, string $relatedField, string $clientLang, array $langs = array()): string { $colLabel = $field; if(isset($langs[$field][$clientLang])) { $colLabel = $langs[$field][$clientLang]; } elseif($fieldObject) { $this->app->loadLang($fieldObject); if(isset($this->lang->{$fieldObject}->{$relatedField})) $colLabel = $this->lang->{$fieldObject}->{$relatedField}; } return $colLabel; } /** * 初始化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; } /** * 构建表格数据。 * Build table data. * * @param array $columns * @param string $sql * @param array $filters * @param array $groups * @param string $groupList * @param array $fields * @param string $showColTotal * @param array $cols * @param array $langs * @access private * @return array */ public function processGroupRows(array $columns, string $sql, array $filters, array $groups, string $groupList, array $fields, string $showColTotal, array &$cols ,array $langs): array { list($sql, $connectSQL, $groupSQL, $orderSQL) = $this->initSql($sql, $filters, $groupList); $number = 0; $showOrigin = !empty(array_filter(array_column($settings['columns'] ?? array(), 'showOrigin'))); $groupsRow = array(); foreach($columns as $column) { $columnShowOrigin = zget($column, 'showOrigin', ''); if($columnShowOrigin) $column['slice'] = 'noSlice'; $stat = $column['stat']; $field = $column['field']; $slice = zget($column, 'slice', 'noSlice'); $uuName = $field . $number; $number ++; /* 获取列的原始数据,并且根据原始数据生成表头,切片数据以及统计数据。 */ /* Get the original data of the column, and generate the table header, slice data and statistics data based on the original data. */ $columnRows = $this->getColumnRows($fields, $field, $sql, $connectSQL, $orderSQL, $uuName, $groupList, $groupSQL, $slice, $stat, $columnShowOrigin); $rowcount = array_fill(0, count($columnRows), 1); if($showOrigin && !$columnShowOrigin) { $countSQL = "select $groupList, count(tt.`$field`) as rowCount from ($sql) tt" . $connectSQL . $groupSQL . $orderSQL; $countRows = $this->dao->query($countSQL)->fetchAll(); foreach($countRows as $key => $countRow) $rowcount[$key] = $countRow->rowCount; } $this->getTableHeader($columnRows, $column, $fields, $cols, $sql, $langs, $columnShowOrigin); if($slice != 'noSlice') $columnRows = $this->processSliceData($columnRows, $groups, $slice, $uuName); $columnRows = $this->processShowData($columnRows, $groups, $column, $showColTotal, $uuName); $this->getMergeData($columnRows, $groupsRow); } return $groupsRow; } /** * 获取原始指标数据。 * Get original index data. * * @param array $fields * @param string $field * @param string $sql * @param string $connectSQL * @param string $orderSQL * @param string $uuName * @param string $groupList * @param string $groupSQL * @param string $slice * @param string $stat * @param bool $columnShowOrigin * @access private * @return array */ private function getColumnRows(array $fields, string $field, string $sql, string $connectSQL, string $orderSQL, string $uuName, string $groupList, string $groupSQL, string $slice, string $stat, bool $columnShowOrigin): array { if($columnShowOrigin) { $columnSQL = "select $groupList, tt.`$field` from ($sql) tt" . $connectSQL . $orderSQL; } else { if($stat == 'distinct') { $columnSQL = "count(distinct tt.`$field`) as `$uuName`"; } else { if($fields[$field]['type'] != 'number' && in_array($stat, array('avg', 'sum'))) { $convertSql = $this->config->db->driver == 'mysql' ? "CAST(tt.`$field` AS DECIMAL(32, 2))" : "TO_DECIMAL(tt.`$field`)"; $columnSQL = "$stat($convertSql) as `$uuName`"; } else { $columnSQL = "$stat(tt.`$field`) as `$uuName`"; } } if($slice != 'noSlice') $columnSQL = "select $groupList,`$slice`,$columnSQL from ($sql) tt" . $connectSQL . $groupSQL . ",tt.`$slice`" . $orderSQL . ",tt.`$slice`"; if($slice == 'noSlice') $columnSQL = "select $groupList,$columnSQL from ($sql) tt" . $connectSQL . $groupSQL . $orderSQL; } $columnSQL = str_replace('0000-00-00', '1970-01-01', $columnSQL); return $this->dao->query($columnSQL)->fetchAll(); } /** * 合并数据。 * Merge data. * * @param array $columnRows * @param array $groups * @param string $slice * @param string $uuName * @access private * @return array */ private function getMergeData(array $columnRows, array &$groupsRow) { $rowIndex = 0; foreach($columnRows as $key => $row) { $count = isset($rowcount[$key]) ? $rowcount[$key] : 1; for($i = 0; $i < $count; $i++) { if(!isset($groupsRow[$rowIndex])) $groupsRow[$rowIndex] = new stdclass(); $groupsRow[$rowIndex] = (object)array_merge((array)$groupsRow[$rowIndex], (array)$row); $rowIndex += 1; } } } /** * 通过过滤配置格式化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 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(), $showOrigin = false) { $stat = zget($column, 'stat', ''); $showMode = zget($column, 'showMode', 'default'); $monopolize = $showMode == 'default' ? '' : zget($column, 'monopolize', ''); $col = new stdclass(); $col->name = $column['field']; $col->isGroup = false; $col->showOrigin = $showOrigin; $fieldObject = $fields[$column['field']]['object']; $relatedField = $fields[$column['field']]['field']; $colLabel = $column['field']; if($fieldObject) { $this->app->loadLang($fieldObject); if(isset($this->lang->$fieldObject->$relatedField)) $colLabel = $this->lang->$fieldObject->$relatedField; } $clientLang = $this->app->getClientLang(); if(isset($langs[$column['field']]) and !empty($langs[$column['field']][$clientLang])) $colLabel = $langs[$column['field']][$clientLang]; if(!$showOrigin) { $colLabel = str_replace('{$field}', $colLabel, $this->lang->pivot->colLabel); $colLabel = str_replace('{$stat}', zget($this->lang->pivot->step2->statList, $stat), $colLabel); if($showMode != 'default') $colLabel .= sprintf($this->lang->pivot->colShowMode, zget($this->lang->pivot->step2->showModeList, $showMode)); } $col->label = $colLabel; $slice = zget($column, '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); foreach($sliceList as $field) { $childCol = new stdclass(); $childCol->name = $field; $childCol->isGroup = false; $childCol->label = isset($optionList[$field]) ? $optionList[$field] : $field; $childCol->colspan = $monopolize ? 2 : 1; $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->step2->total; $childCol->colspan = $monopolize ? 2 : 1; $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; } /** * Process column show mode. * * @param array $columnRows * @param array $groups * @param array $column * @param string $showColTotal * @param string $uuName * @access public * @return array */ public function processShowData(array $columnRows, array $groups, array $column, string $showColTotal, string $uuName): array { $slice = zget($column, 'slice', 'noSlice'); $showMode = zget($column, 'showMode', 'default'); $showTotal = $slice == 'noSlice' ? 'noShow' : zget($column, 'showTotal', 'noShow'); $monopolize = $showMode == 'default' ? '' : zget($column, 'monopolize', ''); $colTotal = array(); $rowTotal = array(); $allTotal = 0; foreach($columnRows as $index => $row) { if(!isset($rowTotal[$index])) $rowTotal[$index] = 0; foreach($row as $field => $value) { if(in_array($field, $groups)) continue; if(!isset($colTotal[$field])) $colTotal[$field] = 0; if($monopolize) { if(!isset($colTotal["self_$field"])) $colTotal["self_$field"] = 0; $colTotal["self_$field"] += (float)$value; } $colTotal[$field] += (float)$value; $rowTotal[$index] += (float)$value; $allTotal += (float)$value; } } if($showMode == 'total') { foreach($columnRows as $index => $row) { $columnRow = new stdclass(); foreach($row as $field => $value) { if(in_array($field, $groups)) { $columnRow->$field = $value; continue; } if($monopolize) $columnRow->{"self_$field"} = $value; $columnRow->{$field} = $this->getRatio($value, $allTotal); } if($showTotal == 'sum') { if($monopolize) $columnRow->{"sum_self_$uuName"} = (float)$rowTotal[$index]; $columnRow->{"sum_$uuName"} = $this->getRatio($rowTotal[$index], $allTotal); } $columnRows[$index] = $columnRow; } } if($showMode == 'row') { foreach($columnRows as $index => $row) { $columnRow = new stdclass(); foreach($row as $field => $value) { if(in_array($field, $groups)) { $columnRow->$field = $value; continue; } if($monopolize) $columnRow->{"self_$field"} = $value; $columnRow->{$field} = $this->getRatio($value, $rowTotal[$index]); } if($showTotal == 'sum') { if($monopolize) $columnRow->{"sum_self_$uuName"} = (float)$rowTotal[$index]; $columnRow->{'sum_' . $uuName} = $this->getRatio($rowTotal[$index], $rowTotal[$index]); } $columnRows[$index] = $columnRow; } } if($showMode == 'column') { foreach($columnRows as $index => $row) { $columnRow = new stdclass(); foreach($row as $field => $value) { if(in_array($field, $groups)) { $columnRow->$field = $value; continue; } if($monopolize) $columnRow->{"self_$field"} = $value; $columnRow->{$field} = $this->getRatio($value, $colTotal[$field]); } if($showTotal == 'sum') { if($monopolize) $columnRow->{"sum_self_$uuName"} = (float)$rowTotal[$index]; $columnRow->{'sum_' . $uuName} = $this->getRatio($rowTotal[$index], $allTotal); } $columnRows[$index] = $columnRow; } } if($showMode == 'default' and $showTotal == 'sum') { foreach($columnRows as $index => $row) $row->{'sum_' . $uuName} = $rowTotal[$index]; } if($showColTotal == 'sum') { if(empty($columnRows)) return $columnRows; $colTotalRow = new stdClass(); foreach(reset($columnRows) as $field => $value) { if(in_array($field, $groups)) { $colTotalRow->$field = '$totalGroup$'; } else { if($showTotal == 'sum' and $field == "sum_$uuName") { if($showMode == 'default') { $colTotalRow->{$field} = $allTotal; } else { $colTotalRow->{$field} = $this->getRatio($allTotal, $allTotal); } continue; } if(strpos($field, 'sum_self_') !== false) { $colTotalRow->{$field} = $allTotal; continue; } if(strpos($field, 'self_') !== false) { $colTotalRow->{$field} = $colTotal[$field]; continue; } if($showMode == 'default') $colTotalRow->$field = $colTotal[$field]; if($showMode == 'column') $colTotalRow->$field = $this->getRatio($colTotal[$field], $colTotal[$field]); if(strpos(',total,row,', ",$showMode,") !== false) $colTotalRow->$field = $this->getRatio($colTotal[$field], $allTotal); } } $groupKey = $this->getGroupsKey($groups, $colTotalRow); $columnRows[$groupKey] = $colTotalRow; } return $columnRows; } /** * 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[] = $record->$group; return implode('_', $groupsKey); } /** * 处理表格的切片数据。 * Process data as slice table data. * * @param array $columnRows * @param array $groups * @param string $slice * @param string $uuName * @access public * @return array */ public function processSliceData(array $columnRows, array $groups, string $slice, string $uuName): array { $sliceList = array(); foreach($columnRows as $rows) $sliceList[$rows->{$slice}] = $rows->{$slice}; $index = 0; $sliceRows = array(); foreach($columnRows as $key => $columnRow) { $field = $columnRow->{$slice} . '_slice_' . $uuName; $columnRow->{$field} = $columnRow->{$uuName}; if(!in_array($slice, $groups)) unset($columnRow->{$slice}); unset($columnRow->{$uuName}); if($key == 0) { $index = $key; $sliceRows[$index] = $columnRow; continue; } $sliceFlag = true; foreach($groups as $group) { if($columnRow->{$group} != $sliceRows[$index]->{$group}) $sliceFlag = false; } if(!$sliceFlag) { $index ++; $sliceRows[$index] = $columnRow; } else { $sliceRows[$index]->{$field} = $columnRow->{$field}; } } foreach($sliceRows as $key => $row) { $sliceRow = array(); foreach($row as $field => $value) { if(strpos($field, '_slice_' . $uuName) === false) $sliceRow[$field] = $value; } foreach($sliceList as $field) { $field = $field . '_slice_' . $uuName; $sliceRow[$field] = !empty($row->{$field}) ? $row->{$field} : ''; } $sliceRows[$key] = (object)$sliceRow; } return $sliceRows; } /** * Get sys options. * * @param string $type * @param string $object * @param string $field * @access public * @return array */ public function getSysOptions($type, $object = '', $field = '', $source = '', $saveAs = '') { if(in_array($type, array('user', 'product', 'project', 'execution', 'dept', 'project.status'))) return $this->bi->getScopeOptions($type); if(!$field) return array(); $options = array(); switch($type) { case 'option': $options = $this->bi->getDataviewOptions($object, $field); break; case 'object': if(is_array($source)) { $options = array(); foreach($source as $row) $options[$row->id] = $row->$field; } else { $options = $this->bi->getObjectOptions($object, $field); } break; case 'string': case 'number': if($source) { $options = array(); if(is_array($source)) { foreach($source as $row) $options["{$row->$field}"] = $row->$field; } else { $keyField = $field; $valueField = $saveAs ? $saveAs : $field; $options = $this->bi->getOptionsFromSql($source, $keyField, $valueField); } } break; } if(is_string($source) and $source and $saveAs and in_array($type, array('user', 'product', 'project', 'execution', 'dept', 'option', 'object'))) { $options = $this->bi->getOptionsFromSql($source, $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) { if(!isset($filter['default']) || empty($filter['default'])) continue; if($processDateVar && is_string($filter['default'])) $filter['default']= $this->processDateVar($filter['default']); } return $filters; } /** * 从透视表对象中获取字段。 * 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, string $sql): array { $options = array(); $sqlRecords = $this->dbh->query($sql)->fetchAll(); foreach($fieldSettings as $key => $fieldSetting) { $type = $fieldSetting['type']; $object = $fieldSetting['object']; $field = $fieldSetting['field']; $source = $sql; if(in_array($type, array('string', 'number', 'date'))) $source = $sqlRecords; $options[$key] = $this->getSysOptions($type, $object, $field, $source); } return $options; } /** * Build table use data and rowspan. * * @param object $data * @param array $configs * @param int $page * @access public * @return void * */ public function buildPivotTable($data, $configs, $page = 0) { $width = 128; /* Init table. */ $table = "
"; $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 .= ""; foreach($data->cols as $lineCols) { $table .= ""; foreach($lineCols as $col) { $isGroup = $col->isGroup; $thName = $col->label; $colspan = zget($col, 'colspan', 1); $rowspan = zget($col, 'rowspan', 1); if($isGroup) $thHtml = ""; else $thHtml = ""; $table .= $thHtml; } $table .= ""; } $table .= ""; /* Init table tbody. */ $table .= ""; $rowCount = 0; $useColumnTotal = (!empty($data->columnTotal) and $data->columnTotal === 'sum'); if($page) list($start, $end, $itemCount, $pageTotal) = $this->pagePivot($configs, $page, $useColumnTotal); for($i = 0; $i < count($data->array); $i ++) { $rowCount ++; if($page and ($i < $start or $i > $end)) continue; if($useColumnTotal and $rowCount == count($data->array)) continue; $line = array_values($data->array[$i]); $table .= ""; for($j = 0; $j < count($line); $j ++) { $isGroup = !empty($data->cols[0][$j]) ? $data->cols[0][$j]->isGroup : false; $rowspan = isset($configs[$i][$j]) ? $configs[$i][$j] : 1; $hidden = isset($configs[$i][$j]) ? false : (!$isGroup ? false : true); $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($isGroup) { $groupName = $data->cols[0][$j]->name; } if(is_numeric($lineValue)) $lineValue = round($lineValue, 2); if(!$hidden) $table .= ""; } $table .= ""; } /* Add column total. 如果分页了,只在最后一页展示 */ if($useColumnTotal and !empty($data->array) and (!$page or $page == $pageTotal)) { $table .= ""; $table .= ""; foreach(end($data->array) as $field => $total) { if(in_array($field, $data->groups)) continue; if(is_numeric($total)) $total = round($total, 2); $table .= ""; } $table .= ""; } $table .= ""; $table .= "
$thName$thName
$lineValue
{$this->lang->pivot->step2->total}$total
"; if($page) { $recTotal = $end - $start + 1; $leftPage = $page - 1; $rightPage = $page + 1; if($recTotal) $table .= $this->getTablePager($itemCount, $leftPage, $rightPage, $page, $pageTotal); } echo $table; } } /** * 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; }