Files
EasySoft-ZenTaoPMS/module/productplan/zen.php
T

457 lines
22 KiB
PHP

<?php
declare(strict_types=1);
/**
* The zen file of producrplan 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 Wang Yidong <yidong@easycorp.ltd>
* @package producrplan
* @link https://www.zentao.net
*/
class productplanZen extends productplan
{
/**
* 构建批量编辑计划数据。
* Build plans for batch edit.
*
* @param int $productID
* @access protected
* @return array|string
*/
protected function buildPlansForBatchEdit(): array|string
{
$plans = form::batchData($this->config->productplan->form->batchEdit)->get();
$oldPlans = $this->productplan->getByIDList(array_keys($plans));
$futureConfig = $this->config->productplan->future;
foreach($plans as $planID => $plan)
{
$oldPlan = $oldPlans[$planID];
$parentID = $oldPlan->parent;
if(empty($plan->begin)) $plan->begin = $oldPlan->begin;
if(empty($plan->end)) $plan->end = $oldPlan->end;
if(empty($plan->status)) $plan->status = $oldPlan->status;
if($plan->begin > $plan->end && !empty($plan->end)) return dao::$errors["begin[{$planID}]"] = sprintf($this->lang->productplan->beginGeEnd, $planID);;
if($plan->begin == '') $plan->begin = $this->config->productplan->future;
if($plan->end == '') $plan->end = $this->config->productplan->future;
if(!empty($_POST['future'][$planID]))
{
$plan->begin = $this->config->productplan->future;
$plan->end = $this->config->productplan->future;
}
/* Determine whether the begin and end dates of the parent plan and the child plan are correct. */
if($parentID > 0)
{
$parent = zget($plans, $parentID, $this->productplan->getByID($parentID));
if($parent->begin != $futureConfig && $plan->begin != $futureConfig && $plan->begin < $parent->begin) return dao::$errors["begin[{$planID}]"] = sprintf($this->lang->productplan->beginLessThanParentTip, $planID, $plan->begin, $parent->begin);
if($parent->end != $futureConfig && $plan->end != $futureConfig && $plan->end > $parent->end) return dao::$errors["end[{$planID}]"] = sprintf($this->lang->productplan->endGreatThanParentTip, $planID, $plan->end, $parent->end);
}
elseif($parentID == -1 && $plan->begin != $futureConfig)
{
$childPlans = $this->productplan->getChildren($plan->id);
$minBegin = $plan->begin;
$maxEnd = $plan->end;
foreach($childPlans as $childID => $childPlan)
{
$childPlan = isset($plans[$childID]) ? $plans[$childID] : $childPlan;
if($childPlan->begin < $minBegin && $minBegin != $this->config->productplan->future) $minBegin = $childPlan->begin;
if($childPlan->end > $maxEnd && $maxEnd != $this->config->productplan->future) $maxEnd = $childPlan->end;
}
if($minBegin < $plan->begin && $minBegin != $futureConfig) return dao::$errors["begin[{$planID}]"] = sprintf($this->lang->productplan->beginGreaterChildTip, $planID, $plan->begin, $minBegin);
if($maxEnd > $plan->end && $maxEnd != $futureConfig) return dao::$errors["end[{$planID}]"] = sprintf($this->lang->productplan->endLessThanChildTip, $planID, $plan->end, $maxEnd);
}
if($plan->branch == '') $plan->branch = 0;
}
return $plans;
}
/**
* 设置计划看板页面数据。
* Set kanban page data.
*
* @param object $product
* @param string $branchID
* @param string $orderBy
* @access protected
* @return void
*/
protected function assignKanbanData(object $product, string $branchID, string $orderBy)
{
$branches = array();
$branchPairs = array();
$planCount = 0;
if(!in_array($orderBy, array_keys($this->lang->productplan->orderList))) $orderBy = key($this->lang->productplan->orderList);
if($product->type == 'normal')
{
$planGroup = $this->productplan->getList($product->id, '0', 'all', null, $orderBy, 'skipparent');
$this->view->planCount = count(array_filter($planGroup));
}
else
{
$planGroup = $this->productplan->getGroupByProduct(array($product->id), 'skipparent', $orderBy);
$branches = $this->loadModel('branch')->getPairs($product->id, 'active');
foreach($branches as $id => $name)
{
$plans = isset($planGroup[$product->id][$id]) ? array_filter($planGroup[$product->id][$id]) : array();
$branchPairs[$id] = $name . ' ' . count($plans);
$planCount += count($plans);
}
$this->view->branches = array('all' => $this->lang->productplan->allAB . ' ' . $planCount) + $branchPairs;
}
$this->view->kanbanList = $this->loadModel('kanban')->getPlanKanban($product, $branchID, $planGroup);
}
/**
* 构造计划列表页面数据。
* Build data for browse page.
*
* @param array $plans
* @param array $branchOption
* @access protected
* @return array
*/
protected function buildDataForBrowse(array $plans, array $branchOption): array
{
if(empty($plans)) return $plans;
foreach($plans as $plan)
{
$plan->branchName = '';
if($this->session->currentProductType != 'normal')
{
foreach(explode(',', $plan->branch) as $branchID) $plan->branchName .= $branchOption[$branchID] . ',';
$plan->branchName = trim($plan->branchName, ',');
}
if($plan->begin == $this->config->productplan->future) $plan->begin = $this->lang->productplan->future;
if($plan->end == $this->config->productplan->future) $plan->end = $this->lang->productplan->future;
$plan->actions = $this->buildActionsList($plan);
$plan->projects = array_values($plan->projects);
$plan->desc = strip_tags($plan->desc);
}
return $plans;
}
/**
* 构造计划列表页面操作。
* Build actions for browse page.
*
* @param object $plan
* @access protected
* @return array
*/
protected function buildActionsList(object $plan): array
{
$actions = array();
if(common::hasPriv('productplan', 'start')) $actions[] = 'start';
if(common::hasPriv('productplan', 'finish')) $actions[] = 'finish';
if(common::hasPriv('productplan', 'close')) $actions[] = 'close';
if(common::hasPriv('productplan', 'activate')) $actions[] = 'activate';
if(common::hasPriv('execution', 'create')) $actions[] = 'createExecution';
if(count($actions) > 0) $actions[] = 'divider';
if(common::hasPriv('productplan', 'linkStory')) $actions[] = 'linkStory';
if(common::hasPriv('productplan', 'linkBug')) $actions[] = 'linkBug';
if(common::hasPriv('productplan', 'edit')) $actions[] = 'edit';
if(common::hasPriv('productplan', 'create')) $actions[] = 'create';
if(common::hasPriv('productplan', 'delete')) $actions[] = 'delete';
return $actions;
}
/**
* 统计父计划、子计划和独立计划的总数。
* Get the total count of parent plan, child plan and indepentdent plan.
*
* @param array $planList
* @access protected
* @return string
*/
protected function getSummary(array $planList): string
{
$totalParent = $totalChild = $totalIndependent = 0;
foreach($planList as $plan)
{
if($plan->parent == -1) $totalParent ++;
if($plan->parent > 0) $totalChild ++;
if($plan->parent == 0) $totalIndependent ++;
}
return sprintf($this->lang->productplan->summary, count($planList), $totalParent, $totalChild, $totalIndependent);
}
/**
* 设置计划详情页面session信息。
* Set session for view page.
*
* @param int $planID
* @param string $type
* @param string $orderBy
* @param int $pageID
* @param int $recPerPage
* @access protected
* @return void
*/
protected function setSessionForViewPage(int $planID, string $type, string $orderBy, int $pageID, int $recPerPage)
{
if(in_array($type, array('story', 'bug')) && ($orderBy != 'order_desc' || $pageID != 1 || $recPerPage != 100))
{
if($type == 'story')
{
$this->session->set('storyList', $this->app->getURI(true), 'product');
}
elseif($type == 'bug')
{
$this->session->set('bugList', $this->app->getURI(true), 'qa');
}
else
{
$this->session->set('storyList', $this->createLink('productplan', 'view', "planID={$planID}&type={$type}"), $type == 'story' ? 'product' : 'qa');
}
}
}
/**
* 设置详情页面的属性。
* Set attributes for view page.
*
* @param object $plan
* @access protected
* @return void
*/
protected function assignViewData(object $plan): void
{
if($plan->parent > 0) $this->view->parentPlan = $this->productplan->getById($plan->parent);
if($plan->parent == '-1') $this->view->childrenPlans = $this->productplan->getChildren($plan->id);
$gradeList = $this->loadModel('story')->getGradeList('');
$gradeGroup = array();
foreach($gradeList as $grade) $gradeGroup[$grade->type][$grade->grade] = $grade->name;
$this->view->plan = $plan;
$this->view->gradeGroup = $gradeGroup;
$this->view->actions = $this->loadModel('action')->getList('productplan', $plan->id);
$this->view->users = $this->loadModel('user')->getPairs('noletter');
$this->view->plans = $this->productplan->getPairs($plan->product, $plan->branch, '', true);
$this->view->modules = $this->loadModel('tree')->getOptionMenu($plan->product);
if($this->app->getViewType() == 'json')
{
unset($this->view->storyPager);
unset($this->view->bugPager);
}
}
/**
* 构建关联需求页面的搜索表单。
* Build search form for link story page.
*
* @param object $plan
* @param int $queryID
* @param string $orderBy
* @access protected
* @return void
*/
protected function buildLinkStorySearchForm(object $plan, int $queryID, string $orderBy): void
{
$this->app->loadLang('story');
$products = $this->loadModel('product')->getProductPairsByProject((int)$this->session->project);
/* Build search form. */
$this->config->product->search['actionURL'] = $this->createLink('productplan', 'view', "planID=$plan->id&type=story&orderBy=$orderBy&link=true&param=" . helper::safe64Encode('&browseType=bySearch&queryID=myQueryID'));
$this->config->product->search['queryID'] = $queryID;
$this->config->product->search['style'] = 'simple';
$this->config->product->search['fields']['title'] = $this->lang->productplan->storyTitle;
$this->config->product->search['params']['product']['values'] = $products + array('all' => $this->lang->product->allProductsOfProject);
$this->config->product->search['params']['plan']['values'] = $this->productplan->getPairs($plan->product, $plan->branch, 'withMainPlan', true);
$this->config->product->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($plan->product, 'story', 0, 'all');
$storyStatusList = $this->lang->story->statusList;
unset($storyStatusList['closed']);
$this->config->product->search['params']['status'] = array('operator' => '=', 'control' => 'select', 'values' => $storyStatusList);
$product = $this->loadModel('product')->getByID($plan->product);
if($product->type == 'normal')
{
unset($this->config->product->search['fields']['branch']);
unset($this->config->product->search['params']['branch']);
}
else
{
$branches = $this->loadModel('branch')->getPairsByIdList(explode(',', trim($plan->branch, ',')));
$this->config->product->search['fields']['branch'] = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]);
$this->config->product->search['params']['branch']['values'] = array('' => '', BRANCH_MAIN => $this->lang->branch->main) + $branches;
}
$gradeList = $this->loadModel('story')->getGradeList('');
foreach($gradeList as $grade)
{
if(!$this->config->URAndSR && $grade->type == 'requirement') continue;
if(!$this->config->enableER && $grade->type == 'epic') continue;
$key = (string)$grade->type . (string)$grade->grade;
$gradePairs[$key] = $grade->name;
}
asort($gradePairs);
$this->config->product->search['params']['grade']['values'] = $gradePairs;
unset($this->config->product->search['fields']['product']);
$this->loadModel('search')->setSearchParams($this->config->product->search);
}
/**
* 构造关联bug页面的搜索表单。
* Build search form for link bug page.
*
* @param object $plan
* @param int $queryID
* @param string $orderBy
* @access protected
* @return void
*/
protected function buildBugSearchForm(object $plan, int $queryID, string $orderBy): void
{
$this->config->bug->search['actionURL'] = $this->createLink('productplan', 'view', "planID={$plan->id}&type=bug&orderBy={$orderBy}&link=true&param=" . helper::safe64Encode('&browseType=bySearch&queryID=myQueryID'));
$this->config->bug->search['queryID'] = $queryID;
$this->config->bug->search['style'] = 'simple';
$modulePairs = $this->loadModel('tree')->getOptionMenu($plan->product, 'bug', 0, 'all');
$this->config->bug->search['params']['plan']['values'] = $this->productplan->getPairs($plan->product, $plan->branch, 'withMainPlan', true);
$this->config->bug->search['params']['execution']['values'] = $this->loadModel('product')->getExecutionPairsByProduct($plan->product, $plan->branch);
$this->config->bug->search['params']['module']['values'] = $modulePairs;
$this->config->bug->search['params']['openedBuild']['values'] = $this->loadModel('build')->getBuildPairs(array($plan->product), 'all', 'releasetag');
$this->config->bug->search['params']['resolvedBuild']['values'] = $this->config->bug->search['params']['openedBuild']['values'];
$this->config->bug->search['params']['module']['values'] = $modulePairs;
$this->config->bug->search['params']['project']['values'] = $this->product->getProjectPairsByProduct($plan->product, $plan->branch);
unset($this->config->bug->search['fields']['product']);
$product = $this->loadModel('product')->getByID($plan->product);
if($product->type == 'normal')
{
unset($this->config->bug->search['fields']['branch']);
unset($this->config->bug->search['params']['branch']);
}
else
{
$branches = $this->loadModel('branch')->getPairsByIdList(explode(',', trim($plan->branch, ',')));
$this->config->bug->search['fields']['branch'] = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]);
$this->config->bug->search['params']['branch']['values'] = array('' => '', BRANCH_MAIN => $this->lang->branch->main) + $branches;
}
$this->loadModel('search')->setSearchParams($this->config->bug->search);
}
/**
* 构造需求列表的摘要信息。
* Get the summary of product's stories.
*
* @param array $stories
* @access public
* @return string
*/
public function buildViewSummary(array $stories): string
{
$totalEstimate = 0.0;
$storyIdList = array();
$rateCount = 0;
$SRTotal = 0;
$URTotal = 0;
$ERTotal = 0;
foreach($stories as $story)
{
if($story->type == 'story') $SRTotal += 1;
if($story->type == 'requirement') $URTotal += 1;
if($story->type == 'epic') $ERTotal += 1;
if($story->isParent == '0') $totalEstimate += $story->estimate;
if($story->type != 'story') continue;
if($story->isParent == '0' && ($story->status != 'closed' || in_array($story->closedReason, array('done', 'postponed'))))
{
$storyIdList[] = $story->id;
$rateCount ++;
}
}
$casesCount = count($this->loadModel('product')->filterNoCasesStory($storyIdList));
$rate = empty($stories) || $rateCount == 0 ? 0 : round($casesCount / $rateCount, 2);
return sprintf($this->lang->productplan->storySummary, $ERTotal, $URTotal, $SRTotal, $totalEstimate, $rate * 100 . "%");
}
/**
* 构造计划详情页面的操作菜单。
* Build operate menu for plan detail page.
*
* @param object $plan
* @access public
* @return array
*/
public function buildViewActions(object $plan): array
{
$params = "planID=$plan->id";
$canEdit = common::hasPriv('productplan', 'edit');
$canStart = common::hasPriv('productplan', 'start') && $this->productplan->isClickable($plan, 'start');
$canFinish = common::hasPriv('productplan', 'finish') && $this->productplan->isClickable($plan, 'finish');
$canClose = common::hasPriv('productplan', 'close') && $this->productplan->isClickable($plan, 'close');
$canActivate = common::hasPriv('productplan', 'activate') && $this->productplan->isClickable($plan, 'activate');
$canCreateChild = common::hasPriv('productplan', 'create') && $this->productplan->isClickable($plan, 'create');
$canDelete = common::hasPriv('productplan', 'delete') && $this->productplan->isClickable($plan, 'delete');
$menu = array();
if($canStart) $menu[] = array('icon' => 'play text-primary', 'class' => 'ghost', 'text' => $this->lang->productplan->startAB, 'data-url' => helper::createLink('productplan', 'start', $params), 'data-action' => 'start', 'onclick' => 'ajaxConfirmLoad(this)');
if($canFinish) $menu[] = array('icon' => 'checked text-primary', 'class' => 'ghost', 'text' => $this->lang->productplan->finishAB, 'data-url' => helper::createLink('productplan', 'finish', $params), 'data-action' => 'finish', 'onclick' => 'ajaxConfirmLoad(this)');
if($canClose) $menu[] = array('icon' => 'off text-primary', 'class' => 'ghost', 'text' => $this->lang->productplan->closeAB, 'url' => helper::createLink('productplan', 'close', $params, '', true), 'data-toggle' => 'modal');
if($canActivate) $menu[] = array('icon' => 'magic text-primary', 'class' => 'ghost', 'text' => $this->lang->productplan->activateAB, 'data-url' => helper::createLink('productplan', 'activate', $params), 'data-action' => 'activate', 'onclick' => 'ajaxConfirmLoad(this)');
if($canCreateChild) $menu[] = array('icon' => 'split text-primary', 'class' => 'ghost', 'text' => $this->lang->productplan->children, 'url' => helper::createLink('productplan', 'create', "product={$plan->product}&branch={$plan->branch}&parent={$plan->id}"));
if($canEdit) $menu[] = array('icon' => 'edit text-primary', 'class' => 'ghost', 'text' => $this->lang->edit, 'url' => helper::createLink('productplan', 'edit', $params));
if($canDelete) $menu[] = array('icon' => 'trash text-primary', 'class' => 'ghost', 'text' => $this->lang->delete, 'data-url' => helper::createLink('productplan', 'delete', $params), 'data-action' => 'delete', 'onclick' => 'ajaxConfirmLoad(this)');
return $menu;
}
/**
* 对计划需求按照父子关系重新排序。
* Reorder stories by parent-child relationship.
*
* @access public
* @return void
*/
public function reorderStories()
{
/* 获取不分页的 SQL 语句,为重新按照父子关系排序做准备。*/
$sql = $this->dao->get();
if(strpos($sql, 'LIMIT')) $sql = substr($sql, 0, strpos($sql, 'LIMIT'));
/* 取出用户重新排序的关键字段。*/
$stories = array();
$query = $this->dao->query($sql);
while($story = $query->fetch()) $stories[$story->id] = $story->parent;
/* 对需求重新按照父子关系排序,保证进入需求详情后上一页下一页的URL符合预期。 */
$objectList = $this->loadModel('story')->reorderStories($stories);
if($objectList)
{
$this->session->set('storyBrowseList', array('sql' => $sql, 'idkey' => 'id', 'objectList' => $objectList), $this->app->tab);
$this->session->set('epicBrowseList', array('sql' => $sql, 'idkey' => 'id', 'objectList' => $objectList), $this->app->tab);
$this->session->set('requirementBrowseList', array('sql' => $sql, 'idkey' => 'id', 'objectList' => $objectList), $this->app->tab);
}
}
}