dao->select('product')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($projectID)->fetchPairs(); $multiLinkedProducts = $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'); return $multiLinkedProducts; } /** * Show accessDenied response. * * @access private * @return void */ public function accessDenied() { if(defined('TUTORIAL')) return true; echo(js::alert($this->lang->project->accessDenied)); $this->session->set('project', ''); return print(js::locate(helper::createLink('project', 'index'))); } /** * 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'; return true; } /** * Check has content for project. * * @param int $projectID * @access public * @return bool */ public function checkHasContent($projectID) { $count = 0; $count += (int)$this->dao->select('count(*) as count')->from(TABLE_PROJECT)->where('parent')->eq($projectID)->fetch('count'); $count += (int)$this->dao->select('count(*) as count')->from(TABLE_TASK)->where('project')->eq($projectID)->fetch('count'); return $count > 0; } /** * Check has children project. * * @param int $projectID * @access public * @return bool */ public function checkHasChildren($projectID) { $count = $this->dao->select('count(*) as count')->from(TABLE_PROGRAM)->where('parent')->eq($projectID)->fetch('count'); return $count > 0; } /** * 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; } /** * 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 = key($projects); if($projectID == 0) $projectID = 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)) { $this->session->set('project', key($projects), $this->app->tab); $this->accessDenied(); } } return $this->session->project; } /* * 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; } /** * Get a project by id. * * @param int $projectID * @param string $type project|sprint,stage * @access public * @return object */ public function getByID($projectID, $type = 'project') { if(defined('TUTORIAL')) return $this->loadModel('tutorial')->getProject(); $project = $this->dao->select('*')->from(TABLE_PROJECT) ->where('id')->eq($projectID) ->beginIF($this->config->system == 'new')->andWhere('`type`')->in($type)->fi() ->fetch(); if(!$project) return false; if($project->end == '0000-00-00') $project->end = ''; $project = $this->loadModel('file')->replaceImgURL($project, 'desc'); return $project; } /** * Get project info. * * @param string $status * @param string $orderBy * @param int $pager * @param int $involved * @access public * @return array */ public function getInfoList($status = 'undone', $orderBy = 'order_desc', $pager = null, $involved = 0) { /* Init vars. */ $projects = $this->loadModel('program')->getProjectList(0, $status, 0, $orderBy, $pager, 0, $involved); if(empty($projects)) return array(); $projectIdList = array_keys($projects); $teams = $this->dao->select('root, count(*) as count')->from(TABLE_TEAM) ->where('root')->in($projectIdList) ->groupBy('root') ->fetchAll('root'); $condition = $this->config->systemMode == 'classic' ? 't2.id as project' : 't2.parent as project'; $estimates = $this->dao->select("$condition, sum(estimate) as estimate")->from(TABLE_TASK)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.execution = t2.id') ->where('t1.parent')->lt(1) ->beginIF($this->config->systemMode == 'new')->andWhere('t2.parent')->in($projectIdList)->fi() ->beginIF($this->config->systemMode == 'classic')->andWhere('t2.id')->in($projectIdList)->fi() ->andWhere('t1.deleted')->eq(0) ->andWhere('t2.deleted')->eq(0) ->groupBy('project') ->fetchAll('project'); $this->app->loadClass('pager', $static = 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->getStats($projectID, 'undone', 0, 0, 30, $orderBy, $pager); $project->teamCount = isset($teams[$projectID]) ? $teams[$projectID]->count : 0; $project->estimate = isset($estimates[$projectID]) ? round($estimates[$projectID]->estimate, 2) : 0; $project->parentName = $this->getParentProgram($project); } return $projects; } /** * Get all parent program of a program. * * @param int $parentID * @access public * @return string */ public function getParentProgram($project) { if($project->parent == 0) return ''; $parentName = $this->dao->select('id,name')->from(TABLE_PROGRAM)->where('id')->in(trim($project->path, ','))->andWhere('grade')->lt($project->grade)->fetchPairs(); $parentProgram = ''; foreach($parentName as $name) $parentProgram .= $name . '/'; $parentProgram = rtrim($parentProgram, '/'); return $parentProgram; } /** * Get project overview for block. * * @param string $queryType byId|byStatus * @param string|int $param * @param string $orderBy * @param int $limit * @param string $excludedModel * @access public * @return array */ public function getOverviewList($queryType = 'byStatus', $param = 'all', $orderBy = 'id_desc', $limit = 15, $excludedModel = '') { $queryType = strtolower($queryType); $projects = $this->dao->select('*')->from(TABLE_PROJECT) ->where('type')->eq('project') ->andWhere('vision')->eq($this->config->vision) ->andWhere('deleted')->eq(0) ->beginIF($excludedModel)->andWhere('model')->ne($excludedModel)->fi() ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->projects)->fi() ->beginIF($queryType == 'bystatus' and $param == 'undone')->andWhere('status')->notIN('done,closed')->fi() ->beginIF($queryType == 'bystatus' and $param != 'all' and $param != 'undone')->andWhere('status')->eq($param)->fi() ->beginIF($queryType == 'byid')->andWhere('id')->eq($param)->fi() ->orderBy($orderBy) ->limit($limit) ->fetchAll('id'); if(empty($projects)) return array(); $projectIdList = array_keys($projects); $teams = $this->dao->select('root, count(*) as teams')->from(TABLE_TEAM) ->where('root')->in($projectIdList) ->andWhere('type')->eq('project') ->groupBy('root')->fetchPairs(); $hours = $this->dao->select('t2.parent as project, ROUND(SUM(t1.consumed), 1) AS consumed, ROUND(SUM(t1.estimate), 1) AS estimate')->from(TABLE_TASK)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.execution = t2.id') ->where('t2.project')->in($projectIdList) ->andWhere('t2.deleted')->eq(0) ->andWhere('t1.deleted')->eq(0) ->andWhere('t1.parent')->lt(1) ->groupBy('t2.project') ->fetchAll('project'); $leftTasks = $this->dao->select('t2.parent as project, count(*) as tasks')->from(TABLE_TASK)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.execution = t2.id') ->where('t2.project')->in($projectIdList) ->andWhere('t2.deleted')->eq(0) ->andWhere('t1.deleted')->eq(0) ->andWhere('t1.status')->in('wait,doing,pause') ->groupBy('t2.project') ->fetchPairs(); $this->loadModel('product'); foreach($projectIdList as $projectID) { $productIdList = $this->product->getProductIDByProject($projectID, false); $allStories[$projectID] = $this->getTotalStoriesByProject($projectID, $productIdList, 'story'); $doneStories[$projectID] = $this->getTotalStoriesByProject($projectID, $productIdList, 'story', 'closed'); $leftStories[$projectID] = $this->getTotalStoriesByProject($projectID, $productIdList, 'story', 'active'); } $leftBugs = $this->getTotalBugByProject($projectIdList, 'active'); $allBugs = $this->getTotalBugByProject($projectIdList, 'all'); $doneBugs = $this->getTotalBugByProject($projectIdList, 'resolved'); $leftTasks = $this->getTotalTaskByProject($projectIdList, 'undone'); $allTasks = $this->getTotalTaskByProject($projectIdList, 'all'); $doneTasks = $this->getTotalTaskByProject($projectIdList, 'done'); foreach($projects as $projectID => $project) { $project->consumed = isset($hours[$projectID]) ? (float)$hours[$projectID]->consumed : 0; $project->estimate = isset($hours[$projectID]) ? (float)$hours[$projectID]->estimate : 0; $project->teamCount = isset($teams[$projectID]) ? $teams[$projectID] : 0; $project->leftTasks = isset($leftTasks[$projectID]) ? $leftTasks[$projectID] : 0; $project->leftBugs = isset($leftBugs[$projectID]) ? $leftBugs[$projectID] : 0; $project->allBugs = isset($allBugs[$projectID]) ? $allBugs[$projectID] : 0; $project->doneBugs = isset($doneBugs[$projectID]) ? $doneBugs[$projectID] : 0; $project->allStories = $allStories[$projectID]; $project->doneStories = $doneStories[$projectID]; $project->leftStories = $leftStories[$projectID]; $project->leftTasks = isset($leftTasks[$projectID]) ? $leftTasks[$projectID] : 0; $project->allTasks = isset($allTasks[$projectID]) ? $allTasks[$projectID] : 0; $project->doneTasks = isset($doneTasks[$projectID]) ? $doneTasks[$projectID] : 0; if(is_float($project->consumed)) $project->consumed = round($project->consumed, 1); if(is_float($project->estimate)) $project->estimate = round($project->estimate, 1); } return $projects; } /** * Get the number of stories associated with the project. * * @param int $projectID * @param array $productIdList * @param string $type story|requirement * @param string $status all|closed|active * @access public * @return int */ public function getTotalStoriesByProject($projectID = 0, $productIdList = array(), $type = 'story', $status = 'all') { return $this->dao->select('count(t2.id) as stories')->from(TABLE_PROJECTSTORY)->alias('t1') ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story=t2.id') ->where('t1.project')->eq($projectID) ->andWhere('t2.type')->eq($type) ->andWhere('t2.product')->in($productIdList) ->beginIF($status == 'active')->andWhere('t2.status')->in('active,changed')->fi() ->beginIF($status == 'closed')->andWhere('t2.status')->eq('closed')->fi() ->beginIF($status == 'closed')->andWhere('t2.closedReason')->eq('done')->fi() ->andWhere('deleted')->eq('0') ->fetch('stories'); } /** * 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_TASKESTIMATE)->alias('t1') ->leftJoin(TABLE_TASK)->alias('t2')->on('t1.task=t2.id') ->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($module, $method, $projectID) { $link = helper::createLink('project', 'index', "projectID=%s"); if(strpos(',project,product,projectstory,story,bug,doc,testcase,testtask,testreport,repo,build,projectrelease,stakeholder,issue,risk,meeting,report,measrecord', ',' . $module . ',') !== false) { if($module == 'project' and $method == 'execution') { $link = helper::createLink($module, $method, "status=all&projectID=%s"); } elseif($module == 'project' and strpos(',bug,testcase,testtask,testreport,build,dynamic,view,manageproducts,team,managemembers,whitelist,addwhitelist,group,', ',' . $method . ',') !== false) { $link = helper::createLink($module, $method, "projectID=%s"); } elseif($module == 'project' and $method == 'managePriv') { $link = helper::createLink($module, 'group', "projectID=%s"); } elseif($module == 'product' and $method == 'showerrornone') { $link = helper::createLink('projectstory', 'story', "projectID=%s"); } elseif($module == 'projectstory' and $method == 'story') { $link = helper::createLink($module, $method, "projectID=%s"); } elseif($module == 'projectstory' and $method == 'linkstory') { $link = helper::createLink($module, $method, "projectID=%s"); } elseif($module == 'projectstory' and $method = 'track') { $link = helper::createLink($module, $method, "projectID=%s"); } elseif($module == 'bug') { if($method == 'create') { $link = helper::createLink($module, $method, "productID=0&branch=0&extras=projectID=%s"); } elseif($method == 'edit') { $link = helper::createLink('project', 'bug', "projectID=%s"); } } elseif($module == 'story') { if($method == 'change' or $method == 'create') { $link = helper::createLink('projectstory', 'story', "projectID=%s"); } elseif($method == 'zerocase') { $link = helper::createLink('project', 'testcase', "projectID=%s"); } } elseif($module == 'testcase') { $link = helper::createLink('project', 'testcase', "projectID=%s"); } elseif($module == 'testtask') { if($method == 'browseunits') { $link = helper::createLink('project', 'testcase', "projectID=%s"); } else { $link = helper::createLink('project', 'testtask', "projectID=%s"); } } elseif($module == 'testreport') { $link = helper::createLink('project', 'testreport', "projectID=%s"); } elseif($module == 'repo') { $link = helper::createLink($module, 'browse', "repoID=&branchID=&objectID=%s#app=project"); } elseif($module == 'doc') { $link = helper::createLink($module, 'tablecontents', "type=project&objectID=%s#app=project"); } elseif($module == 'build') { if($method == 'create') { $link = helper::createLink($module, $method, "executionID=&productID=&projectID=%s#app=project"); } else { $link = helper::createLink('project', 'build', "projectID=%s"); } } elseif($module == 'projectrelease') { if($method == 'create') { $link = helper::createLink($module, $method, "projectID=%s"); } else { $link = helper::createLink('projectrelease', 'browse', "projectID=%s"); } } elseif($module == 'stakeholder') { if($method == 'create') { $link = helper::createLink($module, $method, "projectID=%s"); } else { $link = helper::createLink($module, 'browse', "projectID=%s"); } } elseif(strpos("issue,risk,meeting,report,measrecord", $module) !== false) { if($method == 'projectsummary') { $link = helper::createLink($module, $method, "projectID=%s#app=project"); } else { $link = helper::createLink($module, 'browse', "projectID=%s"); } } } if(in_array($module, $this->config->waterfallModules)) { $link = helper::createLink($module, 'browse', "projectID=%s"); if($module == 'reviewissue') { $link = helper::createLink($module, 'issue', "projectID=%s"); } elseif($module == 'cm' and $method = 'report') { $link = helper::createLink($module, $method, "projectID=%s"); } elseif($module == 'weekly' and $method = 'index') { $link = helper::createLink($module, $method, "projectID=%s"); } elseif($module == 'milestone' and $method = 'index') { $link = helper::createLink($module, $method, "projectID=%s"); } elseif($module == 'workestimation' and $method = 'index') { $link = helper::createLink($module, $method, "projectID=%s"); } elseif($module == 'durationestimation' and $method = 'index') { $link = helper::createLink($module, $method, "projectID=%s"); } elseif($module == 'budget' and $method = 'summary') { $link = helper::createLink($module, $method, "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); $storyCount = $this->dao->select('count(t2.story) as storyCount')->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) ->fetch('storyCount'); $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')->ne('0000-00-00') ->andWhere('deadline')->lt(helper::today()) ->andWhere('status')->in('wait,doing') ->andWhere('deleted')->eq(0) ->fetch('count'); $statData = new stdclass(); $statData->storyCount = $storyCount; $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 by programID. * * @param int $programID * @param status $status all|wait|doing|suspended|closed|noclosed * @param bool $isQueryAll * @param string $orderBy * @param string $excludedModel * @access public * @return object */ public function getPairsByProgram($programID = '', $status = 'all', $isQueryAll = false, $orderBy = 'order_asc', $excludedModel = '') { 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($programID !== '')->andWhere('parent')->eq($programID)->fi() ->beginIF($status != 'all' and $status != 'noclosed')->andWhere('status')->eq($status)->fi() ->beginIF($excludedModel)->andWhere('model')->ne($excludedModel)->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(); $projects = $this->dao->select('id')->from(TABLE_PROJECT) ->where('type')->eq('project') ->andWhere('deleted')->eq(0) ->andWhere('path')->like("{$parentProgram->path}%") ->fetchPairs('id'); return $projects; } /** * Get project by id list. * * @param array $projectIdList * @access public * @return object */ public function getByIdList($projectIdList = array()) { return $this->dao->select('*')->from(TABLE_PROJECT) ->where('type')->eq('project') ->andWhere('id')->in($projectIdList) ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->projects)->fi() ->fetchAll('id'); } /** * Get project pairs by id list. * * @param array $projectIdList * @param string $mode * @access public * @return array */ public function getPairsByIdList($projectIdList = array(), $mode = '') { 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 $mode != 'all')->andWhere('id')->in($this->app->user->view->projects)->fi() ->beginIF($mode != 'all' and !empty($mode))->andWhere('model')->in($mode)->fi() ->fetchPairs('id', 'name'); } /** * Get associated bugs by project. * * @param array $projectIdList * @param string $status active|resolved|all * @access public * @return array */ public function getTotalBugByProject($projectIdList, $status) { return $this->dao->select('project, count(*) as bugs')->from(TABLE_BUG) ->where('project')->in($projectIdList) ->andWhere('deleted')->eq(0) ->beginIF($status != 'all')->andWhere('status')->eq($status)->fi() ->groupBy('project') ->fetchPairs('project'); } /** * Get associated tasks by project. * * @param array $projectIdList * @param string $status all|done|undone * @access public * @return array */ public function getTotalTaskByProject($projectIdList, $status) { return $this->dao->select('project, count(*) as tasks')->from(TABLE_TASK) ->where('project')->in($projectIdList) ->andWhere('deleted')->eq(0) ->beginIF($status == 'done')->andWhere('status')->in('done,closed')->fi() ->beginIF($status == 'undone')->andWhere('status')->in('wait,pause,cancel')->fi() ->groupBy('project') ->fetchPairs('project'); } /** * Get branches by project id. * * @param int $projectID * @access public * @return array */ public function getBranchesByProject($projectID) { return $this->dao->select('*')->from(TABLE_PROJECTPRODUCT) ->where('project')->eq($projectID) ->fetchGroup('product', 'branch'); } /** * Get branch group by project id. * * @param int $projectID * @param array|string $productIdList * @access public * @return array */ public function getBranchGroupByProject($projectID, $productIdList) { 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'); } /* * Build search form * * @param int $queryID * @param string $actionURL * * @return 0 * */ public function buildSearchFrom($queryID, $actionURL) { $this->config->project->search['queryID'] = $queryID; $this->config->project->search['actionURL'] = $actionURL; $programPairs = array(0 => ''); $programPairs += $this->loadModel('program')->getPairs(); $this->config->project->search['params']['parent']['values'] = $programPairs; $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->getByID($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(); $projects = $this->dao->select('id, name, path')->from(TABLE_PROJECT) ->where('type')->eq('project') ->andWhere('deleted')->eq('0') ->andWhere('vision')->eq($this->config->vision) ->beginIF($programID)->andWhere('parent')->eq($programID)->fi() ->beginIF($this->config->vision)->andWhere('vision')->eq($this->config->vision)->fi() ->beginIF($model != 'all')->andWhere('model')->eq($model)->fi() ->beginIF(strpos($param, 'noclosed') !== false)->andWhere('status')->ne('closed')->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; $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('product, 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); $lastMenu = "\n"; return $lastMenu; } /** * 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'"); } /** * Create a project. * * @access public * @return int|bool */ public function create() { $project = fixer::input('post') ->callFunc('name', 'trim') ->setDefault('status', 'wait') ->setIF($this->post->delta == 999, 'end', LONG_TIME) ->setIF($this->post->delta == 999, 'days', 0) ->setIF($this->post->acl == 'open', 'whitelist', '') ->setIF(!isset($_POST['whitelist']), 'whitelist', '') ->setDefault('openedBy', $this->app->user->account) ->setDefault('openedDate', helper::now()) ->setDefault('team', substr($this->post->name, 0, 30)) ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', helper::now()) ->add('type', 'project') ->join('whitelist', ',') ->stripTags($this->config->project->editor->create['id'], $this->config->allowedTags) ->remove('products,branch,plans,delta,newProduct,productName,future,contactListMenu,teamMembers') ->get(); $linkedProductsCount = 0; if(isset($_POST['products'])) { foreach($_POST['products'] as $product) { if(!empty($product)) $linkedProductsCount++; } } $program = new stdClass(); if($project->parent) { $program = $this->dao->select('*')->from(TABLE_PROGRAM)->where('id')->eq($project->parent)->fetch(); if($program) { /* Child project begin cannot less than parent. */ if($project->begin < $program->begin) dao::$errors['begin'] = sprintf($this->lang->project->beginGreateChild, $program->begin); /* When parent set end then child project end cannot greater than parent. */ if($program->end != '0000-00-00' and $project->end > $program->end) dao::$errors['end'] = sprintf($this->lang->project->endLetterChild, $program->end); if(dao::isError()) return false; } /* The budget of a child project cannot beyond the remaining budget of the parent program. */ $project->budgetUnit = $program->budgetUnit; if(isset($project->budget) and $program->budget != 0) { $availableBudget = $this->loadModel('program')->getBudgetLeft($program); if($availableBudget > 0 and $project->budget > $availableBudget) dao::$errors['budget'] = $this->lang->program->beyondParentBudget; } /* Judge products not empty. */ if(empty($linkedProductsCount) and !isset($_POST['newProduct'])) { dao::$errors[] = $this->lang->project->productNotEmpty; return false; } } /* Judge workdays is legitimate. */ $workdays = helper::diffDate($project->end, $project->begin) + 1; if(isset($project->days) and $project->days > $workdays) { dao::$errors['days'] = sprintf($this->lang->project->workdaysExceed, $workdays); return false; } if(!empty($project->budget)) { if(!is_numeric($project->budget)) { dao::$errors['budget'] = sprintf($this->lang->project->budgetNumber); return false; } else if(is_numeric($project->budget) and ($project->budget < 0)) { dao::$errors['budget'] = sprintf($this->lang->project->budgetGe0); return false; } else { $project->budget = round((float)$this->post->budget, 2); } } /* When select create new product, product name cannot be empty and duplicate. */ if(isset($_POST['newProduct'])) { if(empty($_POST['productName'])) { $this->app->loadLang('product'); dao::$errors['productName'] = sprintf($this->lang->error->notempty, $this->lang->product->name); return false; } else { $existProductName = $this->dao->select('name')->from(TABLE_PRODUCT)->where('name')->eq($_POST['productName'])->fetch('name'); if(!empty($existProductName)) { dao::$errors['productName'] = $this->lang->project->existProductName; return false; } } } $requiredFields = $this->config->project->create->requiredFields; if($this->post->delta == 999) $requiredFields = trim(str_replace(',end,', ',', ",{$requiredFields},"), ','); /* Redefines the language entries for the fields in the project table. */ foreach(explode(',', $requiredFields) as $field) { if(isset($this->lang->project->$field)) $this->lang->project->$field = $this->lang->project->$field; } $project = $this->loadModel('file')->processImgURL($project, $this->config->project->editor->create['id'], $this->post->uid); $this->dao->insert(TABLE_PROJECT)->data($project) ->autoCheck() ->batchcheck($requiredFields, 'notempty') ->checkIF(!empty($project->name), 'name', 'unique', "`type`='project' and `parent` = $project->parent and `model` = '{$project->model}'") ->checkIF(!empty($project->code), 'code', 'unique', "`type`='project' and `model` = '{$project->model}'") ->checkIF($project->end != '', 'end', 'gt', $project->begin) ->checkFlow() ->exec(); /* Add the creater to the team. */ if(!dao::isError()) { $projectID = $this->dao->lastInsertId(); /* Set team of project. */ $members = isset($_POST['teamMembers']) ? $_POST['teamMembers'] : array(); array_push($members, $project->PM, $project->openedBy); $members = array_unique($members); $roles = $this->loadModel('user')->getUserRoles(array_values($members)); $teamMembers = array(); foreach($members as $account) { if(empty($account)) continue; $member = new stdClass(); $member->root = $projectID; $member->type = 'project'; $member->account = $account; $member->role = zget($roles, $account, ''); $member->join = helper::now(); $member->days = zget($project, 'days', 0); $member->hours = $this->config->execution->defaultWorkhours; $this->dao->insert(TABLE_TEAM)->data($member)->exec(); $teamMembers[$account] = $member; } if($this->config->systemMode == 'new') $this->loadModel('execution')->addProjectMembers($projectID, $teamMembers); $whitelist = explode(',', $project->whitelist); $this->loadModel('personnel')->updateWhitelist($whitelist, 'project', $projectID); /* Create doc lib. */ $this->app->loadLang('doc'); $authorizedUsers = array(); if($project->parent and $project->acl == 'program') { $stakeHolders = $this->loadModel('stakeholder')->getStakeHolderPairs($project->parent); $authorizedUsers = array_keys($stakeHolders); foreach(explode(',', $project->whitelist) as $user) { if(empty($user)) continue; $authorizedUsers[$user] = $user; } $authorizedUsers[$project->PM] = $project->PM; $authorizedUsers[$project->openedBy] = $project->openedBy; $authorizedUsers[$program->PM] = $program->PM; $authorizedUsers[$program->openedBy] = $program->openedBy; } $lib = new stdclass(); $lib->project = $projectID; $lib->name = $this->lang->doclib->main['project']; $lib->type = 'project'; $lib->main = '1'; $lib->acl = $project->acl != 'program' ? $project->acl : 'custom'; $lib->users = ',' . implode(',', array_filter($authorizedUsers)) . ','; $lib->vision = zget($project, 'vision', 'rnd'); $this->dao->insert(TABLE_DOCLIB)->data($lib)->exec(); $this->updateProducts($projectID); if(isset($_POST['newProduct']) or (!$project->parent and empty($linkedProductsCount))) { /* If parent not empty, link products or create products. */ $product = new stdclass(); $product->name = $this->post->productName ? $this->post->productName : $project->name; $product->bind = $this->post->parent ? 0 : 1; $product->program = $project->parent ? current(array_filter(explode(',', $program->path))) : 0; $product->acl = $project->acl == 'open' ? 'open' : 'private'; $product->PO = $project->PM; $product->createdBy = $this->app->user->account; $product->createdDate = helper::now(); $product->status = 'normal'; $product->createdVersion = $this->config->version; $product->vision = zget($project, 'vision', 'rnd'); $this->dao->insert(TABLE_PRODUCT)->data($product)->exec(); $productID = $this->dao->lastInsertId(); $this->loadModel('action')->create('product', $productID, 'opened'); $this->dao->update(TABLE_PRODUCT)->set('`order`')->eq($productID * 5)->where('id')->eq($productID)->exec(); if($product->acl != 'open') $this->loadModel('user')->updateUserView($productID, 'product'); $projectProduct = new stdclass(); $projectProduct->project = $projectID; $projectProduct->product = $productID; $this->dao->insert(TABLE_PROJECTPRODUCT)->data($projectProduct)->exec(); /* Create doc lib. */ $this->app->loadLang('doc'); $lib = new stdclass(); $lib->product = $productID; $lib->name = $this->lang->doclib->main['product']; $lib->type = 'product'; $lib->main = '1'; $lib->acl = 'default'; $this->dao->insert(TABLE_DOCLIB)->data($lib)->exec(); } /* 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. */ $groupPriv = $this->dao->select('t1.*')->from(TABLE_USERGROUP)->alias('t1') ->leftJoin(TABLE_GROUP)->alias('t2')->on('t1.`group` = t2.id') ->where('t1.account')->eq($this->app->user->account) ->andWhere('t2.role')->eq('projectAdmin') ->fetch(); if(!empty($groupPriv)) { $newProject = $groupPriv->project . ",$projectID"; $this->dao->update(TABLE_USERGROUP)->set('project')->eq($newProject)->where('account')->eq($groupPriv->account)->andWhere('`group`')->eq($groupPriv->group)->exec(); } else { $projectAdminID = $this->dao->select('id')->from(TABLE_GROUP)->where('role')->eq('projectAdmin')->fetch('id'); $groupPriv = new stdclass(); $groupPriv->account = $this->app->user->account; $groupPriv->group = $projectAdminID; $groupPriv->project = $projectID; $this->dao->replace(TABLE_USERGROUP)->data($groupPriv)->exec(); } if($project->acl != 'open') $this->loadModel('user')->updateUserView($projectID, 'project'); return $projectID; } } /** * Update project. * * @param int $projectID * @access public * @return array */ public function update($projectID = 0) { $oldProject = $this->dao->findById($projectID)->from(TABLE_PROJECT)->fetch(); $linkedProducts = $this->dao->select('product')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($projectID)->fetchPairs(); $_POST['products'] = isset($_POST['products']) ? array_filter($_POST['products']) : $linkedProducts; $project = fixer::input('post') ->add('id', $projectID) ->callFunc('name', 'trim') ->setDefault('team', mb_substr($this->post->name, 0, 30)) ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', helper::now()) ->setIF($this->post->delta == 999, 'end', LONG_TIME) ->setIF($this->post->delta == 999, 'days', 0) ->setIF($this->post->begin == '0000-00-00', 'begin', '') ->setIF($this->post->end == '0000-00-00', 'end', '') ->setIF($this->post->future, 'budget', 0) ->setIF($this->post->budget != 0, 'budget', round((float)$this->post->budget, 2)) ->setIF(!isset($_POST['whitelist']), 'whitelist', '') ->join('whitelist', ',') ->stripTags($this->config->project->editor->edit['id'], $this->config->allowedTags) ->remove('products,branch,plans,delta,future,contactListMenu,teamMembers') ->get(); if($project->parent) { $program = $this->dao->select('*')->from(TABLE_PROGRAM)->where('id')->eq($project->parent)->fetch(); if($program) { /* Child project begin cannot less than parent. */ if($project->begin < $program->begin) dao::$errors['begin'] = sprintf($this->lang->project->beginGreateChild, $program->begin); /* When parent set end then child project end cannot greater than parent. */ if($program->end != '0000-00-00' and $project->end > $program->end) dao::$errors['end'] = sprintf($this->lang->project->endLetterChild, $program->end); if(dao::isError()) return false; } /* The budget of a child project cannot beyond the remaining budget of the parent project. */ $project->budgetUnit = $program->budgetUnit; if($project->budget != 0 and $program->budget != 0) { $availableBudget = $this->loadModel('program')->getBudgetLeft($program); if($project->budget > $availableBudget + $oldProject->budget) dao::$errors['budget'] = $this->lang->program->beyondParentBudget; } } /* Judge products not empty. */ $linkedProductsCount = 0; foreach($_POST['products'] as $product) { if(!empty($product)) $linkedProductsCount++; } if(empty($linkedProductsCount)) { dao::$errors[] = $this->lang->project->errorNoProducts; return false; } /* Judge workdays is legitimate. */ $workdays = helper::diffDate($project->end, $project->begin) + 1; if(isset($project->days) and $project->days > $workdays) { dao::$errors['days'] = sprintf($this->lang->project->workdaysExceed, $workdays); return false; } $project = $this->loadModel('file')->processImgURL($project, $this->config->project->editor->edit['id'], $this->post->uid); $requiredFields = $this->config->project->edit->requiredFields; if($this->post->delta == 999) $requiredFields = trim(str_replace(',end,', ',', ",{$requiredFields},"), ','); /* Redefines the language entries for the fields in the project table. */ foreach(explode(',', $requiredFields) as $field) { if(isset($this->lang->project->$field)) $this->lang->project->$field = $this->lang->project->$field; } $this->dao->update(TABLE_PROJECT)->data($project) ->autoCheck($skipFields = 'begin,end') ->batchcheck($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` = $project->parent and `model` = '{$project->model}'") ->checkIF(!empty($project->code), 'code', 'unique', "id != $projectID and `type` = 'project' and `model` = '{$project->model}'") ->checkFlow() ->where('id')->eq($projectID) ->exec(); if(dao::isError()) return false; /* Get team and language item. */ $this->loadModel('user'); $team = $this->user->getTeamMemberPairs($projectID, 'project'); $members = isset($_POST['teamMembers']) ? $_POST['teamMembers'] : array(); array_push($members, $project->PM); $members = array_unique($members); $roles = $this->user->getUserRoles(array_values($members)); $teamMembers = array(); foreach($members as $account) { if(empty($account) or isset($team[$account])) continue; $member = new stdclass(); $member->root = (int)$projectID; $member->account = $account; $member->join = helper::today(); $member->role = zget($roles, $account, ''); $member->days = zget($project, 'days', 0); $member->type = 'project'; $member->hours = $this->config->execution->defaultWorkhours; $this->dao->replace(TABLE_TEAM)->data($member)->exec(); $teamMembers[$account] = $member; } if($oldProject->model == 'kanban') { $this->dao->delete()->from(TABLE_TEAM) ->where('root')->eq((int)$projectID) ->andWhere('type')->eq('project') ->andWhere('account')->in(array_keys($team)) ->andWhere('account')->notin(array_values($members)) ->andWhere('account')->ne($oldProject->openedBy) ->exec(); } if(!empty($projectID) and !empty($teamMembers)) $this->loadModel('execution')->addProjectMembers($projectID, $teamMembers); if(!dao::isError()) { $this->updateProductProgram($oldProject->parent, $project->parent, $_POST['products']); $this->updateProducts($projectID, $_POST['products']); $this->file->updateObjectID($this->post->uid, $projectID, 'project'); $whitelist = explode(',', $project->whitelist); $this->loadModel('personnel')->updateWhitelist($whitelist, 'project', $projectID); if($project->acl != 'open') { $this->loadModel('user')->updateUserView($projectID, 'project'); $executions = $this->dao->select('id')->from(TABLE_EXECUTION)->where('project')->eq($projectID)->fetchPairs('id', 'id'); if($executions) $this->user->updateUserView($executions, 'sprint'); } if($oldProject->parent != $project->parent) $this->loadModel('program')->processNode($projectID, $project->parent, $oldProject->path, $oldProject->grade); /* Fix whitelist changes. */ $oldWhitelist = explode(',', $oldProject->whitelist) and $oldWhitelist = array_filter($oldWhitelist); $newWhitelist = explode(',', $project->whitelist) and $newWhitelist = array_filter($newWhitelist); if(count($oldWhitelist) == count($newWhitelist) and count(array_diff($oldWhitelist, $newWhitelist)) == 0) unset($project->whitelist); /* Add linkedproducts changes. */ $oldProject->linkedProducts = implode(',', $linkedProducts); $project->linkedProducts = implode(',', $_POST['products']); 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]; $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]->PM = $data->PMs[$projectID]; $projects[$projectID]->begin = $data->begins[$projectID]; $projects[$projectID]->end = isset($data->ends[$projectID]) ? $data->ends[$projectID] : LONG_TIME; $projects[$projectID]->days = $data->dayses[$projectID]; $projects[$projectID]->acl = $data->acls[$projectID]; $projects[$projectID]->lastEditedBy = $this->app->user->account; $projects[$projectID]->lastEditedDate = helper::now(); if($projects[$projectID]->parent) { $parentProject = $this->dao->select('*')->from(TABLE_PROGRAM)->where('id')->eq($projects[$projectID]->parent)->fetch(); if($parentProject) { /* Child project begin cannot less than parent. */ if($projects[$projectID]->begin < $parentProject->begin) dao::$errors['begin'] = sprintf($this->lang->project->beginGreateChild, $parentProject->begin); /* When parent set end then child project end cannot greater than parent. */ if($parentProject->end != '0000-00-00' and $projects[$projectID]->end > $parentProject->end) dao::$errors['end'] = sprintf($this->lang->project->endLetterChild, $parentProject->end); } } 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 print(js::error(dao::getError())); foreach($projects as $projectID => $project) { $oldProject = $oldProjects[$projectID]; $parentID = !isset($project->parent) ? $oldProject->parent : $project->parent; $this->dao->update(TABLE_PROJECT)->data($project) ->autoCheck($skipFields = '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}'") ->checkIF(!empty($project->code), 'code', 'unique', "id != $projectID and `type`='project' and `model` = '{$project->model}'") ->checkFlow() ->where('id')->eq($projectID) ->exec(); if(dao::isError()) helper::end(js::error('project#' . $projectID . dao::getError(true))); if(!dao::isError()) { $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); } $allChanges[$projectID] = common::createChanges($oldProject, $project); } return $allChanges; } /** * Start project. * * @param int $projectID * @param string $type * @access public * @return array */ public function start($projectID, $type = 'project') { $oldProject = $this->getById($projectID, $type); $now = helper::now(); $project = fixer::input('post') ->add('id', $projectID) ->setDefault('status', 'doing') ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', $now) ->remove('comment')->get(); $this->dao->update(TABLE_PROJECT)->data($project) ->autoCheck() ->check($this->config->project->start->requiredFields, 'notempty') ->checkIF($project->realBegan != '', 'realBegan', 'le', helper::today()) ->checkFlow() ->where('id')->eq((int)$projectID) ->exec(); /* When it has multiple errors, only the first one is prompted */ if(dao::isError() and count(dao::$errors['realBegan']) > 1) dao::$errors['realBegan'] = dao::$errors['realBegan'][0]; if(!dao::isError()) return common::createChanges($oldProject, $project); } /** * Put project off. * * @param int $projectID * @access public * @return void */ public function putoff($projectID) { $oldProject = $this->getById($projectID); $now = helper::now(); $project = fixer::input('post') ->add('id', $projectID) ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', $now) ->remove('comment') ->get(); $this->dao->update(TABLE_PROJECT)->data($project) ->autoCheck() ->checkFlow() ->where('id')->eq((int)$projectID) ->exec(); if(!dao::isError()) return common::createChanges($oldProject, $project); } /** * Suspend project. * * @param int $projectID * @access public * @return void */ public function suspend($projectID) { $oldProject = $this->getById($projectID); $project = fixer::input('post') ->add('id', $projectID) ->setDefault('status', 'suspended') ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', helper::now()) ->setDefault('suspendedDate', helper::today()) ->remove('comment')->get(); $this->dao->update(TABLE_PROJECT)->data($project) ->autoCheck() ->checkFlow() ->where('id')->eq((int)$projectID) ->exec(); if(!dao::isError()) return common::createChanges($oldProject, $project); } /** * Activate project. * * @param int $projectID * @access public * @return void */ public function activate($projectID) { $oldProject = $this->getById($projectID); $now = helper::now(); $project = fixer::input('post') ->add('id', $projectID) ->setDefault('realEnd','') ->setDefault('status', 'doing') ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', $now) ->setIF($oldProject->realBegan == '0000-00-00', 'realBegan', helper::today()) ->remove('comment,readjustTime,readjustTask') ->get(); if(!$this->post->readjustTime) { unset($project->begin); unset($project->end); } $this->dao->update(TABLE_PROJECT)->data($project) ->autoCheck() ->checkFlow() ->where('id')->eq((int)$projectID) ->exec(); /* Readjust task. */ if($this->post->readjustTime and $this->post->readjustTask) { $beginTimeStamp = strtotime($project->begin); $tasks = $this->dao->select('id,estStarted,deadline,status')->from(TABLE_TASK) ->where('deadline')->ne('0000-00-00') ->andWhere('status')->in('wait,doing') ->andWhere('project')->eq($projectID) ->fetchAll(); foreach($tasks as $task) { if($task->status == 'wait' and !helper::isZeroDate($task->estStarted)) { $taskDays = helper::diffDate($task->deadline, $task->estStarted); $taskOffset = helper::diffDate($task->estStarted, $oldProject->begin); $estStartedTimeStamp = $beginTimeStamp + $taskOffset * 24 * 3600; $estStarted = date('Y-m-d', $estStartedTimeStamp); $deadline = date('Y-m-d', $estStartedTimeStamp + $taskDays * 24 * 3600); if($estStarted > $project->end) $estStarted = $project->end; if($deadline > $project->end) $deadline = $project->end; $this->dao->update(TABLE_TASK)->set('estStarted')->eq($estStarted)->set('deadline')->eq($deadline)->where('id')->eq($task->id)->exec(); } else { $taskOffset = helper::diffDate($task->deadline, $oldProject->begin); $deadline = date('Y-m-d', $beginTimeStamp + $taskOffset * 24 * 3600); if($deadline > $project->end) $deadline = $project->end; $this->dao->update(TABLE_TASK)->set('deadline')->eq($deadline)->where('id')->eq($task->id)->exec(); } } } if(!dao::isError()) return common::createChanges($oldProject, $project); } /** * Close project. * * @param int $projectID * @access public * @return array */ public function close($projectID) { $oldProject = $this->getById($projectID); $now = helper::now(); $project = fixer::input('post') ->add('id', $projectID) ->setDefault('status', 'closed') ->setDefault('closedBy', $this->app->user->account) ->setDefault('closedDate', $now) ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', $now) ->remove('comment') ->get(); $this->lang->error->ge = $this->lang->project->ge; $this->dao->update(TABLE_PROJECT)->data($project) ->autoCheck() ->check($this->config->project->close->requiredFields, 'notempty') ->checkIF($project->realEnd != '', 'realEnd', 'le', helper::today()) ->checkIF($project->realEnd != '', 'realEnd', 'ge', $oldProject->realBegan) ->checkFlow() ->where('id')->eq((int)$projectID) ->exec(); /* When it has multiple errors, only the first one is prompted */ if(dao::isError() and count(dao::$errors['realEnd']) > 1) dao::$errors['realEnd'] = dao::$errors['realEnd'][0]; if(!dao::isError()) { $this->loadModel('score')->create('project', 'close', $oldProject); return common::createChanges($oldProject, $project); } } /** * 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($projectID, $account, $removeExecution = 'no') { $this->dao->delete()->from(TABLE_TEAM)->where('root')->eq((int)$projectID)->andWhere('type')->eq('project')->andWhere('account')->eq($account)->exec(); $this->loadModel('user')->updateUserView($projectID, 'project', array($account)); if($removeExecution == 'yes') { $executions = $this->loadModel('execution')->getByProject($projectID, 'undone', 0, true); $this->dao->delete()->from(TABLE_TEAM)->where('root')->in(array_keys($executions))->andWhere('type')->eq('execution')->andWhere('account')->eq($account)->exec(); $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->getByID($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(); $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; 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; } $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->loadModel('product')->getProductPairsByProject($projectID); $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($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(); } } /** * 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; $id = $col->id; $projectLink = $this->config->systemMode == 'new' ? helper::createLink('project', 'index', "projectID=$project->id", '', '', $project->id) : helper::createLink('execution', 'task', "projectID=$project->id"); if($col->show) { $title = ''; $class = "c-$id" . (in_array($id, array('budget', 'teamCount', 'estimate', 'consume')) ? ' c-number' : ''); if($id == 'id') $class .= ' cell-id'; if($id == 'code') { $class .= ' c-name'; $title = "title={$project->code}"; } if($id == 'name') { $class .= ' text-left'; $title = "title='{$project->name}'"; } if($id == 'end') { $project->end = $project->end == LONG_TIME ? $this->lang->project->longTime : $project->end; $class .= ' c-name'; $title = "title='{$project->end}'"; } if($id == '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($id == 'estimate') $title = "title='{$project->hours->totalEstimate} {$this->lang->execution->workHour}'"; if($id == 'consume') $title = "title='{$project->hours->totalConsumed} {$this->lang->execution->workHour}'"; if($id == 'surplus') $title = "title='{$project->hours->totalLeft} {$this->lang->execution->workHour}'"; echo ""; if($this->config->edition != 'open') $this->loadModel('flow')->printFlowCell('project', $project, $id); switch($id) { 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 = ''; if($project->model === 'waterfall') $prefix = "{$this->lang->project->waterfall} "; if($project->model === 'scrum') $prefix = "{$this->lang->project->scrum} "; if($project->model === 'kanban') $prefix = "{$this->lang->project->kanban} "; if(isset($project->delay)) $suffix = "{$this->lang->project->statusList['delay']}"; if(!empty($suffix) or !empty($prefix)) echo '
    '; if(!empty($prefix)) echo $prefix; echo html::a($projectLink, $project->name, '', "class='text-ellipsis text-primary'"); 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 : ''; $PMLink = helper::createLink('user', 'profile', "userID=$userID", '', true); $userName = zget($users, $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 '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; } echo ''; } } /** * Convert budget unit. * * @param int $budget * @access public * @return void */ public function getBudgetWithUnit($budget) { if($budget < 10000) { $budget = round((float)$budget, 2); $unit = ''; } elseif($budget < 100000000 and $budget >= 10000) { $budget = round((float)$budget/10000, 2); $unit = $this->lang->project->tenThousand; } else { $budget = round((float)$budget/100000000, 2); $unit = $this->lang->project->hundredMillion; } $projectBudget = in_array($this->app->getClientLang(), array('zh-cn','zh-tw')) ? $budget . $unit : round((float)$budget, 2); return $projectBudget; } /** * Update products of a project. * * @param int $projectID * @param array $products * @access public * @return void */ public function updateProducts($projectID, $products = '') { $this->loadModel('user'); $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(); $members = array_keys($this->getTeamMembers($projectID)); 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(isset($existedProducts[$productID][$branch])) continue; if(isset($oldProjectProducts[$productID][$branch])) { $oldProjectProduct = $oldProjectProducts[$productID][$branch]; $oldPlan = $oldProjectProduct->plan; } $data = new stdclass(); $data->project = $projectID; $data->product = $productID; $data->branch = $branch; $data->plan = isset($plans[$productID][$branch]) ? $plans[$productID][$branch] : $oldPlan; $this->dao->insert(TABLE_PROJECTPRODUCT)->data($data)->exec(); $existedProducts[$productID][$branch] = true; } /* Delete the execution linked products that is not linked with the execution. */ if((int)$projectID > 0) { $executions = $this->dao->select('id')->from(TABLE_EXECUTION)->where('project')->eq((int)$projectID)->fetchPairs('id'); $this->dao->delete()->from(TABLE_PROJECTPRODUCT)->where('project')->in($executions)->andWhere('product')->notin($products)->exec(); } $oldProductKeys = array_keys($oldProjectProducts); $needUpdate = array_merge(array_diff($oldProductKeys, $products), array_diff($products, $oldProductKeys)); if($needUpdate) $this->user->updateUserView($needUpdate, 'product', $members); } /** * Update userview for involved product and execution. * * @param int $projectID * @param array $users * @access public * @return void */ public function updateInvolvedUserView($projectID, $users = array()) { $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->getByID($projectID); $type = $this->config->systemMode == 'new' ? $project->type : 'project'; 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($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($this->config->systemMode == 'new') { 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 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 project stats. * * @param int $projectID * @param string $status * @param int $productID * @param int $itemCounts * @param string $orderBy * @param object $pager * @access public * @return array */ public function getStats($projectID = 0, $status = 'undone', $productID = 0, $branch = 0, $itemCounts = 30, $orderBy = 'id_asc', $pager = null) { if(empty($productID)) { $myExecutionIDList = array(); if($status == 'involved') { $myExecutionIDList = $this->dao->select('root')->from(TABLE_TEAM) ->where('account')->eq($this->app->user->account) ->andWhere('type')->eq('execution') ->fetchPairs(); } $executions = $this->dao->select('t1.*,t2.name projectName, t2.model as projectModel')->from(TABLE_EXECUTION)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id') ->where('t1.type')->in('sprint,stage,kanban') ->beginIF($projectID != 0)->andWhere('t1.project')->eq($projectID)->fi() ->beginIF($projectID == 0 and $this->config->vision)->andWhere('t1.vision')->eq($this->config->vision)->fi() ->beginIF(!empty($myExecutionIDList))->andWhere('t1.id')->in(array_keys($myExecutionIDList))->fi() ->beginIF($status == 'undone')->andWhere('t1.status')->notIN('done,closed')->fi() ->beginIF($status != 'all' and $status != 'undone' and $status != 'involved')->andWhere('t1.status')->eq($status)->fi() ->beginIF(!$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->sprints)->fi() ->andWhere('t1.vision')->eq($this->config->vision) ->andWhere('t1.deleted')->eq('0') ->orderBy($orderBy) ->page($pager) ->fetchAll('id'); } else { $executions = $this->dao->select('t2.*,t3.name projectName, t3.model as projectModel')->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.project=t2.id') ->leftJoin(TABLE_PROJECT)->alias('t3')->on('t2.project=t3.id') ->where('t1.product')->eq($productID) ->beginIF($projectID)->andWhere('t2.project')->eq($projectID)->fi() ->beginIF($status == 'undone')->andWhere('t2.status')->notIN('done,closed')->fi() ->beginIF($status != 'all' and $status != 'undone')->andWhere('t2.status')->eq($status)->fi() ->beginIF(!$this->app->user->admin)->andWhere('t2.id')->in($this->app->user->view->sprints)->fi() ->andWhere('t2.deleted')->eq('0') ->andWhere('t2.vision')->eq($this->config->vision) ->orderBy($orderBy) ->page($pager) ->fetchAll('id'); } $hours = $this->computerProgress($executions); $emptyHour = array('totalEstimate' => 0, 'totalConsumed' => 0, 'totalLeft' => 0, 'progress' => 0); /* Get burndown charts datas. */ $burns = $this->dao->select('execution, date AS name, `left` AS value') ->from(TABLE_BURN) ->where('execution')->in(array_keys($executions)) ->andWhere('task')->eq(0) ->orderBy('date desc') ->fetchGroup('execution', 'name'); $this->loadModel('execution'); foreach($burns as $executionID => $executionBurns) { /* If executionBurns > $itemCounts, split it, else call processBurnData() to pad burns. */ $begin = $executions[$executionID]->begin; $end = $executions[$executionID]->end; if(helper::isZeroDate($begin)) $begin = $executions[$executionID]->openedDate; $executionBurns = $this->execution->processBurnData($executionBurns, $itemCounts, $begin, $end); /* Shorter names. */ foreach($executionBurns as $executionBurn) { $executionBurn->name = substr($executionBurn->name, 5); unset($executionBurn->execution); } ksort($executionBurns); $burns[$executionID] = $executionBurns; } /* Process executions. */ $parents = array(); $children = array(); foreach($executions as $key => $execution) { /* Process the end time. */ $execution->end = date(DT_DATE1, strtotime($execution->end)); /* Judge whether the execution is delayed. */ if($execution->status != 'done' and $execution->status != 'closed' and $execution->status != 'suspended') { $delay = helper::diffDate(helper::today(), $execution->end); if($delay > 0) $execution->delay = $delay; } /* Process the burns. */ $execution->burns = array(); $burnData = isset($burns[$execution->id]) ? $burns[$execution->id] : array(); foreach($burnData as $data) $execution->burns[] = $data->value; /* Process the hours. */ $execution->hours = isset($hours[$execution->id]) ? $hours[$execution->id] : (object)$emptyHour; $execution->children = array(); $execution->grade == 1 ? $parents[$execution->id] = $execution : $children[$execution->parent][] = $execution; } /* In the case of the waterfall model, calculate the sub-stage. */ if($projectID == 0) { foreach($executions as $key => $execution) { if($execution->type == 'stage') { $execution->children = isset($children[$execution->id]) ? $children[$execution->id] : array(); unset($children[$execution->id]); } } } $project = $this->getByID($projectID); if($project and $project->model == 'waterfall') { foreach($parents as $id => $execution) { $execution->children = isset($children[$id]) ? $children[$id] : array(); unset($children[$id]); } } $orphan = array(); foreach($children as $child) $orphan = array_merge($child, $orphan); return array_merge($parents, $orphan); } /** * Get stats for project kanban. * * @access public * @return void */ public function getStats4Kanban() { $this->loadModel('program'); $projects = $this->program->getProjectStats(0, 'all', 0, 'order_asc'); $executions = $this->getStats(0, 'doing'); $doingExecutions = array(); $latestExecutions = array(); foreach($executions as $execution) $doingExecutions[$execution->project][$execution->id] = $execution; foreach($doingExecutions as $projectID => $executions) { krsort($doingExecutions[$projectID]); $latestExecutions[$projectID] = current($doingExecutions[$projectID]); } $myProjects = array(); $otherProjects = array(); $closedGroup = array(); foreach($projects as $project) { if(strpos('wait,doing,closed', $project->status) === false) continue; $projectPath = explode(',', trim($project->path, ',')); $topProgram = !empty($project->parent) ? $projectPath[0] : $project->parent; if($project->PM == $this->app->user->account) { if($project->status != 'closed') { $myProjects[$topProgram][$project->status][] = $project; } else { $closedGroup['my'][$topProgram][$project->closedDate] = $project; } } else { if($project->status != 'closed') { $otherProjects[$topProgram][$project->status][] = $project; } else { $closedGroup['other'][$topProgram][$project->closedDate] = $project; } } } /* Only display recent two closed projects. */ foreach($closedGroup as $group => $closedProjects) { foreach($closedProjects as $topProgram => $projects) { krsort($projects); if($group == 'my') { $myProjects[$topProgram]['closed'] = array_slice($projects, 0, 2); } else { $otherProjects[$topProgram]['closed'] = array_slice($projects, 0, 2); } } } return array('kanbanGroup' => array('my' => $myProjects, 'other' => $otherProjects), 'latestExecutions' => $latestExecutions); } /** * 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'); /* 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]) and $executions[$executionID]->type == 'stage' and $executions[$executionID]->grade == 2) { $stageParent = $executions[$executionID]->parent; 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 void */ public function setMenu($objectID) { global $lang; $project = $this->getByID($objectID); $model = 'scrum'; if($project and $project->model == 'waterfall') $model = $project->model; 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; } /* 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') { global $lang; $this->loadModel('execution'); $lang->executionCommon = $lang->project->stage; include $this->app->getModulePath('', 'execution') . 'lang/' . $this->app->getClientLang() . '.php'; } $this->lang->switcherMenu = $this->getSwitcher($objectID, $this->app->rawModule, $this->app->rawMethod); $this->saveState($objectID, $this->getPairsByProgram()); common::setMenuVars('project', $objectID); } /** * Check if the project model can be changed. * * @param int $projectID * @param string $model * @access public * @return bool */ public function checkCanChangeModel($projectID, $model) { $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)); $object = new stdclass(); if($table == TABLE_EXECUTION) { $type = $model == 'scrum' ? 'sprint' : 'stage'; } $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 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", $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 = $this->config->systemMode == 'classic' ? "execution" : "project"; if($project->status == 'wait' || $project->status == 'suspended') { $menu .= $this->buildMenu($moduleName, 'start', $params, $project, 'browse', 'play', '', 'iframe', true); } if($project->status == 'doing') $menu .= $this->buildMenu($moduleName, 'close', $params, $project, 'browse', 'off', '', 'iframe', true); if($project->status == 'closed') $menu .= $this->buildMenu($moduleName, 'activate', $params, $project, 'browse', 'magic', '', 'iframe', true); if(common::hasPriv($moduleName, 'suspend') || (common::hasPriv($moduleName, 'close') && $project->status != 'doing') || (common::hasPriv($moduleName, 'activate') && $project->status != 'closed')) { $menu .= "
    "; $menu .= ""; $menu .= ""; $menu .= "
    "; } $from = $project->from == 'project' ? 'project' : 'pgmproject'; $iframe = ($this->app->tab == 'program' || $project->model == 'kanban') ? 'iframe' : ''; $onlyBody = ($this->app->tab == 'program' || $project->model == 'kanban') ? true : ''; $dataApp = $this->config->systemMode == 'classic' ? "data-app=execution" : "data-app=project"; $attr = $project->model == 'kanban' ? " disabled='disabled'" : ''; $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 . $attr, $this->lang->execution->team); if($this->config->systemMode == 'new') $menu .= $this->buildMenu('project', 'group', "$params&programID={$project->programID}", $project, 'browse', 'lock', '', '', '', $dataApp . $attr); 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); if($this->config->systemMode == 'new') $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; } }