dao->select($fields)->from(TABLE_PROJECT) ->where('type')->eq('project') ->beginIF($this->config->vision)->andWhere('vision')->eq($this->config->vision)->fi() ->andWhere('deleted')->eq(0) ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->projects)->fi() ->orderBy('order_asc,id_desc') ->fetchAll('id'); } /** * 查找项目执行下关联的产品 * Get linked products with execution under the project. * * @param array $executionIDs * * @access protected * @return array */ public function getExecutionProductGroup(array $executionIDs): array { return $this->dao->select('project,product')->from(TABLE_PROJECTPRODUCT) ->where('project')->in($executionIDs) ->fetchGroup('project', 'product'); } /** * 检查用户是否有查看项目的权限。 * Check the privilege. * * @param int $projectID * @access public * @return bool */ public function checkPriv(int $projectID): bool { if(empty($projectID)) return false; return $this->app->user->admin || str_contains(",{$this->app->user->view->projects},", ",{$projectID},"); } /** * Judge an action is clickable or not. * * @param object $project * @param string $action * @access public * @return bool */ public static function isClickable($project, $action) { $action = strtolower($action); if(empty($project)) return true; if(!isset($project->type)) return true; if($action == 'start') return $project->status == 'wait' or $project->status == 'suspended'; if($action == 'finish') return $project->status == 'wait' or $project->status == 'doing'; if($action == 'close') return $project->status != 'closed'; if($action == 'suspend') return $project->status == 'wait' or $project->status == 'doing'; if($action == 'activate') return $project->status == 'done' or $project->status == 'closed'; if($action == 'whitelist') return $project->acl != 'open'; if($action == 'group') return $project->model != 'kanban'; return true; } /** * Save project state. * * @param int $projectID * @param array $projects * @access public * @return int */ public function saveState($projectID = 0, $projects = array()) { if(defined('TUTORIAL')) return $projectID; if($projectID == 0 and $this->cookie->lastProject) $projectID = $this->cookie->lastProject; if($projectID == 0 and (int)$this->session->project == 0) $projectID = (int)key($projects); if($projectID == 0) $projectID = (int)key($projects); $this->session->set('project', (int)$projectID, $this->app->tab); if(!isset($projects[$this->session->project])) { if($projectID and strpos(",{$this->app->user->view->projects},", ",{$this->session->project},") === false and !empty($projects)) { /* Redirect old project to new project. */ $newProjectID = $this->dao->select('project')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetch('project'); if($newProjectID and strpos(",{$this->app->user->view->projects},", ",{$newProjectID},") !== false) { $this->session->set('project', (int)$newProjectID, $this->app->tab); return $this->session->project; } $this->session->set('project', (int)key($projects), $this->app->tab); print($this->projectTao->accessDenied()); } else { $this->session->set('project', (int)key($projects), $this->app->tab); } } setcookie('lastProject', (string)$this->session->project, $this->config->cookieLife, $this->config->webRoot, '', $this->config->cookieSecure, true); return $this->session->project; } /** * Get budget unit list. * * @access public * @return array */ public function getBudgetUnitList() { $budgetUnitList = array(); if($this->config->vision != 'lite') { foreach(explode(',', $this->config->project->unitList) as $unit) $budgetUnitList[$unit] = zget($this->lang->project->unitList, $unit, ''); } return $budgetUnitList; } /** * Get Multiple linked products for project. * * @param int $projectID * @access public * @return array */ public function getMultiLinkedProducts($projectID) { $linkedProducts = $this->dao->select('product')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($projectID)->fetchPairs(); return $this->dao->select('t3.id,t3.name')->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id') ->leftJoin(TABLE_PRODUCT)->alias('t3')->on('t1.product = t3.id') ->where('t1.product')->in($linkedProducts) ->andWhere('t1.project')->ne($projectID) ->andWhere('t2.type')->eq('project') ->andWhere('t2.deleted')->eq('0') ->andWhere('t3.deleted')->eq('0') ->fetchPairs('id', 'name'); } /* * Get project swapper. * * @param int $projectID * @param string $currentModule * @param string $currentMethod * @access public * @return string */ public function getSwitcher($projectID, $currentModule, $currentMethod) { if($currentModule == 'project' and $currentMethod == 'browse') return; $currentProjectName = $this->lang->project->common; if($projectID) { $currentProject = $this->getById($projectID); $currentProjectName = $currentProject->name; } if($this->app->viewType == 'mhtml' and $projectID) { $output = $this->lang->project->common . $this->lang->colon; $output .= "{$currentProjectName} "; return $output; } $dropMenuLink = helper::createLink('project', 'ajaxGetDropMenu', "objectID=$projectID&module=$currentModule&method=$currentMethod"); $output = "
"; return $output; } /** * 根据项目ID获取项目信息。 * Get a project by id. * * @param int $projectID * @access public * @return object|false */ public function getByID(int $projectID): object|false { /* Using demo data during tutorials. */ if(defined('TUTORIAL')) return $this->loadModel('tutorial')->getProject(); /* Get project info. */ $project = $this->projectTao->fetchProjectInfo($projectID); if(!$project) return false; /* Replace image url. */ $project = $this->loadModel('file')->replaceImgURL($project, 'desc'); return $project; } /** * 通过影子产品ID获取一条项目记录。 * Get a project by its shadow product. * * @param int $productID * @access public * @return object|false */ public function getByShadowProduct(int $productID): object|false { return $this->dao->select('t2.*')->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id') ->where('t1.product')->eq($productID) ->andWhere('t2.type')->eq('project') ->limit(1) ->fetch(); } /** * 根据状态和和我参与的查询项目列表。 * Get project list by status and with my participation. * * @param string $status * @param string $orderBy * @param bool $involved * @param int $pager * @access public * @return array */ public function getList(string $status = 'undone', string $orderBy = 'order_desc', bool $involved = false, object|null $pager = null): array { /* Get project list by status. */ $projects = $this->projectTao->fetchProjectList($status, $orderBy, $involved, $pager); if(empty($projects)) return array(); /* Get team members and estimates under the project. */ $projectIdList = array_keys($projects); $teamCount = $this->projectTao->fetchMemberCountByIdList($projectIdList); $estimates = $this->projectTao->fetchTaskEstimateByIdList($projectIdList, 'estimate'); /* Set project attribute. */ $this->app->loadClass('pager', true); foreach($projects as $projectID => $project) { $orderBy = $project->model == 'waterfall' ? 'id_asc' : 'id_desc'; $pager = $project->model == 'waterfall' ? null : new pager(0, 1, 1); $project->executions = $this->loadModel('execution')->getStatData($projectID, 'undone', 0, 0, false, '', $orderBy, $pager); $project->teamCount = isset($teamCount[$projectID]) ? $teamCount[$projectID] : 0; $project->estimate = isset($estimates[$projectID]) ? round($estimates[$projectID]->estimate, 2) : 0; $project->parentName = $project->parent ? $this->projectTao->getParentProgram($project->path, $project->grade) : ''; } return $projects; } /** * Get project overview for block. * * @param string $status * @param int $projectID * @param string $orderBy * @param int $limit * @param string $excludedModel * @access public * @return array */ public function getOverviewList(string $status = '', int $projectID = 0, string $orderBy = 'id_desc', int $limit = 15, string $excludedModel = ''): array { /* Get project list by query. */ $projects = $this->projectTao->fetchProjectListByQuery($status, $projectID, $orderBy, $limit, $excludedModel); if(empty($projects)) return array(); /* Get team members under the project. */ $projectIdList = array_keys($projects); $teamCount = $this->projectTao->fetchMemberCountByIdList($projectIdList); /* Get all consumed and all estimate under the project. */ $hours = $this->projectTao->fetchTaskEstimateByIdList($projectIdList, 'consumed,estimate'); /* Get bug, task and story summary under the project. */ $bugSummary = $this->projectTao->getTotalBugByProject($projectIdList); $taskSummary = $this->projectTao->getTotalTaskByProject($projectIdList); $storySummary = $this->projectTao->getTotalStoriesByProject($projectIdList); /* Set project attribute. */ foreach($projects as $projectID => $project) { $project->teamCount = zget($teamCount, $projectID, 0); $project->consumed = isset($hours[$projectID]) ? round((float)$hours[$projectID]->consumed, 1) : 0; $project->estimate = isset($hours[$projectID]) ? round((float)$hours[$projectID]->estimate, 1) : 0; $project->leftBugs = isset($bugSummary[$projectID]) ? $bugSummary[$projectID]->leftBugs : 0; $project->allBugs = isset($bugSummary[$projectID]) ? $bugSummary[$projectID]->allBugs : 0; $project->doneBugs = isset($bugSummary[$projectID]) ? $bugSummary[$projectID]->doneBugs : 0; $project->allStories = isset($storySummary[$projectID]) ? $storySummary[$projectID]->allStories : 0; $project->doneStories = isset($storySummary[$projectID]) ? $storySummary[$projectID]->doneStories : 0; $project->leftStories = isset($storySummary[$projectID]) ? $storySummary[$projectID]->leftStories : 0; $project->leftTasks = isset($taskSummary[$projectID]) ? $taskSummary[$projectID]->leftTasks : 0; $project->allTasks = isset($taskSummary[$projectID]) ? $taskSummary[$projectID]->allTasks : 0; $project->waitTasks = isset($taskSummary[$projectID]) ? $taskSummary[$projectID]->waitTasks : 0; $project->doingTasks = isset($taskSummary[$projectID]) ? $taskSummary[$projectID]->doingTasks : 0; $project->rndDoneTasks = isset($taskSummary[$projectID]) ? $taskSummary[$projectID]->doneTasks : 0; $project->liteDoneTasks = isset($taskSummary[$projectID]) ? $taskSummary[$projectID]->litedoneTasks : 0; } return $projects; } /** * Get waterfall project progress. * * @param array $projectIDList * @access public * @return int */ public function getWaterfallProgress($projectIDList) { $projectList = $this->dao->select('t1.*')->from(TABLE_EXECUTION)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id') ->where('t1.type')->in('stage') ->andWhere('t1.deleted')->eq('0') ->andWhere('t1.vision')->eq($this->config->vision) ->andWhere('t1.project')->in($projectIDList) ->andWhere('t2.model')->eq('waterfall') ->fetchGroup('project', 'id'); $totalHour = $this->dao->select("t1.project, t1.execution, ROUND(SUM(if(t1.status !='closed' && t1.status !='cancel', `left`, 0)), 2) AS totalLeft, ROUND(SUM(consumed), 1) AS totalConsumed")->from(TABLE_TASK)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.execution = t2.id') ->where('t2.project')->in(array_keys($projectList)) ->andWhere('t1.deleted')->eq(0) ->andWhere('t1.parent')->lt(1) ->groupBy('t1.project,t1.execution') ->fetchGroup('project', 'execution'); $progressList = array(); foreach($projectList as $projectID => $stageList) { $progress = 0; $projectConsumed = 0; $projectLeft = 0; foreach($stageList as $stageID => $stage) { if($stage->project != $projectID) continue; $stageTotalConsumed = isset($totalHour[$projectID][$stageID]) ? $totalHour[$projectID][$stageID]->totalConsumed : 0; $stageTotalLeft = isset($totalHour[$projectID][$stageID]) ? round($totalHour[$projectID][$stageID]->totalLeft, 1) : 0; $projectConsumed += $stageTotalConsumed; $projectLeft += $stageTotalLeft; } $progress += ($projectConsumed + $projectLeft) == 0 ? 0 : floor($projectConsumed / ($projectConsumed + $projectLeft) * 1000) / 1000 * 100; $progressList[$projectID] = $progress; } if(!is_numeric($projectIDList)) return $progressList; return empty($progressList) ? 0 : $progressList[$projectIDList]; } /** * Get waterfall general PV and EV. * * @param int $projectID * @access public * @return array */ public function getWaterfallPVEVAC($projectID) { $executions = $this->dao->select('id,begin,end,realEnd,status')->from(TABLE_EXECUTION)->where('deleted')->eq(0)->andWhere('vision')->eq($this->config->vision)->andWhere('project')->eq($projectID)->fetchAll('id'); $stmt = $this->dao->select('id,status,estimate,consumed,`left`,closedReason')->from(TABLE_TASK)->where('execution')->in(array_keys($executions))->andWhere("parent")->ge(0)->andWhere("deleted")->eq(0)->andWhere('status')->ne('cancel')->query(); $PV = 0; $EV = 0; $left = 0; while($task = $stmt->fetch()) { $PV += $task->estimate; $left += $task->left; if($task->status == 'done' or $task->closedReason == 'done') { $EV += $task->estimate; } else { $task->progress = 0; if(($task->consumed + $task->left) > 0) $task->progress = round($task->consumed / ($task->consumed + $task->left) * 100, 2); $EV += round($task->estimate * $task->progress / 100, 2); } } $AC = $this->dao->select('SUM(consumed) as consumed')->from(TABLE_EFFORT) ->where('deleted')->eq(0) ->andWhere('project')->eq($projectID) ->fetch('consumed'); if(is_null($AC)) $AC = 0; return array('PV' => sprintf("%.2f", $PV), 'EV' => sprintf("%.2f", $EV), 'AC' => sprintf("%.2f", $AC), 'left' => sprintf("%.2f", $left)); } /** * Get project workhour info. * * @param int $projectID * @access public * @return object */ public function getWorkhour($projectID) { $total = $this->dao->select('ROUND(SUM(estimate), 1) AS totalEstimate, ROUND(SUM(`left`), 2) AS totalLeft')->from(TABLE_TASK)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.execution = t2.id') ->where('t2.project')->in($projectID) ->andWhere('t2.deleted')->eq(0) ->andWhere('t1.deleted')->eq(0) ->andWhere('t1.parent')->lt(1) ->fetch(); $totalConsumed = $this->dao->select('ROUND(SUM(consumed), 1) AS totalConsumed')->from(TABLE_TASK)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.execution = t2.id') ->where('t2.project')->in($projectID) ->andWhere('t2.deleted')->eq(0) ->andWhere('t1.deleted')->eq(0) ->andWhere('t1.parent')->lt(1) ->fetch('totalConsumed'); $closedTotalLeft = $this->dao->select('ROUND(SUM(`left`), 2) AS totalLeft')->from(TABLE_TASK)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.execution = t2.id') ->where('t2.project')->in($projectID) ->andWhere('t2.deleted')->eq(0) ->andWhere('t1.deleted')->eq(0) ->andWhere('t1.parent')->lt(1) ->andWhere('t1.status')->in('closed,cancel') ->fetch('totalLeft'); $workhour = new stdclass(); $workhour->totalHours = $this->dao->select('sum(t1.days * t1.hours) AS totalHours')->from(TABLE_TEAM)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.root=t2.id') ->where('t2.id')->in($projectID) ->andWhere('t2.deleted')->eq(0) ->andWhere('t1.type')->eq('project') ->fetch('totalHours'); $workhour->totalEstimate = $total->totalEstimate; $workhour->totalConsumed = $totalConsumed; $workhour->totalLeft = round($total->totalLeft - $closedTotalLeft, 1); return $workhour; } /** * Get projects consumed info. * * @param array $projectID * @param string $time * @access public * @return array */ public function getProjectsConsumed($projectIdList, $time = '') { $projects = array(); $totalConsumeds = $this->dao->select('t2.project,ROUND(SUM(t1.consumed), 1) AS totalConsumed')->from(TABLE_EFFORT)->alias('t1') ->leftJoin(TABLE_TASK)->alias('t2')->on("t1.objectID=t2.id and t1.objectType = 'task'") ->where('t2.project')->in($projectIdList) ->beginIF($time == 'THIS_YEAR')->andWhere('LEFT(t1.`date`, 4)')->eq(date('Y'))->fi() ->andWhere('t2.deleted')->eq(0) ->andWhere('t2.parent')->lt(1) ->groupBy('t2.project') ->fetchAll('project'); foreach($projectIdList as $projectID) { $project = new stdClass(); $project->totalConsumed = isset($totalConsumeds[$projectID]->totalConsumed) ? $totalConsumeds[$projectID]->totalConsumed : 0; $projects[$projectID] = $project; } return $projects; } /** * 生成项目下拉框跳转链接 * Create the link from module,method. * * @param string $module * @param string $method * @param int $projectID * @access public * @return string */ public function getProjectLink(string $module, string $method, int $projectID) :string { $link = helper::createLink('project', 'index', "projectID=%s"); $project = $this->projectTao->fetchProjectInfo($projectID); if(empty($project->multiple)) return $link; if($module == 'build' && $method !== 'create') { $fromModule = $this->app->tab == 'project' ? 'projectbuild' : 'project'; $fromMethod = $this->app->tab == 'project' ? 'browse' : 'build'; return helper::createLink($fromModule, $fromMethod, "projectID=%s"); } if(!empty($this->config->project->linkMap->$module[$method])) { $linkParams = $this->config->project->linkMap->$module[$method]; if(!$linkParams[0]) $linkParams[0] = $module; if(!$linkParams[1]) $linkParams[1] = $method; return helper::createLink($linkParams[0], $linkParams[1], $linkParams[2]) . $linkParams[3]; } if(!empty($this->config->project->linkMap->$module[''])) { $linkParams = $this->config->project->linkMap->$module['']; if(!$linkParams[0]) $linkParams[0] = $module; return helper::createLink($linkParams[0], $linkParams[1], $linkParams[2]) . $linkParams[3]; } if(in_array($module, $this->config->waterfallModules)) { return helper::createLink($module, 'browse', "projectID=%s"); } return $link; } /** * Get project stat data . * * @param int $projectID * @access public * @return object */ public function getStatData($projectID) { $executions = $this->loadModel('execution')->getPairs($projectID); $storyTypeCount = $this->dao->select('count(t2.story) as storyCount,t1.type')->from(TABLE_STORY)->alias('t1') ->leftJoin(TABLE_PROJECTSTORY)->alias('t2')->on('t1.id = t2.story') ->where('t2.project')->eq($projectID) ->andWhere('t1.deleted')->eq(0) ->groupBy('t1.type') ->fetchPairs('type', 'storyCount'); $storyCount = isset($storyTypeCount['story']) ? $storyTypeCount['story'] : 0; $requirementCount = isset($storyTypeCount['requirement']) ? $storyTypeCount['requirement'] : 0; $taskCount = $this->dao->select('count(id) as taskCount')->from(TABLE_TASK)->where('execution')->in(array_keys($executions))->andWhere('deleted')->eq(0)->fetch('taskCount'); $bugCount = $this->dao->select('count(id) as bugCount')->from(TABLE_BUG)->where('project')->in($projectID)->andWhere('deleted')->eq(0)->fetch('bugCount'); $statusPairs = $this->dao->select('status,count(id) as count')->from(TABLE_TASK)->where('execution')->in(array_keys($executions))->andWhere('deleted')->eq(0)->groupBy('status')->fetchPairs('status', 'count'); $finishedCount = $this->dao->select('count(id) as taskCount')->from(TABLE_TASK)->where('execution')->in(array_keys($executions))->andWhere('finishedBy')->ne('')->andWhere('deleted')->eq(0)->fetch('taskCount'); $delayedCount = $this->dao->select('count(id) as count')->from(TABLE_TASK) ->where('execution')->in(array_keys($executions)) ->andWhere('deadline')->notZeroDate() ->andWhere('deadline')->lt(helper::today()) ->andWhere('status')->in('wait,doing') ->andWhere('deleted')->eq(0) ->fetch('count'); $statData = new stdclass(); $statData->storyCount = $storyCount; $statData->requirementCount = $requirementCount; $statData->taskCount = $taskCount; $statData->bugCount = $bugCount; $statData->waitCount = zget($statusPairs, 'wait', 0); $statData->doingCount = zget($statusPairs, 'doing', 0); $statData->finishedCount = $finishedCount; $statData->delayedCount = $delayedCount; return $statData; } /** * Get project pairs. * * @access public * @return object */ public function getPairs() { return $this->dao->select('id, name')->from(TABLE_PROJECT) ->where('type')->eq('project') ->andWhere('deleted')->eq(0) ->andWhere('vision')->eq($this->config->vision) ->fetchPairs(); } /** * Get project pairs by programID. * * @param int $programID * @param status $status all|wait|doing|suspended|closed|noclosed * @param bool $isQueryAll * @param string $orderBy * @param string $excludedModel * @param string|array $model * @param string $param multiple|product * @access public * @return object */ public function getPairsByProgram($programID = '', $status = 'all', $isQueryAll = false, $orderBy = 'order_asc', $excludedModel = '', $model = '', $param = '') { if(defined('TUTORIAL')) return $this->loadModel('tutorial')->getProjectPairs(); return $this->dao->select('id, name')->from(TABLE_PROJECT) ->where('type')->eq('project') ->andWhere('deleted')->eq(0) ->andWhere('vision')->eq($this->config->vision) ->beginIF(!empty($programID))->andWhere('path')->like("%,$programID,%")->fi() ->beginIF($programID === 0)->andWhere('parent')->eq(0)->fi() ->beginIF($status != 'all' and $status != 'noclosed')->andWhere('status')->eq($status)->fi() ->beginIF($excludedModel)->andWhere('model')->ne($excludedModel)->fi() ->beginIF($model)->andWhere('model')->in($model)->fi() ->beginIF(strpos($param, 'multiple') !== false)->andWhere('multiple')->eq(1)->fi() ->beginIF(strpos($param, 'product') !== false)->andWhere('hasProduct')->eq(1)->fi() ->beginIF($status == 'noclosed')->andWhere('status')->ne('closed')->fi() ->beginIF(!$this->app->user->admin and !$isQueryAll)->andWhere('id')->in($this->app->user->view->projects)->fi() ->orderBy($orderBy) ->fetchPairs(); } /** * Get all the projects under the program set to which an project belongs. * * @param object $project * @access public * @return void */ public function getBrotherProjects($project) { if($project->parent == 0) return array($project->id => $project->id); $projectIds = array_filter(explode(',', $project->path)); $parentProgram = $this->dao->select('*')->from(TABLE_PROGRAM) ->where('id')->in($projectIds) ->andWhere('`type`')->eq('program') ->orderBy('grade desc') ->fetch(); return $this->dao->select('id')->from(TABLE_PROJECT) ->where('type')->eq('project') ->andWhere('deleted')->eq(0) ->andWhere('path')->like("{$parentProgram->path}%") ->fetchPairs('id'); } /** * Get project by id list. * * @param array $projectIdList * @param string $mode all * @access public * @return object */ public function getByIdList($projectIdList = array(), $mode = '') { return $this->dao->select('*')->from(TABLE_PROJECT) ->where('type')->eq('project') ->andWhere('id')->in($projectIdList) ->beginIF(!$this->app->user->admin and $mode != 'all')->andWhere('id')->in($this->app->user->view->projects)->fi() ->fetchAll('id'); } /** * Get project pairs by id list. * * @param array $projectIdList * @param string $model * @param string $param * @access public * @return array */ public function getPairsByIdList($projectIdList = array(), $model = '', $param = '') { return $this->dao->select('id, name')->from(TABLE_PROJECT) ->where('type')->eq('project') ->andWhere('deleted')->eq(0) ->beginIF($projectIdList)->andWhere('id')->in($projectIdList)->fi() ->beginIF(!$this->app->user->admin and $model != 'all')->andWhere('id')->in($this->app->user->view->projects)->fi() ->beginIF($model != 'all' and !empty($model))->andWhere('model')->in($model)->fi() ->beginIF(strpos($param, 'multiple') !== false)->andWhere('multiple')->eq('1')->fi() ->fetchPairs('id', 'name'); } /** * 获取可复制项目的键值对。 * get the copy project pairs. * * @param string $name * @param string $model * * @access public * @return array */ public function getCopyProjectPairs(string $name, string $model = 'kanban'): array { $projectPairs = $this->dao->select('id, name')->from(TABLE_PROJECT) ->where('type')->eq('project') ->andWhere('deleted')->eq(0) ->andWhere('vision')->eq($this->config->vision) ->andWhere('model')->eq($model) ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->projects)->fi() ->beginIF(trim($name))->andWhere('name')->like("%$name%")->fi() ->fetchPairs(); return array_keys($projectPairs); } /** * 根据项目ID获取关联产品及分支 * Get branches by project id. * * @param int $projectID * * @access public * @return array */ public function getBranchesByProject(int $projectID): array { return $this->dao->select('*')->from(TABLE_PROJECTPRODUCT) ->where('project')->eq($projectID) ->fetchGroup('product', 'branch'); } /** * 根据项目ID获取分支分组。 * Get branch groups. * * @param int $projectID * @param array $productIdList * * @access public * @return array */ public function getBranchGroup(int $projectID, array $productIdList): array { return $this->dao->select('t1.product as productID, t1.branch as branchID, t2.*') ->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id') ->where('t1.product')->in($productIdList) ->andWhere('t2.deleted')->eq(0) ->andWhere('t2.project')->eq($projectID) ->fetchGroup('productID', 'branchID'); } /** * Get No product project|execution List. * * @access public * @return array */ public function getNoProductList() { return $this->dao->select('t1.product, t2.*')->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id') ->where('t2.hasProduct')->eq(0) ->andWhere('t2.deleted')->eq(0) ->fetchAll('id'); } /** * 获取项目与执行的键值对 * Get project and execution pairs. * * @access public * @return array */ public function getProjectExecutionPairs(): array { return $this->dao->select('project, id')->from(TABLE_PROJECT) ->where('multiple')->eq('0') ->andWhere('deleted')->eq('0') ->fetchPairs('id', 'project'); } /** * 根据项目类型生成权限数据。 * Get project priv data according by the project type. * * @param string $model scrum|waterfall|noSprint|agileplus|waterfallplus * @access public * @return object|false */ public function getPrivsByModel(string $model = 'waterfall'): object|false { if(!isset($this->config->programPriv->$model)) return false; if($model == 'noSprint') $this->config->project->includedPriv = $this->config->project->noSprintPriv; $this->app->loadLang('group'); $privs = new stdclass(); foreach($this->lang->resource as $module => $methods) { if(empty($methods)) continue; if(!in_array($module, $this->config->programPriv->$model)) continue; foreach($methods as $method => $label) { if(isset($this->config->project->includedPriv[$module]) and !in_array($method, $this->config->project->includedPriv[$module])) continue; if(!isset($privs->$module)) $privs->$module = new stdclass(); $privs->$module->$method = $label; } } return $privs; } /* * Build search form. * * @param int $queryID * @param string $actionURL * * @return 0 * */ public function buildSearchForm($queryID, $actionURL) { $this->config->project->search['queryID'] = $queryID; $this->config->project->search['actionURL'] = $actionURL; $statusList = $this->lang->project->statusList; unset($statusList['delay']); $this->config->project->search['params']['status']['values'] = $statusList; $programPairs = array(0 => ''); $programPairs += $this->loadModel('program')->getPairs(); $this->config->project->search['params']['parent']['values'] = $programPairs; if(!isset($this->config->setCode) or $this->config->setCode == 0) unset($this->config->project->search['fields']['code'], $this->config->project->search['params']['code']); if($this->config->systemMode == 'light') unset($this->config->project->search['fields']['parent'], $this->config->project->search['params']['parent']); $this->loadModel('search')->setSearchParams($this->config->project->search); } /** * Build the query. * * @param int $projectID * @access public * @return object */ public function buildMenuQuery($projectID = 0) { $path = ''; $project = $this->projectTao->fetchProjectInfo($projectID); if($project) $path = $project->path; return $this->dao->select('*')->from(TABLE_PROJECT) ->where('deleted')->eq('0') ->andWhere('type')->eq('program')->fi() ->andWhere('status')->ne('closed') ->andWhere('id')->in($this->app->user->view->programs)->fi() ->beginIF($projectID > 0)->andWhere('path')->like($path . '%')->fi() ->orderBy('grade desc, `order`') ->get(); } /** * Build project build search form. * * @param array $products * @param int $queryID * @param string $actionURL * @param string $type project|execution * @access public * @return void */ public function buildProjectBuildSearchForm($products, $queryID, $actionURL, $type = 'project') { $this->loadModel('build'); /* Set search param. */ if($type == 'execution') $this->config->build->search['module'] = 'executionBuild'; if($type == 'project') $this->config->build->search['module'] = 'projectBuild'; $this->config->build->search['actionURL'] = $actionURL; $this->config->build->search['queryID'] = $queryID; $this->config->build->search['params']['product']['values'] = $products; $this->loadModel('search')->setSearchParams($this->config->build->search); } /** * Get project pairs by model and project. * * @param string $model all|scrum|waterfall|kanban * @param int $programID * @param string $param noclosed * @param string|int|array $append * @access public * @return array */ public function getPairsByModel($model = 'all', $programID = 0, $param = '', $append = '', $orderBy = 'order_asc') { if(defined('TUTORIAL')) return $this->loadModel('tutorial')->getProjectPairs(); if($model == 'agileplus') $model = array('scrum', 'agileplus'); if($model == 'waterfallplus') $model = array('waterfall', 'waterfallplus'); $projects = $this->dao->select('id, name, path')->from(TABLE_PROJECT) ->where('type')->eq('project') ->andWhere('deleted')->eq('0') ->beginIF($programID)->andWhere('parent')->eq($programID)->fi() ->beginIF($this->config->vision)->andWhere('vision')->eq($this->config->vision)->fi() ->beginIF($model != 'all')->andWhere('model')->in($model)->fi() ->beginIF(strpos($param, 'noclosed') !== false)->andWhere('status')->ne('closed')->fi() ->beginIF(strpos($param, 'multiple') !== false)->andWhere('multiple')->eq('1')->fi() ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->projects)->fi() ->beginIF(!empty($append))->orWhere('id')->in($append)->fi() ->orderBy($orderBy) ->fetchAll(); $programIdList = array(); $projectProgram = array(); foreach($projects as $project) { list($programID) = explode(',', trim($project->path, ',')); $programIdList[$programID] = $programID; $projectProgram[$project->id] = $programID; } $programs = $this->dao->select('id, name')->from(TABLE_PROGRAM)->where('id')->in($programIdList)->orderBy($orderBy)->fetchPairs('id', 'name'); /* Sort by project order in the program list. */ $allProjects = array(); foreach($programs as $programID => $program) $allProjects[$programID] = array(); foreach($projects as $project) { $programID = zget($projectProgram, $project->id, ''); $allProjects[$programID][] = $project; } $pairs = array(); foreach($allProjects as $programID => $projects) { foreach($projects as $project) { $projectName = $project->name; if($this->config->systemMode == 'ALM') { $programID = zget($projectProgram, $project->id, ''); if($programID != $project->id) $projectName = zget($programs, $programID, '') . ' / ' . $projectName; } $pairs[$project->id] = $projectName; } } return $pairs; } /** * Get stories by project id. * * @param int $projectID * @access public * @return array */ public function getStoriesByProject($projectID = 0) { return $this->dao->select("t2.product, t2.branch, GROUP_CONCAT(t2.story) as storyIDList")->from(TABLE_STORY)->alias('t1') ->leftJoin(TABLE_PROJECTSTORY)->alias('t2')->on('t1.id=t2.story') ->where('t1.deleted')->eq(0) ->beginIF($projectID)->andWhere('t2.project')->eq($projectID)->fi() ->groupBy('t2.product, t2.branch') ->fetchGroup('product', 'branch'); } /** * Get the tree menu of project. * * @param int $projectID * @param string $userFunc * @param int $param * @access public * @return string */ public function getTreeMenu($projectID = 0, $userFunc = '', $param = 0) { $projectMenu = array(); $stmt = $this->dbh->query($this->buildMenuQuery($projectID)); while($project = $stmt->fetch()) { $linkHtml = call_user_func($userFunc, $project, $param); if(isset($projectMenu[$project->id]) and !empty($projectMenu[$project->id])) { if(!isset($projectMenu[$project->parent])) $projectMenu[$project->parent] = ''; $projectMenu[$project->parent] .= "
  • $linkHtml"; $projectMenu[$project->parent] .= "\n"; } else { if(isset($projectMenu[$project->parent]) and !empty($projectMenu[$project->parent])) { $projectMenu[$project->parent] .= "
  • $linkHtml\n"; } else { $projectMenu[$project->parent] = "
  • $linkHtml\n"; } } $projectMenu[$project->parent] .= "
  • \n"; } krsort($projectMenu); $projectMenu = array_pop($projectMenu); return $projectMenu; } /** * 根据项目ID获取项目集列表。 * Get the program tree of project. * * @param int $projectID * @access public * @return array */ public function getProgramTree(int $projectID = 0): array { $programs = array(); $stmt = $this->dbh->query($this->buildMenuQuery($projectID)); while($project = $stmt->fetch()) { $program = new stdClass(); $program->id = $project->id; $program->name = $project->name; $program->parent = $project->parent; $program->url = helper::createLink('project', 'browse', "programID={$program->id}"); $programs[] = $program; } return $programs; } /** * Create the manage link. * * @param object $project * @access public * @return string */ public function createManageLink($project) { $link = $project->type == 'program' ? helper::createLink('project', 'browse', "projectID={$project->id}&status=all") : helper::createLink('project', 'index', "projectID={$project->id}", '', '', $project->id); if($this->app->rawModule == 'execution') $link = helper::createLink('execution', 'all', "status=all&projectID={$project->id}"); return html::a($link, $project->name, '_self', "id=program{$project->id} title='{$project->name}' class='text-ellipsis'"); } /** * 创建项目后,新增团队成员. * Add team members after create a project. * * @param int $projectID * @param object $project * @param array $members * @access protected * @return bool */ protected function addTeamMembers(int $projectID, object $project, array $members): array { /* Set team of project. */ array_push($members, $project->PM, $project->openedBy); $members = array_unique($members); $roles = $this->loadModel('user')->getUserRoles(array_values($members)); $teamMembers = array(); $member = new stdClass(); $member->root = $projectID; $member->type = 'project'; $member->join = helper::now(); $member->days = zget($project, 'days', 0); $member->hours = $this->config->loadModel('execution')->defaultWorkhours; foreach($members as $account) { if(empty($account)) continue; $member->account = $account; $member->role = zget($roles, $account, ''); $this->dao->insert(TABLE_TEAM)->data($member)->exec(); $teamMembers[$account] = $member; } $this->execution->addProjectMembers($projectID, $teamMembers); return !dao::isError(); } /** * Create a project. * * @param object $project * @param object $postData * @access public * @return int|bool */ public function create(object $project, object $postData): int|bool { $requiredFields = $this->config->project->create->requiredFields; if($this->post->delta == 999) $requiredFields = trim(str_replace(',end,', ',', ",{$requiredFields},"), ','); $project = $this->loadModel('file')->processImgURL($project, $this->config->project->editor->create['id'], $this->post->uid); $this->projectTao->doCreate($project); if(dao::isError()) return false; $projectID = $this->dao->lastInsertId(); /* Add project whitelist. */ $whitelist = explode(',', $project->whitelist); $this->loadModel('personnel')->updateWhitelist($whitelist, 'project', $projectID); $program = $project->parent ? $this->getByID((int)$project->parent) : new stdclass(); $this->projectTao->createDocLib($projectID, $project, $program); if($project->hasProduct) { $this->updateProducts($projectID); /* If $_POST has product name, create it. */ $linkedProductsCount = $this->projectTao->getLinkedProductsCount($project, $postData->rawdata); $needCreateProduct = !$project->hasProduct || isset($postData->rawdata->newProduct) || (!$project->parent && empty($linkedProductsCount)); if($needCreateProduct && !$this->projectTao->createProduct($projectID, $project, $postData, $program)) return false; } /* Save order. */ $this->dao->update(TABLE_PROJECT)->set('`order`')->eq($projectID * 5)->where('id')->eq($projectID)->exec(); $this->file->updateObjectID($this->post->uid, $projectID, 'project'); $this->loadModel('program')->setTreePath($projectID); /* Add project admin. */ $this->projectTao->addProjectAdmin($projectID); if($project->acl != 'open') $this->loadModel('user')->updateUserView($projectID, 'project'); if(empty($project->multiple) and $project->model != 'waterfall' and $project->model != 'waterfallplus') $this->loadModel('execution')->createDefaultSprint($projectID); return $projectID; } /** * 检查输入的$product和$branch变量是否合规。 * Check branch and product valid by project. * * @param int $parent * @param array $products * @param array $branch * @access public * @return bool */ public function checkBranchAndProduct(int $parent, array $products, array $branch): bool { $topProgramID = $this->loadModel('program')->getTopByID($parent); $multipleProducts = $this->loadModel('product')->getMultiBranchPairs($topProgramID); foreach($products as $index => $productID) { if(isset($multipleProducts[$productID]) and empty($branch[$index])) { dao::$errors[] = $this->lang->project->emptyBranch; return false; } } return true; } /** * 检查执行的起止日期是否小于项目的起止日期。 * Check if execution's start and end dates in project's start and end dates. * * @param int $projectID * @param object $project * @access public * @return bool */ public function checkDates($projectID, $project): bool { $executionsCount = $this->dao->select('COUNT(*) as count')->from(TABLE_PROJECT) ->where('project') ->eq($projectID) ->andWhere('deleted') ->eq('0') ->fetch('count'); if(empty($executionsCount))return true; $maxExecutionEnd = $this->dao->select('`end` as maxEnd')->from(TABLE_PROJECT) ->where('project')->eq($projectID) ->andWhere('deleted')->eq('0') ->orderBy('end_desc') ->fetch(); $minExecutionBegin = $this->dao->select('`begin` as minBegin')->from(TABLE_PROJECT) ->where('project')->eq($projectID) ->andWhere('deleted')->eq('0') ->orderBy('begin_asc') ->fetch(); if($maxExecutionEnd && $project->end < $maxExecutionEnd->maxEnd) dao::$errors['end'] = sprintf($this->lang->project->endGreateExecution, $maxExecutionEnd->maxEnd); if($minExecutionBegin && $project->begin > $minExecutionBegin->minBegin) dao::$errors['begin'] = sprintf($this->lang->project->begigLetterExecution, $minExecutionBegin->minBegin); return !dao::isError(); } /** * 更新项目关联的团队成员列表。 * update teammembers while update project. * * @param object $project * @param object $oldProject * @param array $newMembers * @access public * @return bool */ public function updateTeamMembers(object $project, object $oldProject, array $newMembers): bool { /* Get old project's team and roles. */ array_push($newMembers, $project->PM); $newMembers = array_unique($newMembers); $roles = $this->user->getUserRoles(array_values($newMembers)); $projectID = $oldProject->id; $oldMembers = $this->loadModel('user')->getTeamMemberPairs($projectID, 'project'); /* Delete members while old model is kanban. */ if($oldProject->model == 'kanban') { $deleteMembers = array_diff(array_keys($oldMembers), array_values($newMembers)); $this->projectTao->deleteMembers($projectID, $oldProject->openedBy, $deleteMembers); } /* Init member default for update members. */ $member = new stdclass(); $member->type = 'project'; $member->root = $projectID; $member->join = helper::today(); $member->days = zget($project, 'days', 0); $member->hours = $this->config->execution->defaultWorkhours; /* Prepare $addMembers for addProjectMembers(). */ $addMembers = array(); foreach($newMembers as $account) { if(empty($account) or isset($oldMembers[$account])) continue; $member->account = $account; $member->role = zget($roles, $account, ''); $addMembers[$account] = $member; } /* Add members. */ if(!empty(count($addMembers))) $this->loadModel('execution')->addProjectMembers($projectID, $addMembers); return !dao::isError(); } /** * 更新此项目下或影子产品下的白名单列表。 * Update whitelist by project. * * @param object $project * @param object $oldProject * @access public * @return bool */ public function updateWhitelist(object $project, object $oldProject): bool { /* 对比新旧白名单检查是否需要更新白名单。*/ /* Check if whitelist shoud update .*/ $projectID = $oldProject->id; $whitelist = array_filter(explode(',', $project->whitelist)); $oldWhitelist = array_filter(explode(',', $oldProject->whitelist)); if(count($oldWhitelist) != count($whitelist) || !empty(array_diff($oldWhitelist, $whitelist))) { if(!$oldProject->hasProduct) { $linkedProducts = $this->dao->select('product')->from(TABLE_PROJECTPRODUCT) ->where('project')->eq($projectID) ->fetchPairs(); $this->loadModel('personnel')->updateWhitelist($whitelist, 'product', current($linkedProducts)); } else { $this->loadModel('personnel')->updateWhitelist($whitelist, 'project', $projectID); } } return !dao::isError(); } /** * 更新项目的影子产品的状态。 * Update shadow product's status. * * @param int $projectID * @param string $status * @access public * @return bool */ public function updateShadowProductStatus(int $projectID, string $status): bool { /* Activate or close the shadow product. */ $productID = $this->loadModel('product')->getProductIDByProject($projectID); if($status == 'doing') $this->product->activate($productID); if($status == 'closed') $this->product->close($productID); return !dao::isError(); } /** * 更新用户视图。 * Update user view. * * @param int $projectID * @param string $acl * @access public * @return bool */ public function updateUserView(int $projectID, string $acl): bool { if($acl == 'open') return true; $this->loadModel('user')->updateUserView($projectID, 'project'); $executions = $this->dao->select('id')->from(TABLE_EXECUTION) ->where('project')->eq($projectID) ->fetchPairs(); if($executions) $this->user->updateUserView($executions, 'sprint'); return !dao::isError(); } /** * 更新项目下的所有产品的阶段。 * Update product stage by project. * * @param int $projectID * @param string $stageBy * @access public * @return bool */ public function updateProductStage(int $projectID, string $stageBy): bool { /* 如果项目的阶段为由按产品创建,则更新此项目下每个产品。*/ if($stageBy == 'project') return true; $executions = $this->loadModel('execution')->getPairs($projectID); foreach(array_keys($executions) as $executionID) $this->execution->updateProducts($executionID); // 更新项目下所有产品的阶段。 return !dao::isError(); } /** * 更新项目。 * Update project. * * @param object $project * @param object $oldProject * @param string $uid * @access public * @return array|false */ public function update(object $project, object $oldProject, string $uid): array|false { /* 通过主键查老项目信息, 处理父节点和图片字段。*/ /* Fetch old project's info and dispose parent and file info. */ $projectID = $oldProject->id; if(!isset($project->parent)) $project->parent = $oldProject->parent; $project = $this->loadModel('file')->processImgURL($project, $this->config->project->editor->edit['id'], $uid); /* 若此项目为多迭代项目, 检查起止日期不得小于迭代的起止日期。*/ /* If this project has multiple stage, check if execution's start and end dates in project's start and end dates. */ if($oldProject->multiple && !$this->checkDates($projectID, $project)) return false; /* 更新项目表。*/ /* Update project table. */ $success = $this->projectTao->doUpdate($projectID, $project); if(!$success) return false; /* 更新项目的关联信息。*/ /* Update relation info of this project. */ $this->updateUserView($projectID, $project->acl); // 更新用户视图。 $this->updateShadowProduct($project, $oldProject); // 更新影子产品关联信息。 $this->updateWhitelist($project, $oldProject); // 更新关联的白名单列表。 $this->updateProductStage($projectID, $oldProject->stageBy); // 更新关联的所有产品的阶段。 $this->file->updateObjectID($uid, $projectID, 'project'); // 通过uid更新文件id。 if($oldProject->parent != $project->parent) $this->loadModel('program')->fixPath($projectID, $project->parent, $oldProject->path, $oldProject->grade); // 更新项目从属路径。 if(empty($oldProject->multiple) and $oldProject->model != 'waterfall') $this->loadModel('execution')->syncNoMultipleSprint($projectID); // 无迭代的非瀑布项目需要更新。 if(dao::isError()) return false; return common::createChanges($oldProject, $project); } /** * Batch update projects. * * @access public * @return array */ public function batchUpdate() { $projects = array(); $allChanges = array(); $data = fixer::input('post')->get(); $oldProjects = $this->getByIdList($this->post->projectIdList); $nameList = array(); $extendFields = $this->getFlowExtendFields(); foreach($data->projectIdList as $projectID) { $projectID = (int)$projectID; $projectName = $data->names[$projectID]; if(isset($data->codes)) $projectCode = $data->codes[$projectID]; $projects[$projectID] = new stdClass(); if(isset($data->parents[$projectID])) $projects[$projectID]->parent = $data->parents[$projectID]; $projects[$projectID]->id = $projectID; $projects[$projectID]->name = $projectName; $projects[$projectID]->model = $oldProjects[$projectID]->model; $projects[$projectID]->PM = $data->PMs[$projectID]; $projects[$projectID]->begin = $data->begins[$projectID]; $projects[$projectID]->end = $data->ends[$projectID] == $this->lang->project->longTime ? LONG_TIME : $data->ends[$projectID]; $projects[$projectID]->days = $data->ends[$projectID] == $this->lang->project->longTime ? 0 : $data->dayses[$projectID]; $projects[$projectID]->acl = $data->acls[$projectID]; $projects[$projectID]->lastEditedBy = $this->app->user->account; $projects[$projectID]->lastEditedDate = helper::now(); if(isset($data->codes)) $projects[$projectID]->code = $projectCode; foreach($extendFields as $extendField) { $projects[$projectID]->{$extendField->field} = $this->post->{$extendField->field}[$projectID]; if(is_array($projects[$projectID]->{$extendField->field})) $projects[$projectID]->{$extendField->field} = join(',', $projects[$projectID]->{$extendField->field}); $projects[$projectID]->{$extendField->field} = htmlSpecialString($projects[$projectID]->{$extendField->field}); } } if(dao::isError()) return false; $this->loadModel('execution'); $this->lang->error->unique = $this->lang->error->repeat; foreach($projects as $projectID => $project) { $oldProject = $oldProjects[$projectID]; $parentID = !isset($project->parent) ? $oldProject->parent : $project->parent; $this->dao->update(TABLE_PROJECT)->data($project) ->autoCheck('begin,end') ->batchCheck($this->config->project->edit->requiredFields, 'notempty') ->checkIF($project->begin != '', 'begin', 'date') ->checkIF($project->end != '', 'end', 'date') ->checkIF($project->end != '', 'end', 'gt', $project->begin) ->checkIF(!empty($project->name), 'name', 'unique', "id != $projectID and `type`='project' and `parent` = $parentID and `model` = '{$project->model}' and `deleted` = '0'") ->checkIF(!empty($project->code), 'code', 'unique', "id != $projectID and `type`='project' and `model` = '{$project->model}' and `deleted` = '0'") ->checkFlow() ->where('id')->eq($projectID) ->exec(); if(dao::isError()) { $errors = dao::getError(); foreach($errors as $key => $error) dao::$errors[$key][0] = 'ID' . $projectID . $error[0]; return false; } if(!dao::isError()) { if(!$oldProject->hasProduct and ($oldProject->name != $project->name or $oldProject->parent != $project->parent or $oldProject->acl != $project->acl)) $this->updateShadowProduct($project); if(isset($project->parent)) { $linkedProducts = $this->dao->select('product')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($projectID)->fetchPairs(); $this->updateProductProgram($oldProject->parent, $project->parent, $linkedProducts); if($oldProject->parent != $project->parent) $this->loadModel('program')->processNode($projectID, $project->parent, $oldProject->path, $oldProject->grade); } /* When acl is open, white list set empty. When acl is private,update user view. */ if($project->acl == 'open') $this->loadModel('personnel')->updateWhitelist(array(), 'project', $projectID); if($project->acl != 'open') $this->loadModel('user')->updateUserView($projectID, 'project'); $this->executeHooks($projectID); if(empty($oldProject->multiple) and $oldProject->model != 'waterfall') $this->execution->syncNoMultipleSprint($projectID); } $allChanges[$projectID] = common::createChanges($oldProject, $project); } return $allChanges; } /** * 开始项目并更改其状态. * * @param int $projectID * @param object $project * @access public * @return array|false */ public function start(int $projectID, object $project):array|false { $oldProject = $this->getById($projectID); $project = $this->loadModel('file')->processImgURL($project, $this->config->project->editor->start, $this->post->uid); $this->projectTao->doStart($projectID, $project); /* When it has multiple errors, only the first one is prompted */ if(dao::isError()) { if(count(dao::$errors['realBegan']) > 1) dao::$errors['realBegan'] = dao::$errors['realBegan'][0]; return false; } if(!$oldProject->multiple) $this->projectTao->changeExecutionStatus($projectID, 'start'); return common::createChanges($oldProject, $project); } /** * Suspend project and update status. * 暂停项目并更改其状态 * * @param int $projectID * @param object $project * @param string $type * * @access public * @return array|flase */ public function suspend(int $projectID, object $project, string $type = 'project'): array|false { $editorIdList = $this->config->project->editor->suspend['id']; $oldProject = $this->getById($projectID, $type); $project = $this->loadModel('file')->processImgURL($project, $editorIdList, $this->post->uid); $this->projectTao->doSuspend($projectID, $project); if(!$oldProject->multiple) $this->projectTao->changeExecutionStatus($projectID, 'suspend'); return common::createChanges($oldProject, $project); } /** * Activate project. * * @param int $projectID * @param object $project * @access public * @return array $changes|false */ public function activate(int $projectID, object $project) :array|false { $oldProject = $this->projectTao->fetchProjectInfo($projectID); $daoSuccess = $this->projectTao->doActivate($projectID, $project); if(!$daoSuccess) return false; if(empty($oldProject->multiple) and $oldProject->model != 'waterfall') $this->loadModel('execution')->syncNoMultipleSprint($projectID); /* Update start and end date of tasks in this project. */ if($project->readjustTime and $project->readjustTask) { $tasks = $this->projectTao->fetchUndoneTasks((int)$projectID); $this->projectTao->updateTasksStartAndEndDate($tasks, $oldProject, $project); } /* Activate the shadow product of the project. (only change product status) */ if(!$oldProject->hasProduct) { $productID = $this->loadModel('product')->getProductIDByProject($projectID); $this->product->activate($productID); } return common::createChanges($oldProject, $project); } /** * 关闭项目并更改其状态 * Close project and update status. * * @param int $projectID * @param object $project * * @access public * @return array|false */ public function close(int $projectID, object $project): array|false { $oldProject = $this->getByID($projectID); $editorIdList = $this->config->project->editor->close['id']; $project = $this->loadModel('file')->processImgURL($project, $editorIdList, $this->post->uid); $this->projectTao->doClosed($projectID, $project, $oldProject); /* When it has multiple errors, only the first one is prompted */ if(dao::isError()) { if(count(dao::$errors['realEnd']) > 1) dao::$errors['realEnd'] = dao::$errors['realEnd'][0]; return false; } if(!$oldProject->multiple) $this->projectTao->changeExecutionStatus($projectID, 'close'); /* Close the shadow product of the project. */ if(!$oldProject->hasProduct) { $productID = $this->loadModel('product')->getProductIDByProject($projectID); $this->product->close($productID); } $this->loadModel('score')->create('project', 'close', $oldProject); return common::createChanges($oldProject, $project); } /** * 如果是无产品项目,更新影子产品信息。 * Update shadow product. * * @param object $project * @param object $oldProject * @access public * @return bool */ public function updateShadowProduct(object $project, object $oldProject): bool { /* If this is a project without product, update shadow product's info. */ if($oldProject->hasProduct) return true; $projectID = $oldProject->id; /* If oldProject has no product and name or parent or acl has changed, update shadow product. */ if($oldProject->name != $project->name || $oldProject->parent != $project->parent || $oldProject->acl != $project->acl) { $product = $this->dao->select('product')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($projectID)->fetch('product'); $topProgram = !empty($project->parent) ? $this->loadModel('program')->getTopByID($project->parent) : 0; $this->dao->update(TABLE_PRODUCT) ->set('name')->eq($project->name) ->set('program')->eq($topProgram) ->set('acl')->eq($project->acl) ->where('id')->eq($product) ->exec(); } /* Update shadow product's status if need .*/ if($oldProject->status != $project->status && str_contains('doing,closed', $project->status)) $this->updateShadowProductStatus($projectID, $project->status); return !dao::isError(); } /** * 删除项目下关联执行与产品与文档库 * Deletes related items under project. * * @param string $table product|execution|doclib * @param int|array $idList * * @access public * @return bool */ public function deleteByTableName(string $table, int|array $idList): bool { if(strpos($table, 'doclib') !== false) { $this->dao->update($table)->set('deleted')->eq(1)->where('execution')->eq($idList)->exec(); } else { $this->dao->update($table)->set('deleted')->eq(1)->where('id')->in($idList)->exec(); } return !dao::isError(); } /** * Update the program of the product. * * @param int $oldProgram * @param int $newProgram * @param array $products * * @access public * @return void */ public function updateProductProgram($oldProgram, $newProgram, $products) { $this->loadModel('action'); $this->loadModel('program'); /* Product belonging project set processing. */ $oldTopProgram = $this->program->getTopByID($oldProgram); $newTopProgram = $this->program->getTopByID($newProgram); if($oldTopProgram != $newTopProgram) { foreach($products as $productID) { $oldProduct = $this->dao->findById($productID)->from(TABLE_PRODUCT)->fetch(); $this->dao->update(TABLE_PRODUCT)->set('program')->eq((int)$newTopProgram)->where('id')->eq((int)$productID)->exec(); $newProduct = $this->dao->findById($productID)->from(TABLE_PRODUCT)->fetch(); $changes = common::createChanges($oldProduct, $newProduct); $actionID = $this->action->create('product', $productID, 'edited'); $this->action->logHistory($actionID, $changes); } } } /** * Unlink a member. * * @param int $projectID * @param string $account * @param string $removeExecution no|yes * @access public * @return void */ public function unlinkMember(int $projectID, string $account, string $removeExecution = 'no'): void { $this->projectTao->unlinkTeamMember($projectID, 'project', $account); $this->loadModel('user')->updateUserView($projectID, 'project', array($account)); if($removeExecution == 'yes') { $executions = $this->loadModel('execution')->getByProject($projectID, 'undone', 0, true); $this->projectTao->unlinkTeamMember(array_keys($executions), 'execution', $account); $this->user->updateUserView(array_keys($executions), 'sprint', array($account)); } $linkedProducts = $this->loadModel('product')->getProductPairsByProject($projectID); if(!empty($linkedProducts)) $this->user->updateUserView(array_keys($linkedProducts), 'product', array($account)); } /** * Manage team members. * * @param int $projectID * @access public * @return void */ public function manageMembers($projectID) { $project = $this->projectTao->fetchProjectInfo($projectID); $data = (array)fixer::input('post')->get(); extract($data); $projectID = (int)$projectID; $projectType = 'project'; $accounts = array_unique($accounts); $oldJoin = $this->dao->select('`account`, `join`')->from(TABLE_TEAM)->where('root')->eq($projectID)->andWhere('type')->eq($projectType)->fetchPairs(); foreach($accounts as $key => $account) { if(empty($account)) continue; if(!empty($project->days) and (int)$days[$key] > $project->days) { dao::$errors['message'][] = sprintf($this->lang->project->daysGreaterProject, $project->days); return false; } if((float)$hours[$key] > 24) { dao::$errors['message'][] = $this->lang->project->errorHours; return false; } } $this->dao->delete()->from(TABLE_TEAM)->where('root')->eq($projectID)->andWhere('type')->eq($projectType)->exec(); $projectMember = array(); foreach($accounts as $key => $account) { if(empty($account)) continue; $member = new stdclass(); $member->role = $roles[$key]; $member->days = $days[$key]; $member->hours = $hours[$key]; $member->limited = isset($limited[$key]) ? $limited[$key] : 'no'; $member->root = $projectID; $member->account = $account; $member->join = isset($oldJoin[$account]) ? $oldJoin[$account] : helper::today(); $member->type = $projectType; $projectMember[$account] = $member; $this->dao->insert(TABLE_TEAM)->data($member)->exec(); } /* Only changed account update userview. */ $oldAccounts = array_keys($oldJoin); $removedAccounts = array_diff($oldAccounts, $accounts); $changedAccounts = array_merge($removedAccounts, array_diff($accounts, $oldAccounts)); $changedAccounts = array_unique($changedAccounts); $childSprints = $this->dao->select('id')->from(TABLE_PROJECT)->where('project')->eq($projectID)->andWhere('type')->in('stage,sprint')->andWhere('deleted')->eq('0')->fetchPairs(); $linkedProducts = $this->dao->select("t2.id")->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product = t2.id') ->where('t2.deleted')->eq(0) ->andWhere('t1.project')->eq($projectID) ->andWhere('t2.vision')->eq($this->config->vision) ->fetchPairs(); $this->loadModel('user')->updateUserView(array($projectID), 'project', $changedAccounts); if(!empty($childSprints)) $this->user->updateUserView($childSprints, 'sprint', $changedAccounts); if(!empty($linkedProducts)) $this->user->updateUserView(array_keys($linkedProducts), 'product', $changedAccounts); /* Remove execution members. */ if($this->post->removeExecution == 'yes' and !empty($childSprints) and !empty($removedAccounts)) { $this->dao->delete()->from(TABLE_TEAM) ->where('root')->in($childSprints) ->andWhere('type')->eq('execution') ->andWhere('account')->in($removedAccounts) ->exec(); } if(empty($project->multiple) and $project->model != 'waterfall') $this->loadModel('execution')->syncNoMultipleSprint($projectID); } /** * Print datatable cell. * * @param object $col * @param object $project * @param array $users * @param int $programID * @access public * @return void */ public function printCell($col, $project, $users, $programID = 0) { $canOrder = common::hasPriv('project', 'updateOrder'); $canBatchEdit = common::hasPriv('project', 'batchEdit'); $account = $this->app->user->account; $field = $col->id; $projectLink = helper::createLink('project', 'index', "projectID=$project->id", '', false, $project->id); if($col->show) { $title = ''; $class = "c-$field" . (in_array($field, array('budget', 'teamCount', 'estimate', 'consume')) ? ' c-number' : ''); if($field == 'id') $class .= ' cell-id'; if($field == 'code') { $class .= ' c-name'; $title = "title={$project->code}"; } elseif($field == 'name') { $class .= ' text-left'; $title = "title='{$project->name}'"; } elseif($field == 'PM') { $class .= ' c-manager'; } if($field == 'end') { $project->end = $project->end == LONG_TIME ? $this->lang->project->longTime : $project->end; $class .= ' c-name'; $title = "title='{$project->end}'"; } if($field == 'budget') { $projectBudget = $this->getBudgetWithUnit($project->budget); $budgetTitle = $project->budget != 0 ? zget($this->lang->project->currencySymbol, $project->budgetUnit) . ' ' . $projectBudget : $this->lang->project->future; $title = "title='$budgetTitle'"; } if($field == 'estimate') $title = "title='{$project->hours->totalEstimate} {$this->lang->execution->workHour}'"; if($field == 'consume') $title = "title='{$project->hours->totalConsumed} {$this->lang->execution->workHour}'"; if($field == 'surplus') $title = "title='{$project->hours->totalLeft} {$this->lang->execution->workHour}'"; echo ""; if($this->config->edition != 'open') $this->loadModel('flow')->printFlowCell('project', $project, $field); switch($field) { case 'id': if($canBatchEdit) { echo html::checkbox('projectIdList', array($project->id => '')) . html::a($projectLink, sprintf('%03d', $project->id)); } else { printf('%03d', $project->id); } break; case 'name': $prefix = ''; $suffix = ''; $projectIcon = ''; if(isset($project->delay)) $suffix = "{$this->lang->project->statusList['delay']}"; $projectType = $project->model == 'scrum' ? 'sprint' : $project->model; if(!empty($suffix) or !empty($prefix)) echo '
    '; if(!empty($prefix)) echo $prefix; if($this->config->vision == 'rnd') $projectIcon = " "; echo html::a($projectLink, $projectIcon . $project->name, '', "class='text-ellipsis'"); if(!empty($suffix)) echo $suffix; if(!empty($suffix) or !empty($prefix)) echo '
    '; break; case 'code': echo $project->code; break; case 'PM': $user = $this->loadModel('user')->getByID($project->PM, 'account'); $userID = !empty($user) ? $user->id : ''; $userAvatar = !empty($user) ? $user->avatar : ''; $PMLink = helper::createLink('user', 'profile', "userID=$userID", '', true); $userName = zget($users, $project->PM); if($project->PM) echo html::smallAvatar(array('avatar' => $userAvatar, 'account' => $project->PM, 'name' => $userName), "avatar-circle avatar-{$project->PM}"); echo empty($project->PM) ? '' : html::a($PMLink, $userName, '', "title='{$userName}' data-toggle='modal' data-type='iframe' data-width='600'"); break; case 'begin': echo $project->begin; break; case 'end': echo $project->end; break; case 'status': echo " " . zget($this->lang->project->statusList, $project->status) . ""; break; case 'hasProduct': echo zget($this->lang->project->projectTypeList, $project->hasProduct); break; case 'budget': echo $budgetTitle; break; case 'teamCount': echo $project->teamCount; break; case 'estimate': echo $project->hours->totalEstimate . $this->lang->execution->workHourUnit; break; case 'consume': echo $project->hours->totalConsumed . $this->lang->execution->workHourUnit; break; case 'surplus': echo $project->hours->totalLeft . $this->lang->execution->workHourUnit; break; case 'progress': echo html::ring($project->hours->progress); break; case 'actions': $project->programID = $programID; echo $this->buildOperateMenu($project, 'browse'); break; default: echo $project->$field; break; } echo ''; } } /** * Print datatable cell for ZIN. * * @param object $col * @param object $project * @param object $item * @param int $programID * @access public * @return void */ public function printCellZin($col, $project, &$item, $programID = 0) { if(!$col->show) return; $canOrder = common::hasPriv('project', 'updateOrder'); $canBatchEdit = common::hasPriv('project', 'batchEdit'); $account = $this->app->user->account; $field = $col->id; $projectLink = helper::createLink('project', 'index', "projectID=$project->id", '', false, $project->id); $title = ''; $class = "c-$field" . (in_array($field, array('budget', 'teamCount', 'estimate', 'consume')) ? ' c-number' : ''); if($field == 'id') $class .= ' cell-id'; if($field == 'code') { $class .= ' c-name'; $title = "title={$project->code}"; } elseif($field == 'name') { $class .= ' text-left'; $title = "title='{$project->name}'"; } elseif($field == 'PM') { $class .= ' c-manager'; } if($field == 'end') { $project->end = $project->end == LONG_TIME ? $this->lang->project->longTime : $project->end; $class .= ' c-name'; $title = "title='{$project->end}'"; } if($field == 'budget') { $projectBudget = $this->getBudgetWithUnit($project->budget); $budgetTitle = $project->budget != 0 ? zget($this->lang->project->currencySymbol, $project->budgetUnit) . ' ' . $projectBudget : $this->lang->project->future; $title = "title='$budgetTitle'"; } if($field == 'estimate') $title = "title='{$project->hours->totalEstimate} {$this->lang->execution->workHour}'"; if($field == 'consume') $title = "title='{$project->hours->totalConsumed} {$this->lang->execution->workHour}'"; if($field == 'surplus') $title = "title='{$project->hours->totalLeft} {$this->lang->execution->workHour}'"; /* if($this->config->edition != 'open') $this->loadModel('flow')->printFlowCell('project', $project, $field); */ switch($field) { case 'id': $item->id = sprintf('%03d', $project->id); break; case 'name': $item->name = $project->name; $item->delay = isset($project->delay) ? $project->delay : 0; break; case 'code': $item->code = $project->code; break; case 'PM': $item->PM = $project->PM; break; case 'begin': $item->begin = $project->begin; break; case 'end': $item->end = $project->end; break; case 'status': $item->status = zget($this->lang->project->statusList, $project->status); break; case 'hasProduct': $item->hasProduct = zget($this->lang->project->projectTypeList, $project->hasProduct); break; case 'budget': $item->budget = $budgetTitle; break; case 'teamCount': $item->teamCount = $project->teamCount; break; case 'estimate': $item->estimate = $project->hours->totalEstimate . $this->lang->execution->workHourUnit; break; case 'consume': $item->consume = $project->hours->totalConsumed . $this->lang->execution->workHourUnit; break; case 'surplus': $item->surplus = $project->hours->totalLeft . $this->lang->execution->workHourUnit; break; case 'progress': $item->progress = $project->hours->progress; break; case 'actions': $project->programID = $programID; $this->buildOperateMenuZin($project, $item, 'browse'); break; default: $item->$field = $project->$field; break; } $item->storyCount = rand(100, 100000) / 10.0; $item->executionCount = rand(10, 200); $item->invested = rand(10, 100); } /** * 将数字转换成带单位的数字。 * Convert budget unit. * * @param float|string $budget * @access public * @return float|string $projectBudget */ public function getBudgetWithUnit(float|string $budget): float|string { $budget = (float)$budget; if($budget < $this->config->project->budget->tenThousand) { $budget = round($budget, $this->config->project->budget->precision); $unit = ''; } elseif($budget < $this->config->project->budget->oneHundredMillion && $budget >= $this->config->project->budget->tenThousand) { $budget = round($budget/$this->config->project->budget->tenThousand, $this->config->project->budget->precision); $unit = $this->lang->project->tenThousand; } else { $budget = round($budget/$this->config->project->budget->oneHundredMillion, $this->config->project->budget->precision); $unit = $this->lang->project->hundredMillion; } $projectBudget = in_array($this->app->getClientLang(), array('zh-cn','zh-tw')) ? $budget . $unit : round($budget, $this->config->project->budget->precision); return $projectBudget; } /** * Update products of a project. * * @param int $projectID * @param array $products * @access public * @return bool */ public function updateProducts(int $projectID, array $products = array()): bool { $this->loadModel('user'); $members = array_keys($this->getTeamMembers($projectID)); /* Link products of other programs. */ if(!empty($_POST['otherProducts'])) { $products = array(); $otherProducts = $_POST['otherProducts']; foreach($otherProducts as $index => $otherProduct) { if(!$otherProduct) continue; $data = new stdclass(); $data->project = $projectID; $data->plan = 0; if(strpos($otherProduct, '_') !== false) { $params = explode('_', $otherProduct); $data->product = $params[0]; $data->branch = $params[1]; } else { $data->product = $otherProduct; $data->branch = 0; } $this->dao->insert(TABLE_PROJECTPRODUCT)->data($data)->exec(); $products[] = $data->product; } $this->user->updateUserView($products, 'product', $members); if((int)$projectID > 0 and !empty($_POST['stageBy'])) { $this->dao->update(TABLE_PROJECT)->set('stageBy')->eq('product')->where('id')->eq((int)$projectID)->exec(); $this->dao->update(TABLE_EXECUTION)->set('stageBy')->eq('product')->where('project')->eq((int)$projectID)->exec(); } return !dao::isError(); } /* Link products of current program of the project. */ $products = isset($_POST['products']) ? $_POST['products'] : $products; $oldProjectProducts = $this->dao->select('*')->from(TABLE_PROJECTPRODUCT)->where('project')->eq((int)$projectID)->fetchGroup('product', 'branch'); $this->dao->delete()->from(TABLE_PROJECTPRODUCT)->where('project')->eq((int)$projectID)->exec(); if(empty($products)) { $this->user->updateUserView(array_keys($oldProjectProducts), 'product', $members); return true; } $branches = isset($_POST['branch']) ? $_POST['branch'] : array(); $plans = isset($_POST['plans']) ? $_POST['plans'] : array();; $existedProducts = array(); foreach($products as $i => $productID) { if(empty($productID)) continue; if(!isset($existedProducts[$productID])) $existedProducts[$productID] = array(); $oldPlan = 0; $branch = isset($branches[$i]) ? $branches[$i] : 0; if(!is_array($branch)) $branch = array($branch => $branch); foreach($branch as $branchID) { if(isset($existedProducts[$productID][$branchID])) continue; if(isset($oldProjectProducts[$productID][$branchID])) { $oldProjectProduct = $oldProjectProducts[$productID][$branchID]; if($this->app->rawMethod != 'edit') $oldPlan = $oldProjectProduct->plan; } $data = new stdclass(); $data->project = $projectID; $data->product = $productID; $data->branch = $branchID; $data->plan = isset($plans[$productID]) ? implode(',', $plans[$productID]) : $oldPlan; $data->plan = trim((string)$data->plan, ','); $data->plan = empty($data->plan) ? 0 : ",$data->plan,"; $this->dao->insert(TABLE_PROJECTPRODUCT)->data($data)->exec(); $existedProducts[$productID][$branchID] = true; } } /* Delete the execution linked products that is not linked with the execution. */ $projectID = (int)$projectID; if($projectID > 0) { $executions = $this->dao->select('id')->from(TABLE_EXECUTION)->where('project')->eq($projectID)->fetchPairs('id'); $this->dao->delete()->from(TABLE_PROJECTPRODUCT)->where('project')->in($executions)->andWhere('product')->notin($products)->exec(); if(!empty($_POST['stageBy'])) { $this->dao->update(TABLE_PROJECT)->set('stageBy')->eq('product')->where('id')->eq((int)$projectID)->exec(); $this->dao->update(TABLE_EXECUTION)->set('stageBy')->eq('product')->where('project')->eq((int)$projectID)->exec(); } $project = $this->projectTao->fetchProjectInfo($projectID); if(!empty($project) && !empty($executions) && $project->stageBy == 'project' && in_array($project->model, array('waterfall', 'waterfallplus'))) { $this->loadModel('execution'); foreach($executions as $executionID) $this->execution->updateProducts($executionID); } } $oldProductKeys = array_keys($oldProjectProducts); $needUpdate = array_merge(array_diff($oldProductKeys, $products), array_diff($products, $oldProductKeys)); if($needUpdate) $this->user->updateUserView($needUpdate, 'product', $members); $unlinkedProducts = array_diff($oldProductKeys, $products); if(!empty($unlinkedProducts)) { $products = $this->dao->select('name')->from(TABLE_PRODUCT) ->where('id')->in($unlinkedProducts) ->fetchPairs(); $this->loadModel('action')->create('project', $projectID, 'unlinkproduct', '', implode(',', $products)); } return !dao::isError(); } /** * 更新关联的产品和执行的用户视图。 * Update userview for involved product and execution. * * @param int $projectID * @param array $users * @access public * @return void */ public function updateInvolvedUserView(int $projectID, array $users = array()): void { $products = $this->dao->select('product')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($projectID)->fetchPairs('product', 'product'); $this->loadModel('user')->updateUserView($products, 'product', $users); $executions = $this->dao->select('id')->from(TABLE_EXECUTION)->where('project')->eq($projectID)->fetchPairs('id', 'id'); if($executions) $this->user->updateUserView($executions, 'sprint', $users); } /** * Get team members. * * @param int $projectID * @access public * @return array */ public function getTeamMembers($projectID) { if(defined('TUTORIAL')) return $this->loadModel('tutorial')->getTeamMembers(); $project = $this->projectTao->fetchProjectInfo($projectID); if(empty($project)) return array(); return $this->dao->select("t1.*, t1.hours * t1.days AS totalHours, t2.id as userID, if(t2.deleted='0', t2.realname, t1.account) as realname")->from(TABLE_TEAM)->alias('t1') ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account = t2.account') ->where('t1.root')->eq((int)$projectID) ->andWhere('t1.type')->eq($project->type) ->andWhere('t2.deleted')->eq('0') ->beginIF($this->config->vision)->andWhere("CONCAT(',', t2.visions, ',')")->like("%,{$this->config->vision},%")->fi() ->fetchAll('account'); } /** * Get team member pairs by projectID. * * @param int $projectID * @access public * @return array */ public function getTeamMemberPairs($projectID) { $project = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetch(); if(empty($project)) return array(); $type = 'project'; if($project->type == 'sprint' or $project->type == 'stage' or $project->type == 'kanban') $type = 'execution'; $members = $this->dao->select("t1.account, if(t2.deleted='0', t2.realname, t1.account) as realname")->from(TABLE_TEAM)->alias('t1') ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account = t2.account') ->where('t1.root')->eq((int)$projectID) ->andWhere('t1.type')->eq($type) ->andWhere('t2.deleted')->eq('0') ->beginIF($this->config->vision)->andWhere("CONCAT(',', t2.visions, ',')")->like("%,{$this->config->vision},%")->fi() ->fetchPairs('account', 'realname'); return array('' => '') + $members; } /** * Get team member group. * * @param array|string $projectIdList * @access public * @return array */ public function getTeamMemberGroup($projectIdList) { if(empty($projectIdList)) return array(); return $this->dao->select("t1.account, if(t2.deleted='0', t2.realname, t1.account) as realname, t1.root as project")->from(TABLE_TEAM)->alias('t1') ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account = t2.account') ->where('t1.root')->in($projectIdList) ->andWhere('t1.type')->eq('project') ->andWhere('t2.deleted')->eq('0') ->beginIF($this->config->vision)->andWhere("CONCAT(',', t2.visions, ',')")->like("%,{$this->config->vision},%")->fi() ->fetchGroup('project', 'account'); } /** * Get members of a project who can be imported. * * @param int $projectID * @param array $currentMembers * @access public * @return array */ public function getMembers2Import($projectID, $currentMembers) { if($projectID == 0) return array(); return $this->dao->select('account, role, hours') ->from(TABLE_TEAM) ->where('root')->eq($projectID) ->andWhere('type')->eq('project') ->andWhere('account')->notIN($currentMembers) ->fetchAll('account'); } /** * Get stats for project kanban. * * @access public * @return void */ public function getStats4Kanban() { $ongoingExecutions = $this->projectTao->getOngoingExecutions(); $projectsStats = $this->projectTao->getProjectsStats(); return array('kanbanGroup' => $projectsStats, 'ongoingExecutions' => $ongoingExecutions); } /** * Computer execution progress. * * @param array $executions * @access public * @return array */ public function computerProgress($executions) { $hours = array(); $emptyHour = array('totalEstimate' => 0, 'totalConsumed' => 0, 'totalLeft' => 0, 'progress' => 0); /* Get all tasks and compute totalEstimate, totalConsumed, totalLeft, progress according to them. */ $tasks = $this->dao->select('id, execution, estimate, consumed, `left`, status, closedReason') ->from(TABLE_TASK) ->where('execution')->in(array_keys($executions)) ->andWhere('parent')->lt(1) ->andWhere('deleted')->eq(0) ->fetchGroup('execution', 'id'); $projects = $this->dao->select('t1.id,t2.model') ->from(TABLE_PROJECT)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id') ->where('t1.deleted')->eq('0') ->andWhere('t1.id')->in(array_keys($executions)) ->fetchPairs(); /* Compute totalEstimate, totalConsumed, totalLeft. */ foreach($tasks as $executionID => $executionTasks) { $hour = (object)$emptyHour; foreach($executionTasks as $task) { $hour->totalEstimate += $task->estimate; $hour->totalConsumed += $task->consumed; if($task->status != 'cancel' and $task->status != 'closed') $hour->totalLeft += $task->left; } $hours[$executionID] = $hour; if(isset($executions[$executionID]) && $executions[$executionID]->grade > 1 && isset($projects[$executionID]) && in_array($projects[$executionID], array('waterfall', 'waterfallplus'))) { $stageParents = $this->dao->select('id')->from(TABLE_EXECUTION)->where('id')->in(trim($executions[$executionID]->path, ','))->andWhere('type')->eq('stage')->andWhere('id')->ne($executions[$executionID]->id)->orderBy('grade')->fetchPairs(); foreach($stageParents as $stageParent) { if(!isset($hours[$stageParent])) { $hours[$stageParent] = clone $hour; continue; } $hours[$stageParent]->totalEstimate += $hour->totalEstimate; $hours[$stageParent]->totalConsumed += $hour->totalConsumed; $hours[$stageParent]->totalLeft += $hour->totalLeft; } } } /* Compute totalReal and progress. */ foreach($hours as $hour) { $hour->totalEstimate = round($hour->totalEstimate, 1) ; $hour->totalConsumed = round($hour->totalConsumed, 1); $hour->totalLeft = round($hour->totalLeft, 1); $hour->totalReal = $hour->totalConsumed + $hour->totalLeft; $hour->progress = $hour->totalReal ? round($hour->totalConsumed / $hour->totalReal * 100, 2) : 0; } return $hours; } /** * Set menu of project module. * * @param int $objectID projectID * @access public * @return int */ public function setMenu($objectID) { global $lang; $objectID = (int)$objectID; $model = 'scrum'; $objectID = (empty($objectID) and $this->session->project) ? $this->session->project : $objectID; $project = $this->projectTao->fetchProjectInfo($objectID); if(!$project) { $execution = $this->loadModel('execution')->getByID($objectID); if($execution and $execution->project and !$execution->multiple) { $project = $this->projectTao->fetchProjectInfo($execution->project); $objectID = $execution->project; } } if($project and $project->model == 'waterfall') $model = $project->model; if($project and $project->model == 'waterfallplus') $model = 'waterfall'; if($project and $project->model == 'kanban') { $model = $project->model . 'Project'; $lang->executionCommon = $lang->project->kanban; } if(isset($lang->$model)) { $lang->project->menu = $lang->{$model}->menu; $lang->project->menuOrder = $lang->{$model}->menuOrder; $lang->project->dividerMenu = $lang->{$model}->dividerMenu; } if(empty($project->hasProduct)) { unset($lang->project->menu->settings['subMenu']->products); $projectProduct = $this->dao->select('product')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($objectID)->fetch('product'); $lang->project->menu->settings['subMenu']->module['link'] = sprintf($lang->project->menu->settings['subMenu']->module['link'], $projectProduct); if(isset($project->model) and ($project->model == 'scrum' or $project->model == 'agileplus')) { $lang->project->menu->projectplan['link'] = sprintf($lang->project->menu->projectplan['link'], $projectProduct); } else { unset($lang->project->menu->projectplan); } if(!empty($this->config->URAndSR) && $project->model !== 'kanban' && isset($lang->project->menu->storyGroup)) { $lang->project->menu->settings['subMenu']->module['link'] = sprintf($lang->project->menu->settings['subMenu']->module['link'], $projectProduct); if($project->model !== 'kanban' && isset($lang->project->menu->storyGroup)) { $lang->project->menu->story = $lang->project->menu->storyGroup; $lang->project->menu->story['link'] = sprintf($lang->project->menu->storyGroup['link'], '%s', $projectProduct); $lang->project->menu->story['dropMenu']->story['link'] = sprintf($lang->project->menu->storyGroup['dropMenu']->story['link'], '%s', $projectProduct); $lang->project->menu->story['dropMenu']->requirement['link'] = sprintf($lang->project->menu->storyGroup['dropMenu']->requirement['link'], '%s', $projectProduct); } unset($lang->project->menu->settings['subMenu']->products); } } else { unset($lang->project->menu->settings['subMenu']->module); unset($lang->project->menu->projectplan); } if(isset($lang->project->menu->storyGroup)) unset($lang->project->menu->storyGroup); /* Reset project priv. */ $moduleName = $this->app->rawModule; $methodName = $this->app->rawMethod; $this->loadModel('common')->resetProjectPriv($objectID); if(!$this->common->isOpenMethod($moduleName, $methodName) and !commonModel::hasPriv($moduleName, $methodName)) $this->common->deny($moduleName, $methodName, false); if(isset($project->model) and ($project->model == 'waterfall' or $project->model == 'waterfallplus')) { $lang->project->createExecution = str_replace($lang->executionCommon, $lang->project->stage, $lang->project->createExecution); $lang->project->lastIteration = str_replace($lang->executionCommon, $lang->project->stage, $lang->project->lastIteration); $this->loadModel('execution'); $executionCommonLang = $lang->executionCommon; $lang->executionCommon = $lang->project->stage; include $this->app->getModulePath('', 'execution') . 'lang/' . $this->app->getClientLang() . '.php'; $lang->execution->typeList['sprint'] = $executionCommonLang; } $lang->switcherMenu = $this->getSwitcher($objectID, $moduleName, $methodName); $this->saveState($objectID, $this->getPairsByProgram()); if(isset($project->acl) and $project->acl == 'open') unset($lang->project->menu->settings['subMenu']->whitelist); common::setMenuVars('project', $objectID); $this->setNoMultipleMenu($objectID); return $objectID; } /** * Set multi-scrum menu. * * @param int $objectID * @access public * @return void */ public function setNoMultipleMenu($objectID) { $moduleName = $this->app->rawModule; $methodName = $this->app->rawMethod; $this->session->set('multiple', true); $project = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($objectID)->andWhere('multiple')->eq('0')->fetch(); if(empty($project)) return; if(!in_array($project->type, array('project', 'sprint', 'kanban'))) return; if($project->type == 'project') { $model = $project->model; $projectID = $project->id; $executionID = $this->dao->select('id')->from(TABLE_EXECUTION) ->where('project')->eq($projectID) ->andWhere('multiple')->eq('0') ->andWhere('type')->in('kanban,sprint') ->andWhere('deleted')->eq('0') ->fetch('id'); } else { $model = $project->type == 'kanban' ? 'kanban' : 'scrum'; $executionID = $project->id; $projectID = $project->project; } if(empty($projectID) or empty($executionID)) return; $this->session->set('project', $projectID, 'project'); $this->session->set('multiple', false); $navGroup = zget($this->lang->navGroup, $moduleName); $this->lang->$navGroup->menu = $this->lang->project->noMultiple->{$model}->menu; $this->lang->$navGroup->menuOrder = $this->lang->project->noMultiple->{$model}->menuOrder; $this->lang->$navGroup->dividerMenu = $this->lang->project->noMultiple->{$model}->dividerMenu; /* Single execution and has no product project menu. */ if(!$project->hasProduct && !$project->multiple && !empty($this->config->URAndSR) && isset($this->lang->$navGroup->menu->storyGroup)) { $this->lang->$navGroup->menu->story = $this->lang->$navGroup->menu->storyGroup; $this->lang->$navGroup->menu->story['link'] = sprintf($this->lang->$navGroup->menu->storyGroup['link'], '%s', $projectID); $this->lang->$navGroup->menu->story['dropMenu']->story['link'] = sprintf($this->lang->$navGroup->menu->storyGroup['dropMenu']->story['link'], '%s', $projectID); $this->lang->$navGroup->menu->story['dropMenu']->requirement['link'] = sprintf($this->lang->$navGroup->menu->storyGroup['dropMenu']->requirement['link'], '%s', $projectID); } if(isset($this->lang->$navGroup->menu->storyGroup)) unset($this->lang->$navGroup->menu->storyGroup); foreach($this->lang->$navGroup->menu as $label => $menu) { $objectID = 0; if(strpos($this->config->project->multiple['project'], ",{$label},") !== false) $objectID = $projectID; if(strpos($this->config->project->multiple['execution'], ",{$label},") !== false) { $objectID = $executionID; $this->lang->$navGroup->menu->{$label}['subModule'] = 'project'; } $this->lang->$navGroup->menu->$label = commonModel::setMenuVarsEx($menu, $objectID); if(isset($menu['subMenu'])) { foreach($menu['subMenu'] as $key1 => $subMenu) $this->lang->$navGroup->menu->{$label}['subMenu']->$key1 = common::setMenuVarsEx($subMenu, $objectID); } if(!isset($menu['dropMenu'])) continue; foreach($menu['dropMenu'] as $key2 => $dropMenu) { $this->lang->$navGroup->menu->{$label}['dropMenu']->$key2 = common::setMenuVarsEx($dropMenu, $objectID); if(!isset($dropMenu['subMenu'])) continue; foreach($dropMenu['subMenu'] as $key3 => $subMenu) $this->lang->$navGroup->menu->{$label}['dropMenu']->$key3 = common::setMenuVarsEx($subMenu, $objectID); } } /* If objectID is set, cannot use homeMenu. */ unset($this->lang->project->homeMenu); $this->lang->switcherMenu = $this->getSwitcher($projectID, $moduleName, $methodName); $this->lang->project->menu = $this->lang->$navGroup->menu; $this->lang->project->menuOrder = $this->lang->$navGroup->menuOrder; $this->lang->project->dividerMenu = $this->lang->$navGroup->dividerMenu; if(empty($project->hasProduct)) { unset($this->lang->project->menu->settings['subMenu']->products); } else { unset($this->lang->project->menu->settings['subMenu']->module); unset($this->lang->project->menu->projectplan); } $this->loadModel('common')->resetProjectPriv($projectID); } /** * 检查是否可以修改model字段 * Check if the project model can be changed. * * @param int $projectID * @param string $model * @access public * @return bool */ public function checkCanChangeModel(int $projectID, string $model): bool { if(empty($model)) return false; $checkList = $this->config->project->checkList->$model; if($this->config->edition == 'max') $checkList = $this->config->project->maxCheckList->$model; foreach($checkList as $module) { if($module == '') continue; $type = ''; $table = constant('TABLE_'. strtoupper($module)); if($module == 'execution') { $this->app->loadConfig('execution'); $type = zget($this->config->execution->modelList, $model, ''); } /* 检查对应类型的数据是否已经存在。 */ /* Check if the data of the type already exists. */ $object = $this->getDataByProject($table, $projectID, $type); if(!empty($object)) return false; } return true; } /** * Get the objects under the project. * * @param constant $table * @param int $projectID * @param string $type * @access public * @return object */ public function getDataByProject($table, $projectID, $type = '') { $result = $this->dao->select('id')->from($table) ->where('project')->eq($projectID) ->beginIF(!empty($type))->andWhere('type')->eq($type)->fi() ->fetch(); return $result; } /** * Build project action menu. * * @param object $project * @param object $item * @param string $type * @access public * @return string */ public function buildOperateMenuZin($project, &$item, $type = 'view') { $function = 'buildOperate' . ucfirst($type) . 'MenuZin'; return $this->$function($project, $item); } /** * Build project action menu. * * @param object $project * @param string $type * @access public * @return string */ public function buildOperateMenu($project, $type = 'view') { $function = 'buildOperate' . ucfirst($type) . 'Menu'; return $this->$function($project); } /** * Build project view action menu. * * @param object $project * @access public * @return string */ public function buildOperateViewMenu($project) { if($project->deleted) return ''; $menu = ''; $params = "projectID=$project->id"; $menu .= "
    "; $menu .= $this->buildMenu('project', 'start', $params, $project, 'view', 'play', '', 'iframe', true, '', $this->lang->project->start); $menu .= $this->buildMenu('project', 'activate', $params, $project, 'view', 'magic', '', 'iframe', true, '', $this->lang->project->activate); $menu .= $this->buildMenu('project', 'suspend', $params, $project, 'view', 'pause', '', 'iframe', true, '', $this->lang->project->suspend); $menu .= $this->buildMenu('project', 'close', $params, $project, 'view', 'off', '', 'iframe', true, '', $this->lang->close); $menu .= "
    "; $menu .= $this->buildFlowMenu('project', $project, 'view', 'direct'); $menu .= "
    "; $menu .= $this->buildMenu('project', 'edit', "project=$project->id&from=view", $project, 'button', 'edit', '', '', '', '', $this->lang->edit); $menu .= $this->buildMenu('project', 'delete', "project=$project->id&confirm=no&from=view", $project, 'button', 'trash', 'hiddenwin', '', '', '', $this->lang->delete); return $menu; } /** * Build project browse action menu. * * @param object $project * @access public * @return string */ public function buildOperateBrowseMenu($project) { $menu = ''; $params = "projectID=$project->id"; $moduleName = "project"; if($project->status == 'wait' || $project->status == 'suspended') { $menu .= $this->buildMenu($moduleName, 'start', $params, $project, 'browse', 'play', '', 'iframe', true); } $canClose = common::hasPriv($moduleName, 'close'); if($project->status == 'doing') { $menu .= $this->buildMenu($moduleName, 'close', $params, $project, 'browse', 'off', '', 'iframe', true); $canClose = false; } $canActivate = common::hasPriv($moduleName, 'activate'); if($project->status == 'closed') { $menu .= $this->buildMenu($moduleName, 'activate', $params, $project, 'browse', 'magic', '', 'iframe', true); $canActivate = false; } if(common::hasPriv($moduleName, 'suspend') || $canClose || $canActivate) { $menu .= "
    "; $menu .= ""; $menu .= ""; $menu .= "
    "; } $from = $project->from == 'project' ? 'project' : 'pgmproject'; $iframe = $this->app->tab == 'program' ? 'iframe' : ''; $onlyBody = $this->app->tab == 'program' ? true : ''; $dataApp = "data-app=project"; $menu .= $this->buildMenu($moduleName, 'edit', $params, $project, 'browse', 'edit', '', $iframe, $onlyBody, $dataApp); if($this->config->vision != 'lite') { $menu .= $this->buildMenu($moduleName, 'team', $params, $project, 'browse', 'group', '', '', '', $dataApp, $this->lang->execution->team); $menu .= $this->buildMenu('project', 'group', "$params&programID={$project->programID}", $project, 'browse', 'lock', '', '', '', $dataApp); if(common::hasPriv($moduleName, 'manageProducts') || common::hasPriv($moduleName, 'whitelist') || common::hasPriv($moduleName, 'delete')) { $menu .= "
    "; $menu .= ""; $menu .= ""; $menu .= "
    "; } } else { $menu .= $this->buildMenu($moduleName, 'team', $params, $project, 'browse', 'group', '', '', '', $dataApp, $this->lang->execution->team); $menu .= $this->buildMenu('project', 'whitelist', "$params&module=project&from=$from", $project, 'browse', 'shield-check', '', 'btn-action', '', $dataApp); $menu .= $this->buildMenu($moduleName, "delete", $params, $project, 'browse', 'trash', 'hiddenwin', 'btn-action'); } return $menu; } /** * Add plans. * * @param int $projectID * @param array $plans * @access public * @return bool */ public function addPlans(int $projectID, array $plans): bool { $planIdList = array(); foreach($plans as $planList) { foreach($planList as $planID) { $planIdList[$planID] = $planID; } } if(empty($planIdList)) return true; $planStoryGroup = $this->loadModel('story')->getStoriesByPlanIdList($planIdList); foreach($planIdList as $planID) { $planStories = $planProducts = array(); $planStory = isset($planStoryGroup[$planID]) ? $planStoryGroup[$planID] : array(); if(!empty($planStory)) { foreach($planStory as $id => $story) { if($story->status == 'draft' or $story->status == 'reviewing') { unset($planStory[$id]); continue; } $planProducts[$story->id] = $story->product; } $planStories = array_keys($planStory); $this->loadModel('execution')->linkStory($projectID, $planStories, $planProducts); } } return !dao::isError(); } /** * Update plans. * * @param int $projectID * @param array $plans * @access public * @return bool */ public function updatePlans(int $projectID, array $plans): bool { /* Transfer multi dimensional array to one dimensional array. */ $newPlans = array(); if(isset($plans)) { foreach($plans as $planList) { if(is_array($planList)) foreach($planList as $planID) $newPlans[$planID] = $planID; } } if(empty($newPlans)) return true; /* Fetch old plan list. */ $oldPlanList = $this->dao->select('plan')->from(TABLE_PROJECTPRODUCT) ->where('project')->eq($projectID) ->andWhere('plan')->ne(0) ->fetchPairs(); $oldPlans = array(); foreach($oldPlanList as $oldPlanIDList) { if(is_numeric($oldPlanIDList)) $oldPlans[$oldPlanIDList] = $oldPlanIDList; if(!is_numeric($oldPlanIDList)) { $oldPlanIDList = explode(',', $oldPlanIDList); foreach($oldPlanIDList as $oldPlanID) $oldPlans[$oldPlanID] = $oldPlanID; } } if(count($newPlans) != count($oldPlans) || !empty(array_diff($newPlans, $oldPlans))) $this->loadModel('productplan')->linkProject($projectID, $newPlans); return !dao::isError(); } /** * 更新项目排序 * update project order * * @param array $idList * @param string $orderBy * * @access public * @return bool */ public function updateOrder(array $idList, string $orderBy): bool { $projects = $this->dao->select('id,`order`')->from(TABLE_PROJECT) ->where('id')->in($idList) ->orderBy($orderBy) ->fetchPairs('order', 'id'); foreach($projects as $order => $id) { $newID = array_shift($idList); if($id == $newID) continue; $this->dao->update(TABLE_PROJECT) ->set('`order`')->eq($order) ->set('lastEditedBy')->eq($this->app->user->account) ->set('lastEditedDate')->eq(helper::now()) ->where('id')->eq($newID) ->exec(); } return !dao::isError(); } /** * 获取项目集的最小开始时间 * Get program min begin * * @param int $objectID * * @access public * @return string */ public function getProgramMinBegin(int $objectID): string { return $this->dao->select('`begin` as minBegin')->from(TABLE_PROGRAM) ->where('id')->ne($objectID) ->andWhere('deleted')->eq(0) ->andWhere('path')->like("%,{$objectID},%") ->orderBy('begin_asc') ->fetch('minBegin'); } /** * 获取项目集的最大结束时间 * get program max end * * @param int $objectID * * @access public * @return string */ public function getProgramMaxEnd(int $objectID): string { return $this->dao->select('`end` as maxEnd')->from(TABLE_PROGRAM) ->where('id')->ne($objectID) ->andWhere('deleted')->eq(0) ->andWhere('path')->like("%,{$objectID},%") ->andWhere("`end` is true") ->orderBy('end_desc') ->fetch('maxEnd'); } /** * 获取执行的团队成员。 * get execution members. * * @param string $account * @param array $executions * * @access protected * @return array */ protected function getExecutionMembers(string $account, array $executions): array { return $this->dao->select('t1.root,t2.name')->from(TABLE_TEAM)->alias('t1') ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.root=t2.id') ->where('t1.root')->in($executions) ->andWhere('t1.type')->eq('execution') ->andWhere('t1.account')->eq($account) ->fetchPairs(); } }