1033 lines
44 KiB
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();
|
|
}
|
|
}
|