znt/module/programplan/tao.php

1033 lines
44 KiB
PHP

<?php
declare(strict_types=1);
/**
* The tao file of programplan module of ZenTaoPMS.
*
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
* @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
* @author lanzongjun <lanzongjun@easycorp.ltd>
* @link https://www.zentao.net
*/
class programplanTao extends programplanModel
{
/**
* 获取阶段信息。
* Get stage list.
*
* @param int $executionID
* @param int $productID
* @param string $browseType
* @param string $orderBy
* @access protected
* @return array
*/
protected function getStageList(int $executionID, int $productID, string $browseType, string $orderBy = 'id_asc'): array
{
if(empty($executionID)) return array();
$projectModel = $this->dao->select('model')->from(TABLE_PROJECT)->where('id')->eq($executionID)->fetch('model');
$stageList = $this->dao->select('t1.*')->from(TABLE_EXECUTION)->alias('t1')
->leftJoin(TABLE_PROJECTPRODUCT)->alias('t2')->on('t1.id = t2.project')
->where('1 = 1')
->beginIF(!in_array($projectModel, array('waterfallplus', 'ipd')))->andWhere('t1.type')->eq('stage')->fi()
->beginIF($productID)->andWhere('t2.product')->eq($productID)->fi()
->beginIF($browseType == 'all' || $browseType == 'leaf')->andWhere('t1.project')->eq($executionID)->fi()
->beginIF($browseType == 'parent')->andWhere('t1.parent')->eq($executionID)->fi()
->beginIF(!$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->sprints)->fi()
->andWhere('((t1.enabled')->eq('on')
->andWhere('t1.deleted')->eq('0')
->markRight(1)
->beginIF($this->app->rawModule == 'programplan' && $this->app->rawMethod == 'create')
->orWhere('(t1.enabled')->eq('off')
->andWhere('t1.deleted')->eq('1')
->markRight(1)
->fi()
->markRight(1)
->orderBy($orderBy)
->fetchAll('id', false);
if($projectModel == 'ipd')
{
$stagePointGroup = $this->dao->select('execution,category,categoryTitle')->from(TABLE_OBJECT)
->where('execution')->in(array_keys($stageList))
->andWhere('type')->eq('decision')
->andWhere('enabled')->eq('1')
->fetchGroup('execution', 'category');
foreach($stageList as $stage)
{
$stage->enabledPoints = array();
if(isset($stagePointGroup[$stage->id]))
{
foreach($stagePointGroup[$stage->id] as $point) $stage->enabledPoints[$point->category] = $point->categoryTitle;
}
}
}
return $stageList;
}
/**
* 更新项目阶段。
* update program plan.
*
* @param object $plan
* @param object $oldPlan
* @access protected
* @return array|false
*/
protected function updateRow(int $planID, int $projectID, object|null $plan): array|false
{
if(empty($plan)) return false;
$oldPlan = $this->fetchByID($planID);
$planChanged = ($oldPlan->name != $plan->name || $oldPlan->milestone != $plan->milestone || $oldPlan->begin != $plan->begin || $oldPlan->end != $plan->end);
if($planChanged) $plan->version = $oldPlan->version + 1;
if(empty($plan->parent)) $plan->parent = $projectID;
$parentStage = empty($projectID) ? null : $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($plan->parent)->andWhere('type')->eq('stage')->fetch();
$relatedIdList = $this->loadModel('execution')->getRelatedExecutions($planID);
$relatedIdList = !empty($relatedIdList) ? implode(',', array_keys($relatedIdList)) : '0';
$setCode = !empty($this->config->setCode);
$getName = ($relatedIdList && $oldPlan->project && $parentStage && $oldPlan->parent) ? true : false;
/* Fix bug #22030. Reset field name for show dao error. */
$this->lang->project->name = $this->lang->programplan->name;
$this->lang->project->code = $this->lang->execution->code;
$this->dao->update(TABLE_PROJECT)->data($plan, 'point,defaultPoint')->autoCheck()
->checkIF(!empty($plan->name) && $getName, 'name', 'unique', "id in ({$relatedIdList}) and type in ('sprint','stage') and `project` = {$oldPlan->project} and `deleted` = '0' and `parent` = {$oldPlan->parent}")
->checkIF(!empty($plan->code) && $setCode, 'code', 'unique', "id != {$planID} and type in ('sprint','stage','kanban') and `project` = {$oldPlan->project} and `deleted` = '0' and `parent` = {$oldPlan->parent}")
->where('id')->eq($planID)
->exec();
/* IPD项目停/启用阶段时,子阶段同样停/启用。*/
if(isset($plan->enabled) && $oldPlan->enabled != $plan->enabled)
{
$this->dao->update(TABLE_PROJECT)
->set('deleted')->eq($plan->deleted)
->set('enabled')->eq($plan->enabled)
->where('project')->eq($projectID)
->andWhere('grade')->gt(1)
->andWhere("FIND_IN_SET($planID, `path`)")
->exec();
}
if($planChanged) $this->insertProjectSpec($planID, $plan);
if(dao::isError()) return false;
return common::createChanges($oldPlan, $plan);
}
/**
* 获取父阶段键值对。
* Get parent stage list.
*
* @param int $executionID
* @param int $planID
* @param int $productID
* @param string $param
* @access protected
* @return array|false
*/
protected function getParentStages(int $executionID, int $planID, int $productID, string $param = ''): array|false
{
dao::$filterTpl = 'skip';
$parentStage = $this->dao->select('t1.id, t1.name')->from(TABLE_PROJECT)->alias('t1')
->beginIF($productID)
->leftJoin(TABLE_PROJECTPRODUCT)->alias('t2')->on('t1.id = t2.project')
->fi()
->where('t1.project')->eq($executionID)
->beginIF($productID)
->andWhere('t2.product')->eq($productID)
->fi()
->andWhere('t1.type')->eq('stage')
->andWhere('t1.deleted')->eq(0)
->andWhere('t1.path')->notlike("%,$planID,%")
->beginIF(!$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->sprints)->fi()
->beginIF(strpos($param, 'noclosed') !== false)->andWhere('status')->ne('closed')->fi()
->orderBy('t1.id desc')
->fetchPairs();
if(dao::isError()) return false;
return $parentStage;
}
/**
* 根据action获取excution。
* Get new parent and action.
*
* @param array $statusCount
* @param object $parent
* @param int $startTasks
* @param string $action
* @param object $project
* @access protected
* @return array
*/
protected function getNewParentAndAction(array $statusCount, object $parent, int $startTasks, string $action, object $project): array
{
$count = count($statusCount);
$newParent = null;
$parentAction = '';
$this->loadModel('execution');
if(isset($statusCount['wait']) && $count == 1 && helper::isZeroDate($parent->realBegan) && $startTasks == 0)
{
if($parent->status == 'wait') return array('newParent' => null, 'parentAction' => '');
$newParent = $this->execution->buildExecutionByStatus('wait');
$parentAction = 'waitbychild';
}
elseif(isset($statusCount['closed']) && $count == 1)
{
if($parent->status == 'closed') return array('newParent' => null, 'parentAction' => '');
$newParent = $this->execution->buildExecutionByStatus('closed');
$parentAction = 'closedbychild';
}
elseif(isset($statusCount['suspended']) && ($count == 1 || (isset($statusCount['closed']) && $count == 2)))
{
if($parent->status == 'suspended') return array('newParent' => null, 'parentAction' => '');
$newParent = $this->execution->buildExecutionByStatus('suspended');
$parentAction = 'suspendedbychild';
}
else
{
if($parent->status == 'doing') return array('newParent' => null, 'parentAction' => '');
$newParent = $this->execution->buildExecutionByStatus('doing');
$parentAction = $parent->status == 'wait' ? 'startbychildstart' : 'startbychild' . $action;
}
return array('newParent' => $newParent, 'parentAction' => $parentAction);
}
/**
* 设置任务基线数据。
* Set task baseline data.
*
* @param array $oldTasks
* @param array $tasks
* @access protected
* @return array
*/
protected function setTaskBaseline(array $oldTasks, array $tasks): array
{
foreach($oldTasks as $id => $oldTask)
{
if(!isset($tasks[$id])) continue;
$tasks[$id]->version = $oldTask->version;
$tasks[$id]->name = $oldTask->name;
$tasks[$id]->estStarted = $oldTask->estStarted;
$tasks[$id]->deadline = $oldTask->deadline;
}
return $tasks;
}
/**
* 设置甘特图阶段数据。
* Set the Gantt chart stage data.
*
* @param array $plans
* @param string $browseType
* @access protected
* @return array
*/
protected function initGanttPlans(array $plans, string $browseType = ''): array
{
$this->app->loadLang('stage');
$datas = $stageIndex = $planIdList = $reviewDeadline = array();
$today = helper::today();
$isMilestone = "<icon class='icon icon-flag icon-sm red'></icon> ";
foreach($plans as $plan)
{
$plan->isParent = false;
if(isset($plans[$plan->parent]))
{
$plans[$plan->parent]->isParent = true;
}
elseif($browseType == 'bysearch')
{
$plans[$plan->id]->parent = 0;
}
}
foreach($plans as $plan)
{
$planIdList[$plan->id] = $plan->id;
$reviewDeadline[$plan->id]['stageEnd'] = $plan->end;
$reviewDeadline[$plan->id]['stageBegin'] = $plan->begin;
$data = $this->buildPlanDataForGantt($plan);
/* Determines if the object is delay. */
$data->delay = $this->lang->programplan->delayList[0];
$data->delayDays = 0;
if($today > $data->endDate and $plan->status != 'closed')
{
$data->delay = $this->lang->programplan->delayList[1];
$data->delayDays = helper::diffDate($today, $data->endDate);
}
$datas['data'][$plan->id] = $data;
$stageIndex[$plan->id] = array('planID' => $plan->id, 'parent' => isset($plans[$plan->parent]) ? $plan->parent : $plan->project, 'totalEstimate' => 0, 'totalConsumed' => 0, 'totalReal' => 0, 'path' => $plan->path);
}
return array('datas' => $datas, 'stageIndex' => $stageIndex, 'planIdList' => $planIdList, 'reviewDeadline' => $reviewDeadline);
}
/**
* 设置甘特图任务数据。
* Set the Gantt chart task data.
*
* @param array $tasks
* @param array $plans
* @param string $selectCustom
* @param array $datas
* @param array $stageIndex
* @access protected
* @return array
*/
protected function setTask(array $tasks, array $plans, string $selectCustom, array $datas, array $stageIndex): array
{
$this->app->loadLang('task');
$this->loadModel('holiday');
$executions = array();
$today = helper::today();
$taskTeams = $this->dao->select('task,account')->from(TABLE_TASKTEAM)->where('task')->in(array_keys($tasks))->fetchGroup('task', 'account');
$users = $this->loadModel('user')->getPairs('noletter');
$firstTask = reset($tasks);
$projectID = $firstTask ? $firstTask->project : 0;
$taskDateLimit = $this->dao->select('taskDateLimit')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetch('taskDateLimit');
foreach($tasks as $task)
{
$plan = zget($plans, $task->execution, null);
$dateLimit = $this->getTaskDateLimit($task, $plan, $taskDateLimit == 'limit' ? zget($tasks, $task->parent, null) : null);
$data = $this->buildTaskDataForGantt($task, $dateLimit);
$data->id = $task->execution . '-' . $task->id;
$data->parent = $task->parent > 0 && isset($tasks[$task->parent]) ? $task->execution . '-' . $task->parent : $task->execution;
$data->allowLinks = $plan->type == 'kanban' ? false : true;
if(!isset($executions[$task->execution])) $executions[$task->execution] = $this->dao->select('status')->from(TABLE_EXECUTION)->where('id')->eq($task->execution)->fetch('status');
/* Determines if the object is delay. */
$data->delay = $this->lang->programplan->delayList[0];
$data->delayDays = 0;
if(isset($task->delay) && $task->delay > 0)
{
$data->delay = $this->lang->programplan->delayList[1];
$data->delayDays = $task->delay;
}
/* If multi task then show the teams. */
if($task->mode == 'multi' && !empty($taskTeams[$task->id])) $data->owner_id = implode(',', array_map(function($assignedTo) use($users){return zget($users, $assignedTo);}, array_keys($taskTeams[$task->id])));
if(strpos($selectCustom, 'task') !== false) $datas['data'][$data->id] = $data;
if($task->isParent) continue;
foreach($stageIndex as $index => $stage)
{
if($stage['planID'] != $task->execution) continue;
if(!isset($stageIndex[$index])) continue;
$stageIndex[$index]['totalEstimate'] += $task->estimate;
$stageIndex[$index]['totalConsumed'] += $task->consumed;
$stageIndex[$index]['totalReal'] += ((($task->status == 'closed' || $task->status == 'cancel') ? 0 : $task->left) + $task->consumed);
foreach(explode(',', $stage['path']) as $planID)
{
if(!isset($stageIndex[$planID]) || $planID == $stage['planID']) continue;
$stageIndex[$planID]['totalEstimate'] += $task->estimate;
$stageIndex[$planID]['totalConsumed'] += $task->consumed;
$stageIndex[$planID]['totalReal'] += ((($task->status == 'closed' || $task->status == 'cancel') ? 0 : $task->left) + $task->consumed);
}
}
}
return array('datas' => $datas, 'stageIndex' => $stageIndex);
}
/**
* 获取子阶段数量。
* Get stage count.
*
* @param int $planID
* @param string $mode
* @access protected
* @return int
*/
protected function getStageCount(int $planID, string $mode = ''): int
{
return $this->dao->select('COUNT(1) AS count')->from(TABLE_PROJECT)
->where('parent')->eq($planID)
->andWhere('type')->eq('stage')
->beginIF($mode == 'milestone')->andWhere('milestone')->eq(1)->fi()
->andWhere('deleted')->eq(0)
->fetch('count');
}
/**
* 计算阶段在数据库中的位置序号。
* Compute the new order for programplan. if orders is not set then begin with the order in zt_project table.
*
* @param array $orders
* @param array $plans
* @access protected
* @return array
*/
protected function computeOrders(array $orders, array $plans): array
{
if(empty($orders)) $orders = array();
asort($orders);
$planCount = count($plans);
if(count($orders) < $planCount)
{
$orderIndex = empty($orders) ? 0 : count($orders);
$lastID = $this->dao->select('id')->from(TABLE_EXECUTION)->orderBy('id_desc')->limit(1)->fetch('id');
for($i = $orderIndex; $i < $planCount; $i ++)
{
$lastID ++;
$orders[$i] = $lastID * 5;
}
}
return $orders;
}
/**
* 插入项目细节。
* Insert project spec.
* @param int $planID
* @param object $plan
* @access protected
* @return bool
*/
protected function insertProjectSpec(int $planID, object $plan): bool
{
if(empty($planID)) return false;
$spec = new stdclass();
$spec->project = $planID;
$spec->version = $plan->version;
$spec->name = $plan->name;
$spec->milestone = $plan->milestone;
$spec->begin = $plan->begin;
$spec->end = $plan->end;
$this->dao->insert(TABLE_PROJECTSPEC)->data($spec)->exec();
return !dao::isError();
}
/**
* 获取阶段百分比。
* Get total percent.
*
* @param object $stage
* @param bool $parent
* @access public
* @return int|float
*/
protected function getTotalPercent(object $stage, bool $parent = false): int|float
{
/* When parent is equal to true, query the total workload of the subphase. */
$executionID = $parent ? $stage->id : $stage->project;
$plans = $this->getStageList((int)$executionID, (int)$stage->product, 'parent');
$totalPercent = 0;
$stageID = $stage->id;
foreach($plans as $id => $stage)
{
if($id == $stageID) continue;
$totalPercent += $stage->percent;
}
return $totalPercent;
}
/**
* Get point end date.
*
* @param int $planID
* @param object $point
* @param array $reviewDeadline
* @access protected
* @return string
*/
protected function getPointEndDate(int $planID, object $point, array $reviewDeadline): string
{
if($point->end and !helper::isZeroDate($point->end)) return $point->end;
$end = $reviewDeadline[$planID]['stageEnd'];
$begin = $reviewDeadline[$planID]['stageBegin'];
$pointType = $this->dao->select('type')->from(TABLE_DECISION)->where('id')->eq($point->id)->fetch('type');
if($pointType == 'DCP') $end = $this->getReviewDeadline($end, 2);
if($pointType == 'TR')
{
if(isset($reviewDeadline[$planID]['taskEnd']) and !helper::isZeroDate($reviewDeadline[$planID]['taskEnd'])) return $reviewDeadline[$planID]['taskEnd'];
$end = $this->getReviewDeadline($end);
}
return $end <= $begin ? $begin : $end;
}
/**
* 构建IPD版本的甘特图数据。
* Build gantt's data for ipd edition.
*
* @param array $datas
* @param int $projectID
* @param int $productID
* @param string $selectCustom
* @param array $reviewDeadline
* @access protected
* @return array
*/
protected function buildGanttData4IPD(array $datas, int $projectID, int $productID, string $selectCustom, array $reviewDeadline): array
{
if($this->config->edition != 'ipd') return $datas;
$this->loadModel('review');
$reviewPoints = $this->dao->select('t1.*, t2.status, t2.lastReviewedDate,t2.id as reviewID')->from(TABLE_OBJECT)->alias('t1')
->leftJoin(TABLE_REVIEW)->alias('t2')->on('t1.id = t2.object')
->where('t1.deleted')->eq('0')
->andWhere('t1.project')->eq($projectID)
->andWhere('t1.enabled')->eq(1)
->fetchAll('id', false);
foreach($datas['data'] as $plan)
{
if($plan->type != 'plan') continue;
foreach($reviewPoints as $point)
{
if($point->execution != $plan->id) continue;
if(!isset($point->status)) $point->status = '';
$dataID = "{$plan->id}-point{$point->category}-{$point->id}";
if($selectCustom && strpos($selectCustom, "point") !== false && !$plan->parent) $datas['data'][$dataID] = $this->buildPointDataForGantt($plan->id, $point, $reviewDeadline);
}
}
return $datas;
}
/**
* 获取创建时的关联产品。
* Get linkProducts for create.
*
* @param int $projectID
* @param int $productID
* @access protected
* @return array
*/
protected function getLinkProductsForCreate(int $projectID, int $productID): array
{
$project = $this->fetchByID($projectID, 'project');
$linkProducts = array();
$linkBranches = array();
$productList = $this->loadModel('product')->getProducts($projectID);
if($project && $project->stageBy == 'product')
{
$linkProducts = array(0 => $productID);
if(!empty($productList[$productID])) $linkBranches = array(0 => zget($productList[$productID], 'branches', array()));
}
else
{
$linkProducts = array_keys($productList);
foreach($linkProducts as $index => $productID)
{
if(!empty($productList)) $linkBranches[$index] = $productList[$productID]->branches;
}
}
return array('products' => $linkProducts, 'branch' => $linkBranches);
}
/**
* 插入阶段数据。
* Insert stage.
* @param object $plan
* @param int $projectID
* @param int $productID
* @param int $parentID
* @access protected
* @return int|false
*/
protected function insertStage(object $plan, int $projectID, int $productID, int $parentID): int|false
{
$this->loadModel('execution');
$project = $this->fetchById($projectID, 'project');
$account = $this->app->user->account;
unset($plan->id);
$plan->status = 'wait';
$plan->stageBy = empty($project) ? 'product' : $project->stageBy;
$plan->version = 1;
$plan->parentVersion = $plan->parent == 0 ? 0 : $this->dao->findByID($plan->parent)->from(TABLE_PROJECT)->fetch('version');
$plan->team = mb_substr($plan->name, 0, 30);
$plan->openedBy = $account;
$plan->openedDate = helper::now();
$plan->openedVersion = $this->config->version;
if(!isset($plan->acl)) $plan->acl = $this->dao->findByID($plan->parent)->from(TABLE_PROJECT)->fetch('acl');
$this->dao->insert(TABLE_PROJECT)->data($plan, 'point,defaultPoint')->autoCheck()->exec();
if(dao::isError()) return false;
$stageID = (int)$this->dao->lastInsertID();
$this->insertProjectSpec($stageID, $plan);
/* Ipd project create default review points. */
if($project && $project->model == 'ipd' && $this->config->edition == 'ipd' && !$parentID) $this->loadModel('review')->createDefaultPoint($project->id, $stageID, $plan->defaultPoint);
if($plan->type == 'kanban')
{
$execution = $this->execution->getByID($stageID);
$this->loadModel('kanban')->createRDKanban($execution);
}
$this->loadModel('execution');
if($projectID) $this->execution->createMainLib($projectID, $stageID);
$this->execution->addExecutionMembers($stageID, array($account, $plan->PM));
$this->setTreePath($stageID);
$this->computeProgress($stageID, 'create');
/* Add the execution team members to parent executions. */
$stagePath = $this->dao->findByID($stageID)->from(TABLE_PROJECT)->fetch('path');
foreach(explode(',', $stagePath) as $stageParentID)
{
if(empty($stageParentID) || $stageParentID == $projectID) continue;
$this->execution->addExecutionMembers((int)$stageParentID, array($plan->PM));
}
return $stageID;
}
/**
* 根据任务列表,构建任务分组。
* Build task group by assignedTo.
*
* @param array $task
* @access protected
* @return array
*/
protected function buildTaskGroup(array $tasks): array
{
$taskGroup = array();
$multiTasks = array();
foreach($tasks as $taskID => $task)
{
$taskGroup[$task->assignedTo][$taskID] = $task;
if($task->mode == 'multi') $multiTasks[$taskID] = $task->assignedTo;
}
if(empty($multiTasks)) return $taskGroup;
$taskTeams = $this->dao->select('t1.*,t2.realname')->from(TABLE_TASKTEAM)->alias('t1')
->leftJoin(TABLE_USER)->alias('t2')->on('t1.account = t2.account')
->where('t1.task')->in(array_keys($multiTasks))
->orderBy('t1.order')
->fetchGroup('task', 'id');
foreach($taskTeams as $taskID => $team)
{
$assignedTo = $multiTasks[$taskID];
foreach($team as $member)
{
$account = $member->account;
if($account == $assignedTo) continue;
if(!isset($taskGroup[$account])) $taskGroup[$account] = array();
$taskGroup[$account][$taskID] = clone $tasks[$taskID];
$taskGroup[$account][$taskID]->id = $taskID . '_' . $account;
$taskGroup[$account][$taskID]->realID = $taskID;
$taskGroup[$account][$taskID]->assignedTo = $account;
$taskGroup[$account][$taskID]->realname = $member->realname;
}
}
return $taskGroup;
}
/**
* Build plan data for gantt.
*
* @param object $plan
* @access protected
* @return object
*/
protected function buildPlanDataForGantt(object $plan): object
{
$start = helper::isZeroDate($plan->begin) ? '' : $plan->begin;
$end = helper::isZeroDate($plan->end) ? '' : $plan->end;
$realBegan = helper::isZeroDate($plan->realBegan) ? '' : $plan->realBegan;
$realEnd = helper::isZeroDate($plan->realEnd) ? '' : $plan->realEnd;
$isMilestone = "<icon class='icon icon-flag icon-sm red'></icon> ";
$data = new stdclass();
$data->id = $plan->id;
$data->type = 'plan';
$data->executionType = $plan->type;
$data->text = empty($plan->milestone) ? $plan->name : $plan->name . $isMilestone;
$data->name = $plan->name;
$data->attribute = zget($this->lang->stage->typeList, $plan->attribute);
$data->milestone = zget($this->lang->programplan->milestoneList, $plan->milestone);
$data->milestonecode = $plan->milestone;
$data->owner_id = $plan->PM;
$data->status = $this->processStatus('execution', $plan);
$data->begin = $start;
$data->deadline = $end;
$data->realBegan = $realBegan ? substr($realBegan, 0, 10) : '';
$data->realEnd = $realEnd ? substr($realEnd, 0, 10) : '';
$data->parent = $plan->grade == 1 ? 0 :$plan->parent;
$data->isParent = $plan->isParent;
$data->isTpl = $plan->isTpl;
$data->open = true;
$data->start_date = $start;
$data->endDate = $end;
$data->duration = 1;
$data->color = $this->lang->execution->gantt->stage->color;
$data->progressColor = $this->lang->execution->gantt->stage->progressColor;
$data->textColor = $this->lang->execution->gantt->stage->textColor;
$data->bar_height = $this->lang->execution->gantt->bar_height;
/* Set default progress from database. */
$data->progress = $plan->progress / 100;
$data->taskProgress = $plan->progress . '%';
$data->frozen = $plan->frozen;
if($data->endDate > $data->start_date) $data->duration = helper::diffDate($data->endDate, $data->start_date) + 1;
if(empty($data->start_date) || empty($data->endDate)) $data->duration = 1;
if(!empty($this->config->setPercent)) $data->percent = $plan->percent;
if($data->start_date) $data->start_date = date('d-m-Y', strtotime($data->start_date));
return $data;
}
/**
* Guild point data for gantt.
*
* @param int $planID
* @param object $point
* @param array $reviewDeadline
* @access protected
* @return object
*/
protected function buildPointDataForGantt(int $planID, object $point, array $reviewDeadline): object
{
$statusList = array();
if(isset($this->lang->review->statusList)) $statusList = $this->lang->review->statusList;
$end = $this->getPointEndDate($planID, $point, $reviewDeadline);
$data = new stdclass();
$data->id = $planID . '-point' . $point->category . '-' . $point->id;
$data->reviewID = $point->reviewID;
$data->type = 'point';
$data->text = "<i class='icon-seal'></i> " . $point->title;
$data->name = $point->title;
$data->attribute = '';
$data->milestone = '';
$data->owner_id = '';
$data->rawStatus = $point->status;
$data->status = $point->status ? zget($statusList, $point->status) : $this->lang->programplan->wait;
$data->status = "<span class='status-{$point->status}'>" . $data->status . '</span>';
$data->begin = $end;
$data->deadline = $end;
$data->realBegan = $point->createdDate;
$data->realEnd = $point->lastReviewedDate;;
$data->parent = $planID;
$data->open = true;
$data->start_date = $end;
$data->endDate = $end;
$data->duration = 1;
$data->color = isset($this->lang->programplan->reviewColorList[$point->status]) ? $this->lang->programplan->reviewColorList[$point->status] : '#FC913F';
$data->progressColor = $this->lang->execution->gantt->stage->progressColor;
$data->textColor = $this->lang->execution->gantt->stage->textColor;
$data->bar_height = $this->lang->execution->gantt->bar_height;
if($data->start_date) $data->start_date = date('d-m-Y', strtotime($data->start_date));
return $data;
}
/**
* 构建甘特图的分组数据。
* Build group data for gantt.
*
* @param int $groupID
* @param string $group
* @param array $users
* @access protected
* @return object
*/
protected function buildGroupDataForGantt(int $groupID, string $group, array $users): object
{
$this->app->loadLang('task');
$groupName = $group;
$groupName = $group != '/' ? zget($users, $group) : $this->lang->task->noAssigned;
$dataGroup = new stdclass();
$dataGroup->id = $groupID;
$dataGroup->type = 'group';
$dataGroup->text = $groupName;
$dataGroup->percent = '';
$dataGroup->attribute = '';
$dataGroup->milestone = '';
$dataGroup->owner_id = $group;
$dataGroup->status = '';
$dataGroup->begin = '';
$dataGroup->deadline = '';
$dataGroup->realBegan = '';
$dataGroup->realEnd = '';
$dataGroup->parent = 0;
$dataGroup->open = true;
$dataGroup->progress = '';
$dataGroup->taskProgress = '';
$dataGroup->color = $this->lang->execution->gantt->stage->color;
$dataGroup->progressColor = $this->lang->execution->gantt->stage->progressColor;
$dataGroup->textColor = $this->lang->execution->gantt->stage->textColor;
$dataGroup->bar_height = $this->lang->execution->gantt->bar_height;
return $dataGroup;
}
/**
* 构建甘特图的任务数据。
* Build task data for gantt.
*
* @param int $groupID
* @param object $task
* @param array $dateLimit array('start' => $start, 'end' => $end, 'realBegan' => $realBegan, 'realEnd' => $realEnd);
* @param array $tasksMap
* @access protected
* @return object
*/
protected function buildTaskDataForGantt(object $task, array $dateLimit, int $groupID = 0, array $tasksMap = array()): object
{
$taskPri = "<span class='pri-%s align-middle' title='%s'>%s</span> ";
$pri = zget($this->lang->task->priList, $task->pri);
$priIcon = sprintf($taskPri, $task->pri, $pri, $pri);
$total = $task->left + $task->consumed;
$progress = $total > 0 ? round($task->consumed / $total, 3) : 0;
$data = new stdclass();
$data->id = $task->id;
$data->type = 'task';
$data->text = $priIcon . "<span class='gantt_title'>{$task->name}</span>";
$data->percent = '';
$data->status = $task->status == 'changed' ? $this->lang->task->storyChange : $this->processStatus('task', $task);
$data->owner_id = $task->assignedTo;
$data->attribute = '';
$data->milestone = '';
$data->begin = substr($dateLimit['start'], 0, 10);
$data->deadline = substr($dateLimit['end'], 0, 10);
$data->realBegan = $dateLimit['realBegan'] ? substr($dateLimit['realBegan'], 0, 10) : '';
$data->realEnd = $dateLimit['realEnd'] ? substr($dateLimit['realEnd'], 0, 10) : '';
$data->pri = $task->pri;
$data->parent = ($task->parent > 0 and $task->assignedTo != '' and !empty($tasksMap[$task->parent]->assignedTo)) ? $task->parent : $groupID;
$data->open = true;
$data->progress = $progress;
$data->taskProgress = ($progress * 100) . '%';
$data->start_date = $dateLimit['start'];
$data->endDate = $dateLimit['end'];
$data->duration = 1;
$data->estimate = $task->estimate;
$data->consumed = $task->consumed;
$data->color = zget($this->lang->execution->gantt->color, $task->pri, $this->lang->execution->gantt->defaultColor);
$data->progressColor = zget($this->lang->execution->gantt->progressColor, $task->pri, $this->lang->execution->gantt->defaultProgressColor);
$data->textColor = zget($this->lang->execution->gantt->textColor, $task->pri, $this->lang->execution->gantt->defaultTextColor);
$data->bar_height = $this->lang->execution->gantt->bar_height;
if($data->endDate > $data->start_date) $data->duration = helper::diffDate($data->endDate, $data->start_date) + 1;
if(empty($data->start_date) or empty($data->endDate)) $data->duration = 1;
if($data->start_date) $data->start_date = date('d-m-Y', strtotime($data->start_date));
return $data;
}
/**
* 构建甘特图的关系链接
* Build gantt links.
*
* @param int $projectID
* @param array $tasks
* @access protected
* @return array
*/
protected function buildGanttLinks(int $projectID, array $tasks = array()): array
{
$this->app->loadConfig('execution');
if($this->config->edition == 'open') return array();
$links = array();
$relations = $this->dao->select('*')->from(TABLE_RELATIONOFTASKS)->where('project')->eq($projectID)->orderBy('task,pretask')->fetchAll();
foreach($relations as $relation)
{
$link = array();
$link['id'] = $relation->id;
$link['source'] = !empty($tasks[$relation->pretask]->id) ? $tasks[$relation->pretask]->id : $relation->pretask;
$link['target'] = !empty($tasks[$relation->task]->id) ? $tasks[$relation->task]->id : $relation->task;
$link['type'] = $this->config->execution->gantt->linkType[$relation->condition][$relation->action];
$links[] = $link;
}
return $links;
}
/**
* 设置阶段统计数据。
* Set stage summary.
*
* @param array $ganttData
* @param array $stages
* @access protected
* @return array
*/
protected function setStageSummary(array $ganttData, array $stages): array
{
foreach($stages as $index => $stage)
{
if(!isset($ganttData['data'][$index])) continue;
$progress = empty($stage['totalReal']) ? 0 : round($stage['totalConsumed'] / $stage['totalReal'], 3);
$ganttData['data'][$index]->progress = $progress;
$ganttData['data'][$index]->taskProgress = ($progress * 100) . '%';
$ganttData['data'][$index]->estimate = $stage['totalEstimate'];
$ganttData['data'][$index]->consumed = $stage['totalConsumed'];
}
return $ganttData;
}
/**
* 获取任务任务限制。获取该任务在甘特图中的开始,结束时间。
* Get task date limit.
*
* @param object $task
* @param object|null $execution
* @param object|null $parent
* @access protected
* @return array
*/
protected function getTaskDateLimit(object $task, ?object $execution = null, ?object $parent = null): array
{
$estStart = helper::isZeroDate($task->estStarted) ? '' : $task->estStarted;
$estEnd = helper::isZeroDate($task->deadline) ? '' : $task->deadline;
$realBegan = helper::isZeroDate($task->realStarted) ? '' : $task->realStarted;
$realEnd = (in_array($task->status, array('done', 'closed')) and !helper::isZeroDate($task->finishedDate)) ? $task->finishedDate : '';
if($parent)
{
if(empty($estStart) && !helper::isZeroDate($parent->estStarted)) $estStart = $parent->estStarted;
if(empty($estEnd) && !helper::isZeroDate($parent->deadline)) $estEnd = $parent->deadline;
}
$start = $estStart;
$end = $estEnd;
if(empty($start) and $execution) $start = $execution->begin;
if(empty($end) and $execution) $end = $execution->end;
if($start > $end) $end = $start;
return array('start' => $start, 'end' => $end, 'realBegan' => $realBegan, 'realEnd' => $realEnd);
}
/**
* Get five days ago.
*
* @param string $date
* @param int $date
* @access public
* @return string
*/
public function getReviewDeadline(string $date, int $counter = 5): string
{
if(helper::isZeroDate($date)) return '';
$this->loadModel('holiday');
$weekendDays = array(6, 7);
$timestamp = strtotime($date);
$index = 0;
while($index < $counter)
{
$timestamp -= 24 * 3600;
$weekday = date('N', $timestamp);
$currentDate = date('Y-m-d', $timestamp);
if(!in_array($weekday, $weekendDays) and !$this->holiday->isHoliday($currentDate)) $index ++;
}
return date('Y-m-d', $timestamp);
}
/**
* 获取基线数据。
* Get baseline data.
*
* @param array $oldPlans
* @param array $plans
* @access protected
* @return array
*/
protected function setPlanBaseline(array $oldPlans, array $plans): array
{
foreach($oldPlans as $id => $oldPlan)
{
if(!isset($plans[$id])) continue;
$plans[$id]->version = $oldPlan->version;
$plans[$id]->name = $oldPlan->name;
$plans[$id]->milestone = $oldPlan->milestone;
$plans[$id]->begin = $oldPlan->begin;
$plans[$id]->end = $oldPlan->end;
}
return $plans;
}
/**
* 将父阶段数据转移到第一个子阶段中。
* Sync parent data to first stage.
*
* @param int $executionID
* @param int $parentID
* @access protected
* @return bool
*/
protected function syncParentData(int $executionID, int $parentID): bool
{
if(empty($executionID) || empty($parentID)) return false;
$this->dao->update(TABLE_TASK)->set('execution')->eq($executionID)->where('execution')->eq($parentID)->andWhere('deleted')->eq('0')->exec();
$this->dao->update(TABLE_EFFORT)->set('execution')->eq($executionID)->where('execution')->eq($parentID)->andWhere('deleted')->eq('0')->exec();
$this->dao->update(TABLE_BUG)->set('execution')->eq($executionID)->where('execution')->eq($parentID)->andWhere('deleted')->eq('0')->exec();
$this->dao->update(TABLE_CASE)->set('execution')->eq($executionID)->where('execution')->eq($parentID)->andWhere('deleted')->eq('0')->exec();
$this->dao->update(TABLE_BUILD)->set('execution')->eq($executionID)->where('execution')->eq($parentID)->andWhere('deleted')->eq('0')->exec();
$this->dao->update(TABLE_TESTTASK)->set('execution')->eq($executionID)->where('execution')->eq($parentID)->andWhere('deleted')->eq('0')->exec();
$this->dao->update(TABLE_TESTREPORT)->set('execution')->eq($executionID)->where('execution')->eq($parentID)->andWhere('deleted')->eq('0')->exec();
$this->dao->update(TABLE_ACTION)->set('execution')->eq($executionID)->where('execution')->eq($parentID)->andWhere('objectType')->ne('execution')->exec();
$this->dao->update(TABLE_ACTIONRECENT)->set('execution')->eq($executionID)->where('execution')->eq($parentID)->andWhere('objectType')->ne('execution')->exec();
$this->dao->update(TABLE_DOCLIB)->set('execution')->eq($executionID)->where('type')->eq('execution')->andWhere('execution')->eq($parentID)->andWhere('main')->eq('0')->andWhere('deleted')->eq('0')->exec();
$storyIdSQL = $this->dao->select('id')->from(TABLE_STORY)->where('deleted')->eq('0')->get();
$this->dao->update(TABLE_PROJECTSTORY)->set('project')->eq($executionID)->where('project')->eq($parentID)->andWhere('story')->subIn($storyIdSQL)->exec();
$caseIdSQL = $this->dao->select('id')->from(TABLE_CASE)->where('deleted')->eq('0')->get();
$this->dao->update(TABLE_PROJECTCASE)->set('project')->eq($executionID)->where('project')->eq($parentID)->andWhere('case')->subIn($caseIdSQL)->exec();
/* Update doc in the main doc lib. */
$parentLibID = $this->dao->select('id')->from(TABLE_DOCLIB)->where('type')->eq('execution')->andWhere('execution')->eq($parentID)->andWhere('main')->eq('1')->limit(1)->fetch('id');
$libID = $this->dao->select('id')->from(TABLE_DOCLIB)->where('type')->eq('execution')->andWhere('execution')->eq($executionID)->andWhere('main')->eq('1')->limit(1)->fetch('id');
if($parentLibID && $libID) $this->dao->update(TABLE_DOC)->set('execution')->eq($executionID)->set('lib')->eq($libID)->where('lib')->eq($parentLibID)->andWhere('deleted')->eq('0')->exec();
/* Update task and doc module. */
$this->dao->update(TABLE_MODULE)->set('root')->eq($executionID)->where('root')->eq($parentID)->andWhere('type')->eq('task')->andWhere('deleted')->eq('0')->exec();
$this->dao->update(TABLE_MODULE)->set('root')->eq($libID)->where('root')->eq($parentLibID)->andWhere('type')->eq('doc')->andWhere('deleted')->eq('0')->exec();
/* Update stage info. */
$parent = $this->fetchByID($parentID);
$data = new stdclass();
$data->status = $parent->status;
$data->progress = $parent->progress;
$data->estimate = $parent->estimate;
$data->left = $parent->left;
$data->consumed = $parent->consumed;
$this->dao->update(TABLE_EXECUTION)->data($data)->where('id')->eq($executionID)->exec();
return !dao::isError();
}
}