* @package kanban * @version $Id: model.php 5118 2021-10-22 10:18:41Z $ * @link https://www.zentao.net */ class kanbanTao extends kanbanModel { /* * 创建看板。 * Create a kanban. * * @param object $kanban * @access public * @return int */ protected function createKanban($kanban) { $this->dao->insert(TABLE_KANBAN)->data($kanban, 'copyRegion,copyKanbanID') ->autoCheck() ->batchCheck($this->config->kanban->create->requiredFields, 'notempty') ->checkIF(!$kanban->fluidBoard, 'colWidth', 'ge', $this->config->minColWidth) ->batchCheckIF($kanban->fluidBoard, 'minColWidth', 'ge', $this->config->minColWidth) ->checkIF($kanban->fluidBoard && $kanban->minColWidth >= $this->config->minColWidth, 'maxColWidth', 'gt', $kanban->minColWidth) ->check('name', 'unique', "space = {$kanban->space}") ->exec(); } /** * 构造看板区域的数据结构。 * Update a kanban. * * @param array $regionData * @param array $groups * @param array $laneGroup * @param array $columnGroup * @param array $cardGroup * @access public * @return array */ protected function buildRegionData($regionData, $groups, $laneGroup, $columnGroup, $cardGroup) { $laneCount = 0; $groupData = array(); foreach($groups as $group) { $lanes = zget($laneGroup, $group->id, array()); if(!$lanes) continue; $cols = zget($columnGroup, $group->id, array()); $items = zget($cardGroup, $group->id, array()); /* 计算各个列上的卡片数量。 */ $columnCount = array(); $parentCols = array(); foreach($cols as $col) $parentCols[$col['id']] = $col['parent']; foreach($items as $colGroup) { foreach($colGroup as $colID => $cards) { if(!isset($columnCount[$colID])) $columnCount[$colID] = 0; $columnCount[$colID] += count($cards); if(isset($parentCols[$colID]) && $parentCols[$colID] > 0) { if(!isset($columnCount[$parentCols[$colID]])) $columnCount[$parentCols[$colID]] = 0; $columnCount[$parentCols[$colID]] += count($cards); } } } foreach($cols as $colIndex => $col) $cols[$colIndex]['cards'] = isset($columnCount[$col['id']]) ? $columnCount[$col['id']] : 0; $laneCount += count($lanes); $groupData['id'] = $group->id; $groupData['key'] = "group{$group->id}"; $groupData['data']['lanes'] = $lanes; $groupData['data']['cols'] = $cols; $groupData['data']['items'] = $items; $regionData['items'][] = $groupData; } $regionData['laneCount'] = $laneCount; return $regionData; } /** * 更新看板区域的排序。 * Update sort of kanban region. * * @param array $regionIdList * @access public * @return void */ protected function updateRegionSort($regionIdList) { $order = 1; foreach($regionIdList as $regionID) { $this->dao->update(TABLE_KANBANREGION)->set('`order`')->eq($order)->where('id')->eq($regionID)->exec(); $order++; } } /** * 更新看板泳道的排序。 * Update sort of kanban lane. * * @param int $regionID * @param array $lanes * @access public * @return void */ protected function updateLaneSort($regionID, $lanes) { $order = 1; foreach($lanes as $laneID) { $this->dao->update(TABLE_KANBANLANE)->set('`order`')->eq($order)->where('id')->eq($laneID)->andWhere('region')->eq($regionID)->exec(); $order++; } } /** * 更新看板列的排序。 * Update sort of kanban lane. * * @param int $regionID * @param array $columns * @access public * @return void */ protected function updateColumnSort($regionID, $columns) { $order = 1; foreach($columns as $columnID) { $this->dao->update(TABLE_KANBANCOLUMN)->set('`order`')->eq($order)->where('id')->eq($columnID)->andWhere('region')->eq($regionID)->exec(); $order ++; } } /** * 为看板子列添加看板单元格。 * Add kanban cell for child column. * * @param int $columnID * @param int $childColumnID * @param int $i * @access public * @return void */ protected function addChildColumnCell($columnID, $childColumnID, $i = 0) { $cellList = $this->dao->select('*')->from(TABLE_KANBANCELL)->where('`column`')->eq($columnID)->fetchAll('', false); foreach($cellList as $cell) { $newCell = new stdclass(); $newCell->kanban = $cell->kanban; $newCell->lane = $cell->lane; $newCell->column = $childColumnID; $newCell->type = 'common'; $newCell->cards = $i == 0 ? $cell->cards : ''; $this->dao->insert(TABLE_KANBANCELL)->data($newCell)->exec(); $this->dao->update(TABLE_KANBANCELL)->set('cards')->eq('')->where('id')->eq($cell->id)->exec(); } } /** * 更新看板列的父列。 * Update parent column of kanban column. * * @param object $column * @access public * @return void */ protected function updateColumnParent($column) { $children = $this->dao->select('COUNT(1) AS count')->from(TABLE_KANBANCOLUMN) ->where('parent')->eq($column->parent) ->andWhere('id')->ne($column->id) ->andWhere('deleted')->eq('0') ->andWhere('archived')->eq('0') ->fetch('count'); if(!$children) $this->dao->update(TABLE_KANBANCOLUMN)->set('parent')->eq(0)->where('id')->eq($column->parent)->exec(); } /** * 转入卡片时,更新卡片的指派人。 * Update assignedTo of card when move in. * * @param int $cardID * @param string $oldAssignedToList * @param array $users * @access public * @return void */ protected function updateCardAssignedTo($cardID, $oldAssignedToList, $users) { $assignedToList = explode(',', $oldAssignedToList); foreach($assignedToList as $index => $account) { if(!isset($users[$account])) unset($assignedToList[$index]); } $assignedToList = implode(',', $assignedToList); $assignedToList = trim($assignedToList, ','); if($oldAssignedToList != $assignedToList) { $this->dao->update(TABLE_KANBANCARD)->set('assignedTo')->eq($assignedToList)->where('id')->eq($cardID)->exec(); } } /** * 获取可转入卡片的产品。 * Get products can be imported. * * @param string $objectType * @access public * @return array */ protected function getCanImportProducts($objectType = 'productplan') { $productPairs = $this->loadModel('product')->getPairs('', 0, '', 'all'); $excludeProducts = array(); if($objectType == 'productPlan') { $excludeProducts = $this->dao->select('t1.product')->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id') ->where('t2.type')->eq('project') ->andWhere('t2.model')->ne('scrum') ->andWhere('t2.hasProduct')->eq('0') ->fetchPairs(); } elseif($objectType == 'release') { $excludeProducts = $this->dao->select('t1.product')->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id') ->where('t2.type')->eq('project') ->andWhere('t2.model')->eq('kanban') ->andWhere('t2.hasProduct')->eq('0') ->fetchPairs(); } if(!empty($excludeProducts)) { foreach($productPairs as $id) { if(isset($excludeProducts[$id])) unset($productPairs[$id]); } } return array($this->lang->kanban->allProducts) + $productPairs; } /** * 构造导入卡片的数据结构。 * Build import card data structure. * * @param object $objectType * @param object $object * @param string $fromType * @param array $creators * @access public * @return object * @param object $objectCard */ protected function buildObjectCard($objectCard, $object, $fromType, $creators) { if($fromType == 'productplan' or $fromType == 'release') { $objectCard->createdBy = zget($creators, $object->id, ''); $objectCard->delay = helper::today() > $objectCard->end ? true : false; } if($fromType =='execution') { if($object->status != 'done' and $object->status != 'closed' and $object->status != 'suspended') { $delay = helper::diffDate(helper::today(), $object->end); if($delay > 0) $objectCard->delay = $delay; } $objectCard->execType = $object->type; $objectCard->progress = $object->progress; $parentExecutions = $this->dao->select('id,name')->from(TABLE_EXECUTION)->where('id')->in(trim($object->path, ','))->andWhere('type')->in('stage,kanban,sprint')->orderBy('grade')->fetchPairs(); $objectCard->title = implode('/', $parentExecutions); $children = $this->dao->select('count(1) as children')->from(TABLE_EXECUTION)->where('parent')->eq($object->id)->andWhere('type')->in('stage,kanban,sprint')->andWhere('deleted')->eq(0)->fetch('children'); $objectCard->children = !empty($children) ? $children : 0; } $objectCard->originDesc = str_replace(array('

', '

'), "\n", $object->desc); $objectCard->originDesc = strip_tags($objectCard->originDesc); $objectCard->desc = strip_tags(htmlspecialchars_decode($object->desc)); $objectCard->objectStatus = $objectCard->status; $objectCard->status = $objectCard->progress == 100 ? 'done' : 'doing'; return $objectCard; } /** * 更新执行下看板的卡片。 * Update card of execution. * * @param int $executionID * @param int $colID * @param int $laneID * @param string $cards * @access public * @return void */ protected function updateExecutionCell($executionID, $colID, $laneID, $cards) { $this->dao->update(TABLE_KANBANCELL)->set('cards')->eq($cards) ->where('kanban')->eq($executionID) ->andWhere('lane')->eq($laneID) ->andWhere('`column`')->eq($colID) ->exec(); } /** * 获取产品计划看板的分支 * Get branches for product plan kanban. * * @param object $product * @param string $branchID * @access public * @return array */ protected function getBranchesForPlanKanban($product, $branchID) { $this->loadModel('branch'); $branches = array(); if($product->type == 'normal') { $branches = array('all' => $this->lang->productplan->allAB); } elseif($branchID == 'all') { $branches = $this->branch->getPairs($product->id, 'active'); } elseif($branchID == BRANCH_MAIN) { $branches = array(BRANCH_MAIN => $this->lang->branch->main); } elseif($branchID) { foreach(explode(',', $branchID) as $id) { $branchName = $this->branch->getById($id); $branches[$id] = $branchName; } } return $branches; } /** * 获取专业研发看板分组视图下的看板数据。 * Get kanban data for group view of RD kanban. * * @param array $regionData * @param array $groups * @param array $laneGroup * @param array $columnGroup * @param array $cardGroup * @param string $searchValue * @access public * @return array */ protected function buildRDRegionData($regionData, $groups, $laneGroup, $columnGroup, $cardGroup, $searchValue = '') { $laneCount = 0; $groupData = array(); $fromKanbanID = ''; foreach($groups as $group) { $lanes = zget($laneGroup, $group->id, array()); if(!$lanes) continue; $kanbanID = "group{$group->id}"; $cols = zget($columnGroup, $group->id, array()); $items = zget($cardGroup, $group->id, array()); if($searchValue != '' and empty($items)) continue; $lane = current($lanes); if($lane['type'] == 'parentStory') { $fromKanbanID = $kanbanID; } elseif($lane['type'] == 'story') { foreach($items as $colGroup) { foreach($colGroup as $cards) { foreach($cards as $card) { $regionData['links'][] = array('fromKanban' => $fromKanbanID, 'toKanban' => $kanbanID, 'from' => $card['parent'], 'to' => $card['id']); } } } } /* 计算各个列上的卡片数量。 */ $columnCount = array(); $parentCols = array(); foreach($cols as $col) $parentCols[$col['id']] = $col['parent']; foreach($items as $colGroup) { foreach($colGroup as $colID => $cards) { if(!isset($columnCount[$colID])) $columnCount[$colID] = 0; $columnCount[$colID] += count($cards); if(isset($parentCols[$colID]) && $parentCols[$colID] > 0) { if(!isset($columnCount[$parentCols[$colID]])) $columnCount[$parentCols[$colID]] = 0; $columnCount[$parentCols[$colID]] += count($cards); } } } foreach($cols as $colIndex => $col) { $cols[$colIndex]['cards'] = isset($columnCount[$col['id']]) ? $columnCount[$col['id']] : 0; $cols[$colIndex]['laneType'] = $lane['type']; } $lanes = array_values($lanes); $laneCount += count($lanes); $groupData['id'] = $group->id; $groupData['key'] = $kanbanID; $groupData['data']['lanes'] = $lanes; $groupData['data']['cols'] = $cols; $groupData['data']['items'] = $items; $regionData['items'][] = $groupData; } $regionData['laneCount'] = $laneCount; return $regionData; } /** * 初始化卡片数据。 * Init card data. * * @param object $card * @param object $cell * @param int $order * @param array $avatarPairs * @param array $users * @access public * @return array */ protected function initCardItem($card, $cell, $order, $avatarPairs, $users) { $item = array(); $item['column'] = $cell->column; $item['lane'] = $cell->lane; $item['title'] = !empty($card->title) ? htmlspecialchars_decode($card->title) : htmlspecialchars_decode($card->name); $item['id'] = $card->id; $item['name'] = $card->id; $item['pri'] = $card->pri; $item['color'] = $card->color; $item['assignedTo'] = $card->assignedTo; $item['isParent'] = !empty($card->isParent) ? $card->isParent: 0; $item['progress'] = !empty($card->progress) ? $card->progress : 0; $item['group'] = !empty($card->group) ? $card->group : ''; $item['region'] = !empty($card->region) ? $card->region : ''; $item['begin'] = !empty($card->begin) ? $card->begin : ''; $item['end'] = !empty($card->end) ? $card->end : ''; $item['fromID'] = !empty($card->fromID) ? $card->fromID : 0; $item['fromType'] = !empty($card->fromType) ? $card->fromType : ''; $item['desc'] = !empty($card->desc) ? $card->desc : ''; $item['originDesc'] = !empty($card->originDesc) ? $card->originDesc : ''; $item['delay'] = !empty($card->delay) ? $card->delay : 0; $item['status'] = !empty($card->status) ? $card->status : ''; $item['objectStatus'] = !empty($card->objectStatus) ? $card->objectStatus : ''; $item['deleted'] = !empty($card->deleted) ? $card->deleted : 0; $item['date'] = !empty($card->date) ? $card->date : ''; $item['estimate'] = !empty($card->estimate) ? $card->estimate : 0; $item['deadline'] = !empty($card->deadline) ? $card->deadline : ''; $item['severity'] = !empty($card->severity) ? $card->severity : ''; $item['cardType'] = $cell->type; $item['avatarList'] = array(); $item['realnames'] = ''; $item['order'] = $order; $item['parent'] = 0; if(!empty($card->originParent)) $item['parent'] = $card->originParent; if(!empty($card->rawParent)) $item['parent'] = $card->rawParent; if($card->assignedTo) { $assignedToList = explode(',', $card->assignedTo); foreach($assignedToList as $account) { if(!$account) continue; $maxTextLen = 2; $realname = zget($users, $account, ''); $mbLength = mb_strlen($realname, 'utf-8'); $strLength = strlen($realname); $displayText = ''; if($realname) { if(isset($realname[0]) && $strLength === $mbLength) { /* Pure alphabet or numbers 纯英文情况 */ $displayText = strtoupper($realname[0]); } else if($strLength % $mbLength == 0 && $strLength % 3 == 0) { /* Pure chinese characters 纯中文的情况 */ $displayText = $mbLength <= $maxTextLen ? $realname : mb_substr($realname, $mbLength - $maxTextLen, $mbLength, 'utf-8'); } else { /* Mix of Chinese and English 中英文混合的情况 */ $displayText = $mbLength <= $maxTextLen ? $realname : mb_substr($realname, 0, $maxTextLen, 'utf-8'); } } $userAvatar = zget($avatarPairs, $account, ''); $userAvatar = $userAvatar ? "" : $displayText; $item['avatarList'][] = $userAvatar; $item['realnames'] .= $realname . ' '; } } if($cell->type == 'task') { $item['left'] = $card->left; $item['estStarted'] = $card->estStarted; $item['mode'] = $card->mode; $item['execution'] = $card->execution; $item['story'] = $card->story; $item['module'] = $card->module; $dbPrivs = array(); $actions = array('edit', 'restart', 'pause', 'recordworkhour', 'activate', 'delete', 'cancel', 'assignto'); foreach($actions as $action) { $dbPrivs[$action] = common::hasDBPriv($card, 'task', $action); } $item['dbPrivs'] = $dbPrivs; $item['canSplit'] = $this->loadModel('task')->isClickable($card, 'batchCreate') && !$card->mode; } return $item; } /** * 处理看板中多人任务的指派人。 * Process assignedTo of multi tasks. * * @param array $cardList * @access public * @return array */ protected function appendTeamMember($cardList) { $multiTasks = array(); foreach($cardList as $id => $task) { if($task->mode == 'multi') $multiTasks[$id] = $task; } $taskTeams = $this->dao->select('t1.account,t1.task,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', 'account'); foreach($multiTasks as $taskID => $task) { $teamPairs = array(); foreach($taskTeams[$taskID] as $account => $team) $teamPairs[$account] = $team->realname; $task->teamMember = $teamPairs; $cardList[$taskID] = $task; } return $cardList; } /** * 构造看板的分组视图。 * Build group view of kanban. * * @param array $lanes * @param array $columns * @param array $cardGroup * @param string $searchValue * @param string $groupBy * @param string $browseType * @param array $menus * @access public * @return array */ protected function buildGroupKanban($lanes, $columns, $cardGroup, $searchValue, $groupBy, $browseType, $menus) { $columnList = array(); $laneList = array(); $cardList = array(); $avatarPairs = $this->loadModel('user')->getAvatarPairs(); $users = $this->loadModel('user')->getPairs('noletter'); $module = $browseType == 'parentStory' ? 'story' : $browseType; $regionID = $this->session->execution ? $this->dao->select('id')->from(TABLE_KANBANREGION)->where('deleted')->eq('0')->andWhere('kanban')->eq($this->session->execution)->orderBy('id_asc')->limit(1)->fetch('id') : 0; foreach($lanes as $laneID => $lane) { $laneData = array(); $laneData['id'] = $groupBy . $laneID; $laneData['type'] = $browseType; $laneData['name'] = $laneData['id']; $laneData['region'] = $regionID ? $regionID : $lane->execution; $laneData['title'] = (($groupBy == 'pri' or $groupBy == 'severity') and $laneID) ? $this->lang->$module->$groupBy . ':' . $lane->name : $lane->name; $laneData['color'] = $lane->color; $laneData['order'] = $lane->order; if(empty($laneID) and !in_array($groupBy, array('module', 'story', 'pri', 'severity'))) $laneID = ''; /* Construct kanban column data. */ foreach($columns as $column) { $cardIdList = array_unique(array_filter(explode(',', $column->cards))); $columnData = $this->buildGroupColumn($columnList, $column, $laneData, $browseType); list($cardCount, $cardData) = $this->buildGroupCard($cardGroup, $cardIdList, $column, (string)$laneID, $groupBy, $browseType, $searchValue, $avatarPairs, $users, $menus); $columnData['cards'] += $cardCount; $cardList[$laneData['id']][$column->column] = $cardData; $columnList[$column->column] = $columnData; } $laneList[] = $laneData; } foreach($columnList as $column) { if(isset($column['parentName']) && isset($columnList[$column['parentName']])) $columnList[$column['parentName']]['cards'] += $column['cards']; } return array($laneList, $columnList, $cardList); } /** * 构造看板分组视图的列。 * Build column of group view. * * @param array $columnsL * @param object $column * @param array $laneData * @param string $browseType * @access public * @return array * @param mixed[] $columnList */ protected function buildGroupColumn($columnList, $column, $laneData, $browseType) { if(!isset($columnList[$column->column])) { $columnData = array(); $columnData['id'] = $column->column; $columnData['type'] = $column->columnType; $columnData['name'] = $column->column; $columnData['title'] = $column->columnName; $columnData['color'] = $column->color; $columnData['limit'] = $column->limit; $columnData['region'] = $laneData['region']; $columnData['laneName'] = $column->lane; $columnData['group'] = $browseType; $columnData['cards'] = 0; $columnData['actionList'] = array('setColumn', 'setWIP'); if($column->parent > 0) $columnData['parentName'] = $column->parent; } else { $columnData = $columnList[$column->column]; } return $columnData; } /** * 构造看板分组视图的卡片。 * Build card of group view. * * @param array $cardGroup * @param array $cardIdList * @param object $column * @param string $laneID * @param string $groupBy * @param string $browseType * @param string $searchValue * @param array $avatarPairs * @param array $users * @param array $menus * @access public * @return array */ protected function buildGroupCard($cardGroup, $cardIdList, $column, $laneID, $groupBy, $browseType, $searchValue, $avatarPairs, $users, $menus) { $cardCount = 0; $cardList = array(); $objects = zget($cardGroup, $column->columnType, array()); foreach($cardIdList as $cardID) { $object = zget($objects, $cardID, array()); if(empty($object)) continue; if(in_array($groupBy, array('module', 'story', 'pri', 'severity')) and $object->$groupBy != $laneID) continue; if(in_array($groupBy, array('type', 'category', 'source')) and $object->$groupBy != $laneID) continue; if($groupBy == 'assignedTo') { $laneID = (string)$laneID; if(empty($object->$groupBy)) $object->$groupBy = ''; if(empty($object->teamMember) and $object->$groupBy != $laneID) continue; if(!empty($object->teamMember) and !in_array($laneID, array_keys($object->teamMember), true)) continue; } $cardData = $this->buildExecutionCard($object, $column, $browseType, $searchValue, $menus); if(empty($cardData)) continue; $cardData['cardType'] = $browseType; if($groupBy == 'assignedTo' && $object->$groupBy !== $laneID) $cardData['assignedTo'] = $laneID; if($cardData['assignedTo']) { $userAvatar = zget($avatarPairs, $cardData['assignedTo'], ''); $userAvatar = $userAvatar ? "" : strtoupper(mb_substr($cardData['assignedTo'], 0, 1, 'utf-8')); $cardData['avatarList'][] = $userAvatar; $cardData['realnames'] = zget($users, $cardData['assignedTo'], ''); } $cardList[] = $cardData; $cardCount ++; } return array($cardCount, $cardList); } /** * 获取看板分组视图的对象键值对。 * Get object pairs for group view. * * @param string $groupBy * @param array $groupByList * @param string $browseType * @param string $orderBy * @access public * @return array */ protected function getObjectPairs($groupBy, $groupByList, $browseType, $orderBy) { if($browseType == 'parentStory') $browseType = 'story'; $objectPairs = array(); if(in_array($groupBy, array('module', 'story', 'assignedTo'))) { if($groupBy == 'module') { $objectPairs += $this->dao->select('id,name')->from(TABLE_MODULE)->where('type')->in('story,task,bug')->andWhere('deleted')->eq('0')->andWhere('id')->in($groupByList)->fetchPairs(); } elseif($groupBy == 'story') { $objectPairs += $this->dao->select('id,title')->from(TABLE_STORY)->where('deleted')->eq(0)->andWhere('id')->in($groupByList)->orderBy($orderBy)->fetchPairs(); } else { $objectPairs += $this->dao->select('account,realname')->from(TABLE_USER)->where('account')->in($groupByList)->fetchPairs(); if(isset($groupByList['closed'])) $objectPairs['closed'] = 'Closed'; } } else { unset($this->lang->$browseType->{$groupBy . 'List'}[0]); unset($this->lang->$browseType->{$groupBy . 'List'}['']); $objectPairs += $this->lang->$browseType->{$groupBy . 'List'}; } if(in_array($groupBy, array('module', 'story', 'pri', 'severity'))) $objectPairs[0] = $this->lang->$browseType->$groupBy . ': ' . $this->lang->kanban->noGroup; if(in_array($groupBy, array('assignedTo', 'source'))) $objectPairs[] = $this->lang->$browseType->$groupBy . ': ' . $this->lang->kanban->noGroup; if($browseType == 'bug' and $groupBy == 'type') $objectPairs[0] = $this->lang->$browseType->$groupBy . ': ' . $this->lang->kanban->noGroup; return $objectPairs; } /** * 更新专业研发看板中的用需、业需、父需求泳道上的卡片。 * Update card of feature, epic, parent story lane in RD kanban. * * @param array $cardPairs * @param int $executionID * @param string $otherCardList * @param string $laneType * @access public * @return array */ protected function refreshERURCards($cardPairs, $executionID, $otherCardList, $laneType = 'story') { $storyType = $laneType == 'parentStory' ? 'story' : $laneType; $stories = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory', '0', $storyType, $otherCardList); foreach($stories as $storyID => $story) { if($laneType == 'parentStory' && $story->isParent != '1') continue; foreach($this->lang->kanban->ERURColumn as $stage => $langItem) { if($story->stage != $stage and strpos((string)$cardPairs[$stage], ",$storyID,") !== false) { $cardPairs[$stage] = str_replace(",$storyID,", ',', $cardPairs[$stage]); } if($story->stage == $stage and strpos((string)$cardPairs[$stage], ",$storyID,") === false) { $cardPairs[$stage] = empty($cardPairs[$stage]) ? ",$storyID," : ",$storyID" . $cardPairs[$stage]; } } } return $cardPairs; } /** * 更新专业研发看板中的需求泳道上的卡片。 * Update card of story lane in RD kanban. * * @param array $cardPairs * @param int $executionID * @param string $otherCardList * @access public * @return array */ protected function refreshStoryCards($cardPairs, $executionID, $otherCardList) { $stories = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory', '0', 'story', $otherCardList); foreach($stories as $storyID => $story) { foreach($this->config->kanban->storyColumnStageList as $colType => $stage) { if(!isset($cardPairs[$colType])) continue; if($story->stage != $stage and strpos($cardPairs[$colType], ",$storyID,") !== false) { $cardPairs[$colType] = str_replace(",$storyID,", ',', $cardPairs[$colType]); } if(strpos(',ready,backlog,design,develop,test,', $colType) !== false) continue; if($story->stage == $stage and strpos($cardPairs[$colType], ",$storyID,") === false) { $cardPairs[$colType] = empty($cardPairs[$colType]) ? ",$storyID," : ",$storyID" . $cardPairs[$colType]; } } if(strpos('wait,projected', $story->stage) !== false and strpos($cardPairs['ready'], ",$storyID,") === false and strpos($cardPairs['backlog'], ",$storyID,") === false) { $cardPairs['backlog'] = empty($cardPairs['backlog']) ? ",$storyID," : ",$storyID" . $cardPairs['backlog']; } } return $cardPairs; } /** * 更新专业研发看板中的Bug泳道上的卡片。 * Update card of bug lane in RD kanban. * * @param array $cardPairs * @param int $executionID * @param string $otherCardList * @access public * @return array */ protected function refreshBugCards($cardPairs, $executionID, $otherCardList) { $bugs = $this->loadModel('bug')->getExecutionBugs($executionID, 0, 'all', '0', '', 0, 'id_desc', $otherCardList); foreach($bugs as $bugID => $bug) { foreach($this->config->kanban->bugColumnStatusList as $colType => $status) { if($bug->status != $status and strpos($cardPairs[$colType], ",$bugID,") !== false) { $cardPairs[$colType] = str_replace(",$bugID,", ',', $cardPairs[$colType]); } if(strpos(',resolving,test,testing,tested,', $colType) !== false) continue; if($colType == 'unconfirmed' and $bug->status == $status and $bug->confirmed == 0 and strpos($cardPairs['unconfirmed'], ",$bugID,") === false and strpos($cardPairs['fixing'], ",$bugID,") === false and $bug->activatedCount == 0) { $cardPairs['unconfirmed'] = empty($cardPairs['unconfirmed']) ? ",$bugID," : ",$bugID" . $cardPairs['unconfirmed']; if(strpos($cardPairs['closed'], ",$bugID,") !== false) $cardPairs['closed'] = str_replace(",$bugID,", ',', $cardPairs['closed']); } elseif($colType == 'confirmed' and $bug->status == $status and $bug->confirmed == 1 and strpos($cardPairs['confirmed'], ",$bugID,") === false and strpos($cardPairs['fixing'], ",$bugID,") === false and $bug->activatedCount == 0) { $cardPairs['confirmed'] = empty($cardPairs['confirmed']) ? ",$bugID," : ",$bugID" . $cardPairs['confirmed']; if(strpos($cardPairs['unconfirmed'], ",$bugID,") !== false) $cardPairs['unconfirmed'] = str_replace(",$bugID,", ',', $cardPairs['unconfirmed']); } elseif($colType == 'fixing' and $bug->status == $status and $bug->activatedCount > 0 and strpos($cardPairs['fixing'], ",$bugID,") === false) { $cardPairs['fixing'] = empty($cardPairs['fixing']) ? ",$bugID," : ",$bugID" . $cardPairs['fixing']; if(strpos($cardPairs['confirmed'], ",$bugID,") !== false) $cardPairs['confirmed'] = str_replace(",$bugID,", ',', $cardPairs['confirmed']); if(strpos($cardPairs['unconfirmed'], ",$bugID,") !== false) $cardPairs['unconfirmed'] = str_replace(",$bugID,", ',', $cardPairs['unconfirmed']); } elseif($colType == 'fixed' and $bug->status == $status and strpos($cardPairs['fixed'], ",$bugID,") === false and strpos($cardPairs['testing'], ",$bugID,") === false and strpos($cardPairs['tested'], ",$bugID,") === false) { $cardPairs['fixed'] = empty($cardPairs['fixed']) ? ",$bugID," : ",$bugID" . $cardPairs['fixed']; if(strpos($cardPairs['testing'], ",$bugID,") !== false) $cardPairs['testing'] = str_replace(",$bugID,", ',', $cardPairs['testing']); if(strpos($cardPairs['tested'], ",$bugID,") !== false) $cardPairs['tested'] = str_replace(",$bugID,", ',', $cardPairs['tested']); } elseif($colType == 'closed' and $bug->status == 'closed' and strpos($cardPairs[$colType], ",$bugID,") === false) { $cardPairs[$colType] = empty($cardPairs[$colType]) ? ",$bugID," : ",$bugID". $cardPairs[$colType]; } } } return $cardPairs; } /** * 更新专业研发看板中的任务泳道上的卡片。 * Update card of bug lane in RD kanban. * * @param array $cardPairs * @param int $executionID * @param string $otherCardList * @access public * @return array */ protected function refreshTaskCards($cardPairs, $executionID, $otherCardList) { $tasks = $this->loadModel('execution')->getKanbanTasks($executionID, 'status_asc, id_desc', explode(',', $otherCardList)); foreach($tasks as $taskID => $task) { $task->status = $task->status == 'changed' ? $task->rawStatus : $task->status; foreach($this->config->kanban->taskColumnStatusList as $colType => $status) { if($colType == 'develop') continue; if(!isset($cardPairs[$colType])) continue; if($task->status == $status and strpos($cardPairs[$colType], ",$taskID,") === false) { $cardPairs[$colType] = empty($cardPairs[$colType]) ? ",$taskID," : ",$taskID". $cardPairs[$colType]; } elseif($task->status != $status and strpos($cardPairs[$colType], ",$taskID,") !== false) { $cardPairs[$colType] = str_replace(",$taskID,", ',', $cardPairs[$colType]); } } } return $cardPairs; } /** * 获取父需求类型的卡片操作菜单。 * Get menu of story card. * * @param int $executionID * @param array $objects * @access public * @return array */ protected function getERURCardMenu($executionID, $objects) { $execution = $this->loadModel('execution')->getByID($executionID); $menus = array(); $objects = $this->loadModel('story')->mergeReviewer($objects); foreach($objects as $story) { $menu = array(); if(common::hasPriv($story->type, 'edit') and $this->story->isClickable($story, 'edit')) $menu[] = array('label' => $this->lang->story->edit, 'icon' => 'edit', 'url' => helper::createLink($story->type, 'edit', "storyID=$story->id"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv($story->type, 'change') and $this->story->isClickable($story, 'change')) $menu[] = array('label' => $this->lang->story->change, 'icon' => 'alter', 'url' => helper::createLink($story->type, 'change', "storyID=$story->id"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv($story->type, 'review') and $this->story->isClickable($story, 'review')) $menu[] = array('label' => $this->lang->story->review, 'icon' => 'search', 'url' => helper::createLink($story->type, 'review', "storyID=$story->id"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv($story->type, 'activate') and $this->story->isClickable($story, 'activate')) $menu[] = array('label' => $this->lang->story->activate, 'icon' => 'magic', 'url' => helper::createLink($story->type, 'activate', "storyID=$story->id"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('execution', 'unlinkStory') && $execution->hasProduct && empty($story->frozen)) $menu[] = array('label' => $this->lang->execution->unlinkStory, 'icon' => 'unlink', 'url' => helper::createLink('execution', 'unlinkStory', "executionID=$executionID&storyID=$story->story&confirm=no&from=taskkanban")); if(common::hasPriv($story->type, 'delete') && empty($story->frozen)) $menu[] = array('label' => $this->lang->story->delete, 'icon' => 'trash', 'url' => helper::createLink($story->type, 'delete', "storyID=$story->id&confirm=no&from=taskkanban")); $menus[$story->id] = $menu; } return $menus; } /** * 获取需求类型的卡片操作菜单。 * Get menu of story card. * * @param object $execution * @param array $objects * @access public * @return array */ protected function getStoryCardMenu($execution, $objects) { $menus = array(); $objects = $this->loadModel('story')->mergeReviewer($objects); foreach($objects as $story) { $menu = array(); $toTaskPriv = strpos('draft,reviewing,closed', $story->status) !== false ? false : true; if(common::hasPriv('story', 'edit') && $this->story->isClickable($story, 'edit')) $menu[] = array('label' => $this->lang->story->edit, 'icon' => 'edit', 'url' => helper::createLink('story', 'edit', "storyID=$story->id"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('story', 'change') && $this->story->isClickable($story, 'change')) $menu[] = array('label' => $this->lang->story->change, 'icon' => 'alter', 'url' => helper::createLink('story', 'change', "storyID=$story->id"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('story', 'review') && $this->story->isClickable($story, 'review')) $menu[] = array('label' => $this->lang->story->review, 'icon' => 'search', 'url' => helper::createLink('story', 'review', "storyID=$story->id"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('task', 'create') && $toTaskPriv) $menu[] = array('label' => $this->lang->execution->wbs, 'icon' => 'plus', 'url' => helper::createLink('task', 'create', "executionID={$execution->id}&storyID=$story->id&moduleID=$story->module"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('task', 'batchCreate') && $toTaskPriv) $menu[] = array('label' => $this->lang->execution->batchWBS, 'icon' => 'pluses', 'url' => helper::createLink('task', 'batchCreate', "executionID={$execution->id}&storyID=$story->id&moduleID=0&taskID=0&iframe=true"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('story', 'activate') && $this->story->isClickable($story, 'activate')) $menu[] = array('label' => $this->lang->story->activate, 'icon' => 'magic', 'url' => helper::createLink('story', 'activate', "storyID=$story->id"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('execution', 'unlinkStory') && $execution->hasProduct && empty($story->frozen)) $menu[] = array('label' => $this->lang->execution->unlinkStory, 'icon' => 'unlink', 'url' => helper::createLink('execution', 'unlinkStory', "executionID={$execution->id}&storyID=$story->story&confirm=no&from=taskkanban")); if(common::hasPriv('story', 'delete') && empty($story->frozen)) $menu[] = array('label' => $this->lang->story->delete, 'icon' => 'trash', 'url' => helper::createLink('story', 'delete', "storyID=$story->id&confirm=no&from=taskkanban")); $menus[$story->id] = $menu; } return $menus; } /** * 获取Bug类型的卡片操作菜单。 * Get menu of bug card. * * @param array $objects * @access public * @return array */ protected function getBugCardMenu($objects) { $menus = array(); $this->loadModel('bug'); foreach($objects as $bug) { $menu = array(); if(common::hasPriv('bug', 'edit') && $this->bug->isClickable($bug, 'edit')) $menu[] = array('label' => $this->lang->bug->edit, 'icon' => 'edit', 'url' => helper::createLink('bug', 'edit', "bugID=$bug->id"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('bug', 'confirm') && $this->bug->isClickable($bug, 'confirm')) $menu[] = array('label' => $this->lang->bug->confirm, 'icon' => 'ok', 'url' => helper::createLink('bug', 'confirm', "bugID=$bug->id&extra=&from=taskkanban"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('bug', 'resolve') && $this->bug->isClickable($bug, 'resolve')) $menu[] = array('label' => $this->lang->bug->resolve, 'icon' => 'checked', 'url' => helper::createLink('bug', 'resolve', "bugID=$bug->id&extra=&from=taskkanban"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('bug', 'close') && $this->bug->isClickable($bug, 'close')) $menu[] = array('label' => $this->lang->bug->close, 'icon' => 'off', 'url' => helper::createLink('bug', 'close', "bugID=$bug->id&extra=&from=taskkanban"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('bug', 'create') && $this->bug->isClickable($bug, 'create')) $menu[] = array('label' => $this->lang->bug->copy, 'icon' => 'copy', 'url' => helper::createLink('bug', 'create', "productID=$bug->product&branch=$bug->branch&extras=bugID=$bug->id"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('bug', 'activate') && $this->bug->isClickable($bug, 'activate')) $menu[] = array('label' => $this->lang->bug->activate, 'icon' => 'magic', 'url' => helper::createLink('bug', 'activate', "bugID=$bug->id"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('story', 'create') && $bug->status != 'closed') $menu[] = array('label' => $this->lang->bug->toStory, 'icon' => 'lightbulb', 'url' => helper::createLink('story', 'create', "product=$bug->product&branch=$bug->branch&module=0&story=0&execution=0&bugID=$bug->id"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('bug', 'delete')) $menu[] = array('label' => $this->lang->bug->delete, 'icon' => 'trash', 'url' => helper::createLink('bug', 'delete', "bugID=$bug->id&confirm=no&from=taskkanban"), 'confirm' => $this->lang->bug->notice->confirmDelete); $menus[$bug->id] = $menu; } return $menus; } /** * 获取任务类型的卡片操作菜单。 * Get menu of bug card. * * @param array $objects * @param int $executionID * @access public * @return array */ protected function getTaskCardMenu($objects, $executionID) { $menus = array(); $canStartExecution = true; if($this->config->edition == 'ipd') { $executionStatus = $this->loadModel('execution')->checkStageStatus($executionID, 'start'); $canStartExecution = empty($executionStatus['disabled']); } $this->loadModel('task'); foreach($objects as $task) { $menu = array(); if(common::hasPriv('task', 'edit') and common::hasDBPriv($task, 'task', 'edit') and $this->task->isClickable($task, 'edit')) $menu['edit'] = array('label' => $this->lang->task->edit, 'icon' => 'edit', 'url' => helper::createLink('task', 'edit', "taskID=$task->id&comment=false&kanbanGroup=default&from=taskkanban"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('task', 'finish') and common::hasDBPriv($task, 'task', 'finish') and $this->task->isClickable($task, 'finish') and $canStartExecution) $menu['finish'] = array('label' => $this->lang->task->finish, 'icon' => 'checked', 'url' => helper::createLink('task', 'finish', "taskID=$task->id&extra=from=taskkanban"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('task', 'pause') and common::hasDBPriv($task, 'task', 'pause') and $this->task->isClickable($task, 'pause')) $menu['pause'] = array('label' => $this->lang->task->pause, 'icon' => 'pause', 'url' => helper::createLink('task', 'pause', "taskID=$task->id&extra=from=taskkanban"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('task', 'start') and common::hasDBPriv($task, 'task', 'start') and $this->task->isClickable($task, 'start')) $menu['start'] = array('label' => $this->lang->task->start, 'icon' => 'play', 'url' => helper::createLink('task', 'start', "taskID=$task->id&from=taskkanban"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('task', 'restart') and common::hasDBPriv($task, 'task', 'restart') and $this->task->isClickable($task, 'restart')) $menu['restart'] = array('label' => $this->lang->task->restart, 'icon' => 'play', 'url' => helper::createLink('task', 'restart', "taskID=$task->id&from=taskkanban"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('task', 'recordWorkhour') and common::hasDBPriv($task, 'task', 'recordWorkhour') and $this->task->isClickable($task, 'recordWorkhour') and $canStartExecution) $menu['recordWorkhour'] = array('label' => $this->lang->task->recordWorkhour, 'icon' => 'time', 'url' => helper::createLink('task', 'recordWorkhour', "taskID=$task->id&from=taskkanban"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('task', 'activate') and common::hasDBPriv($task, 'task', 'activate') and $this->task->isClickable($task, 'activate')) $menu['activate'] = array('label' => $this->lang->task->activate, 'icon' => 'magic', 'url' => helper::createLink('task', 'activate', "taskID=$task->id&extra=from=taskkanban"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('task', 'batchCreate') and common::hasDBPriv($task, 'task', 'batchCreate') and $this->task->isClickable($task, 'batchCreate') and !$task->mode) $menu['batchCreate'] = array('label' => $this->lang->task->children, 'icon' => 'split', 'url' => helper::createLink('task', 'batchCreate', "execution=$task->execution&storyID=$task->story&moduleID=$task->module&taskID=$task->id"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('task', 'create') and common::hasDBPriv($task, 'task', 'create') and $this->task->isClickable($task, 'create')) $menu['create'] = array('label' => $this->lang->task->copy, 'icon' => 'copy', 'url' => helper::createLink('task', 'create', "projctID=$task->execution&storyID=$task->story&moduleID=$task->module&taskID=$task->id"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('task', 'cancel') and common::hasDBPriv($task, 'task', 'cancel') and $this->task->isClickable($task, 'cancel')) $menu['cancel'] = array('label' => $this->lang->task->cancel, 'icon' => 'ban-circle', 'url' => helper::createLink('task', 'cancel', "taskID=$task->id&extra=from=taskkanban"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('task', 'delete') and common::hasDBPriv($task, 'task', 'delete')) $menu['delete'] = array('label' => $this->lang->task->delete, 'icon' => 'trash', 'url' => helper::createLink('task', 'delete', "taskID=$task->id&confirm=no&from=taskkanban"), 'confirm' => $task->isParent ? $this->lang->task->confirmDeleteParent : $this->lang->task->confirmDelete); $menus[$task->id] = $menu; } return $menus; } /** * 获取风险看板操作菜单。 * Get the Kanban risk menu. * * @param int $executionID * @access public * @return array */ public function getRiskCardMenu($risks) { $menus = array(); $riskModel = $this->loadModel('risk'); foreach($risks as $risk) { $menu = array(); if(common::hasPriv('risk', 'edit') and $riskModel->isClickable($risk, 'edit')) $menu[] = array('label' => $this->lang->risk->edit, 'icon' => 'edit', 'url' => helper::createLink('risk', 'edit', "riskID=$risk->id"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('risk', 'track') and $riskModel->isClickable($risk, 'track')) $menu[] = array('label' => $this->lang->risk->track, 'icon' => 'checked', 'url' => helper::createLink('risk', 'track', "riskID=$risk->id"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('risk', 'activate') and $riskModel->isClickable($risk, 'activate')) $menu[] = array('label' => $this->lang->risk->activate, 'icon' => 'magic', 'url' => helper::createLink('risk', 'activate', "riskID=$risk->id"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('risk', 'hangup') and $riskModel->isClickable($risk, 'hangup')) $menu[] = array('label' => $this->lang->risk->hangup, 'icon' => 'pause', 'url' => helper::createLink('risk', 'hangup', "riskID=$risk->id"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('risk', 'cancel') and $riskModel->isClickable($risk, 'cancel')) $menu[] = array('label' => $this->lang->risk->cancel, 'icon' => 'ban-circle', 'url' => helper::createLink('risk', 'cancel', "riskID=$risk->id"), 'modal' => true, 'size' => 'lg'); if(common::hasPriv('risk', 'close') and $riskModel->isClickable($risk, 'close')) $menu[] = array('label' => $this->lang->risk->close, 'icon' => 'off', 'url' => helper::createLink('risk', 'close', "riskID=$risk->id"), 'modal' => true, 'size' => 'lg'); $menus[$risk->id] = $menu; } return $menus; } }