tao.php 53 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134
  1. <?php
  2. /**
  3. * The tao file of kanban module of ZenTaoPMS.
  4. *
  5. * @copyright Copyright 2009-2021 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.cnezsoft.com)
  6. * @license ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
  7. * @author Sun Guangming <sunguangming@easycorp.ltd>
  8. * @package kanban
  9. * @version $Id: model.php 5118 2021-10-22 10:18:41Z $
  10. * @link https://www.zentao.net
  11. */
  12. class kanbanTao extends kanbanModel
  13. {
  14. /*
  15. * 创建看板。
  16. * Create a kanban.
  17. *
  18. * @param object $kanban
  19. * @access public
  20. * @return int
  21. */
  22. protected function createKanban($kanban)
  23. {
  24. $this->dao->insert(TABLE_KANBAN)->data($kanban, 'copyRegion,copyKanbanID')
  25. ->autoCheck()
  26. ->batchCheck($this->config->kanban->create->requiredFields, 'notempty')
  27. ->checkIF(!$kanban->fluidBoard, 'colWidth', 'ge', $this->config->minColWidth)
  28. ->batchCheckIF($kanban->fluidBoard, 'minColWidth', 'ge', $this->config->minColWidth)
  29. ->checkIF($kanban->fluidBoard && $kanban->minColWidth >= $this->config->minColWidth, 'maxColWidth', 'gt', $kanban->minColWidth)
  30. ->check('name', 'unique', "space = {$kanban->space}")
  31. ->exec();
  32. }
  33. /**
  34. * 构造看板区域的数据结构。
  35. * Update a kanban.
  36. *
  37. * @param array $regionData
  38. * @param array $groups
  39. * @param array $laneGroup
  40. * @param array $columnGroup
  41. * @param array $cardGroup
  42. * @access public
  43. * @return array
  44. */
  45. protected function buildRegionData($regionData, $groups, $laneGroup, $columnGroup, $cardGroup)
  46. {
  47. $laneCount = 0;
  48. $groupData = array();
  49. foreach($groups as $group)
  50. {
  51. $lanes = zget($laneGroup, $group->id, array());
  52. if(!$lanes) continue;
  53. $cols = zget($columnGroup, $group->id, array());
  54. $items = zget($cardGroup, $group->id, array());
  55. /* 计算各个列上的卡片数量。 */
  56. $columnCount = array();
  57. $parentCols = array();
  58. foreach($cols as $col) $parentCols[$col['id']] = $col['parent'];
  59. foreach($items as $colGroup)
  60. {
  61. foreach($colGroup as $colID => $cards)
  62. {
  63. if(!isset($columnCount[$colID])) $columnCount[$colID] = 0;
  64. $columnCount[$colID] += count($cards);
  65. if(isset($parentCols[$colID]) && $parentCols[$colID] > 0)
  66. {
  67. if(!isset($columnCount[$parentCols[$colID]])) $columnCount[$parentCols[$colID]] = 0;
  68. $columnCount[$parentCols[$colID]] += count($cards);
  69. }
  70. }
  71. }
  72. foreach($cols as $colIndex => $col) $cols[$colIndex]['cards'] = isset($columnCount[$col['id']]) ? $columnCount[$col['id']] : 0;
  73. $laneCount += count($lanes);
  74. $groupData['id'] = $group->id;
  75. $groupData['key'] = "group{$group->id}";
  76. $groupData['data']['lanes'] = $lanes;
  77. $groupData['data']['cols'] = $cols;
  78. $groupData['data']['items'] = $items;
  79. $regionData['items'][] = $groupData;
  80. }
  81. $regionData['laneCount'] = $laneCount;
  82. return $regionData;
  83. }
  84. /**
  85. * 更新看板区域的排序。
  86. * Update sort of kanban region.
  87. *
  88. * @param array $regionIdList
  89. * @access public
  90. * @return void
  91. */
  92. protected function updateRegionSort($regionIdList)
  93. {
  94. $order = 1;
  95. foreach($regionIdList as $regionID)
  96. {
  97. $this->dao->update(TABLE_KANBANREGION)->set('`order`')->eq($order)->where('id')->eq($regionID)->exec();
  98. $order++;
  99. }
  100. }
  101. /**
  102. * 更新看板泳道的排序。
  103. * Update sort of kanban lane.
  104. *
  105. * @param int $regionID
  106. * @param array $lanes
  107. * @access public
  108. * @return void
  109. */
  110. protected function updateLaneSort($regionID, $lanes)
  111. {
  112. $order = 1;
  113. foreach($lanes as $laneID)
  114. {
  115. $this->dao->update(TABLE_KANBANLANE)->set('`order`')->eq($order)->where('id')->eq($laneID)->andWhere('region')->eq($regionID)->exec();
  116. $order++;
  117. }
  118. }
  119. /**
  120. * 更新看板列的排序。
  121. * Update sort of kanban lane.
  122. *
  123. * @param int $regionID
  124. * @param array $columns
  125. * @access public
  126. * @return void
  127. */
  128. protected function updateColumnSort($regionID, $columns)
  129. {
  130. $order = 1;
  131. foreach($columns as $columnID)
  132. {
  133. $this->dao->update(TABLE_KANBANCOLUMN)->set('`order`')->eq($order)->where('id')->eq($columnID)->andWhere('region')->eq($regionID)->exec();
  134. $order ++;
  135. }
  136. }
  137. /**
  138. * 为看板子列添加看板单元格。
  139. * Add kanban cell for child column.
  140. *
  141. * @param int $columnID
  142. * @param int $childColumnID
  143. * @param int $i
  144. * @access public
  145. * @return void
  146. */
  147. protected function addChildColumnCell($columnID, $childColumnID, $i = 0)
  148. {
  149. $cellList = $this->dao->select('*')->from(TABLE_KANBANCELL)->where('`column`')->eq($columnID)->fetchAll('', false);
  150. foreach($cellList as $cell)
  151. {
  152. $newCell = new stdclass();
  153. $newCell->kanban = $cell->kanban;
  154. $newCell->lane = $cell->lane;
  155. $newCell->column = $childColumnID;
  156. $newCell->type = 'common';
  157. $newCell->cards = $i == 0 ? $cell->cards : '';
  158. $this->dao->insert(TABLE_KANBANCELL)->data($newCell)->exec();
  159. $this->dao->update(TABLE_KANBANCELL)->set('cards')->eq('')->where('id')->eq($cell->id)->exec();
  160. }
  161. }
  162. /**
  163. * 更新看板列的父列。
  164. * Update parent column of kanban column.
  165. *
  166. * @param object $column
  167. * @access public
  168. * @return void
  169. */
  170. protected function updateColumnParent($column)
  171. {
  172. $children = $this->dao->select('COUNT(1) AS count')->from(TABLE_KANBANCOLUMN)
  173. ->where('parent')->eq($column->parent)
  174. ->andWhere('id')->ne($column->id)
  175. ->andWhere('deleted')->eq('0')
  176. ->andWhere('archived')->eq('0')
  177. ->fetch('count');
  178. if(!$children) $this->dao->update(TABLE_KANBANCOLUMN)->set('parent')->eq(0)->where('id')->eq($column->parent)->exec();
  179. }
  180. /**
  181. * 转入卡片时,更新卡片的指派人。
  182. * Update assignedTo of card when move in.
  183. *
  184. * @param int $cardID
  185. * @param string $oldAssignedToList
  186. * @param array $users
  187. * @access public
  188. * @return void
  189. */
  190. protected function updateCardAssignedTo($cardID, $oldAssignedToList, $users)
  191. {
  192. $assignedToList = explode(',', $oldAssignedToList);
  193. foreach($assignedToList as $index => $account)
  194. {
  195. if(!isset($users[$account])) unset($assignedToList[$index]);
  196. }
  197. $assignedToList = implode(',', $assignedToList);
  198. $assignedToList = trim($assignedToList, ',');
  199. if($oldAssignedToList != $assignedToList)
  200. {
  201. $this->dao->update(TABLE_KANBANCARD)->set('assignedTo')->eq($assignedToList)->where('id')->eq($cardID)->exec();
  202. }
  203. }
  204. /**
  205. * 获取可转入卡片的产品。
  206. * Get products can be imported.
  207. *
  208. * @param string $objectType
  209. * @access public
  210. * @return array
  211. */
  212. protected function getCanImportProducts($objectType = 'productplan')
  213. {
  214. $productPairs = $this->loadModel('product')->getPairs('', 0, '', 'all');
  215. $excludeProducts = array();
  216. if($objectType == 'productPlan')
  217. {
  218. $excludeProducts = $this->dao->select('t1.product')->from(TABLE_PROJECTPRODUCT)->alias('t1')
  219. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')
  220. ->where('t2.type')->eq('project')
  221. ->andWhere('t2.model')->ne('scrum')
  222. ->andWhere('t2.hasProduct')->eq('0')
  223. ->fetchPairs();
  224. }
  225. elseif($objectType == 'release')
  226. {
  227. $excludeProducts = $this->dao->select('t1.product')->from(TABLE_PROJECTPRODUCT)->alias('t1')
  228. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')
  229. ->where('t2.type')->eq('project')
  230. ->andWhere('t2.model')->eq('kanban')
  231. ->andWhere('t2.hasProduct')->eq('0')
  232. ->fetchPairs();
  233. }
  234. if(!empty($excludeProducts))
  235. {
  236. foreach($productPairs as $id)
  237. {
  238. if(isset($excludeProducts[$id])) unset($productPairs[$id]);
  239. }
  240. }
  241. return array($this->lang->kanban->allProducts) + $productPairs;
  242. }
  243. /**
  244. * 构造导入卡片的数据结构。
  245. * Build import card data structure.
  246. *
  247. * @param object $objectType
  248. * @param object $object
  249. * @param string $fromType
  250. * @param array $creators
  251. * @access public
  252. * @return object
  253. * @param object $objectCard
  254. */
  255. protected function buildObjectCard($objectCard, $object, $fromType, $creators)
  256. {
  257. if($fromType == 'productplan' or $fromType == 'release')
  258. {
  259. $objectCard->createdBy = zget($creators, $object->id, '');
  260. $objectCard->delay = helper::today() > $objectCard->end ? true : false;
  261. }
  262. if($fromType =='execution')
  263. {
  264. if($object->status != 'done' and $object->status != 'closed' and $object->status != 'suspended')
  265. {
  266. $delay = helper::diffDate(helper::today(), $object->end);
  267. if($delay > 0) $objectCard->delay = $delay;
  268. }
  269. $objectCard->execType = $object->type;
  270. $objectCard->progress = $object->progress;
  271. $parentExecutions = $this->dao->select('id,name')->from(TABLE_EXECUTION)->where('id')->in(trim($object->path, ','))->andWhere('type')->in('stage,kanban,sprint')->orderBy('grade')->fetchPairs();
  272. $objectCard->title = implode('/', $parentExecutions);
  273. $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');
  274. $objectCard->children = !empty($children) ? $children : 0;
  275. }
  276. $objectCard->originDesc = str_replace(array('<p>', '</p>'), "\n", $object->desc);
  277. $objectCard->originDesc = strip_tags($objectCard->originDesc);
  278. $objectCard->desc = strip_tags(htmlspecialchars_decode($object->desc));
  279. $objectCard->objectStatus = $objectCard->status;
  280. $objectCard->status = $objectCard->progress == 100 ? 'done' : 'doing';
  281. return $objectCard;
  282. }
  283. /**
  284. * 更新执行下看板的卡片。
  285. * Update card of execution.
  286. *
  287. * @param int $executionID
  288. * @param int $colID
  289. * @param int $laneID
  290. * @param string $cards
  291. * @access public
  292. * @return void
  293. */
  294. protected function updateExecutionCell($executionID, $colID, $laneID, $cards)
  295. {
  296. $this->dao->update(TABLE_KANBANCELL)->set('cards')->eq($cards)
  297. ->where('kanban')->eq($executionID)
  298. ->andWhere('lane')->eq($laneID)
  299. ->andWhere('`column`')->eq($colID)
  300. ->exec();
  301. }
  302. /**
  303. * 获取产品计划看板的分支
  304. * Get branches for product plan kanban.
  305. *
  306. * @param object $product
  307. * @param string $branchID
  308. * @access public
  309. * @return array
  310. */
  311. protected function getBranchesForPlanKanban($product, $branchID)
  312. {
  313. $this->loadModel('branch');
  314. $branches = array();
  315. if($product->type == 'normal')
  316. {
  317. $branches = array('all' => $this->lang->productplan->allAB);
  318. }
  319. elseif($branchID == 'all')
  320. {
  321. $branches = $this->branch->getPairs($product->id, 'active');
  322. }
  323. elseif($branchID == BRANCH_MAIN)
  324. {
  325. $branches = array(BRANCH_MAIN => $this->lang->branch->main);
  326. }
  327. elseif($branchID)
  328. {
  329. foreach(explode(',', $branchID) as $id)
  330. {
  331. $branchName = $this->branch->getById($id);
  332. $branches[$id] = $branchName;
  333. }
  334. }
  335. return $branches;
  336. }
  337. /**
  338. * 获取专业研发看板分组视图下的看板数据。
  339. * Get kanban data for group view of RD kanban.
  340. *
  341. * @param array $regionData
  342. * @param array $groups
  343. * @param array $laneGroup
  344. * @param array $columnGroup
  345. * @param array $cardGroup
  346. * @param string $searchValue
  347. * @access public
  348. * @return array
  349. */
  350. protected function buildRDRegionData($regionData, $groups, $laneGroup, $columnGroup, $cardGroup, $searchValue = '')
  351. {
  352. $laneCount = 0;
  353. $groupData = array();
  354. $fromKanbanID = '';
  355. foreach($groups as $group)
  356. {
  357. $lanes = zget($laneGroup, $group->id, array());
  358. if(!$lanes) continue;
  359. $kanbanID = "group{$group->id}";
  360. $cols = zget($columnGroup, $group->id, array());
  361. $items = zget($cardGroup, $group->id, array());
  362. if($searchValue != '' and empty($items)) continue;
  363. $lane = current($lanes);
  364. if($lane['type'] == 'parentStory')
  365. {
  366. $fromKanbanID = $kanbanID;
  367. }
  368. elseif($lane['type'] == 'story')
  369. {
  370. foreach($items as $colGroup)
  371. {
  372. foreach($colGroup as $cards)
  373. {
  374. foreach($cards as $card)
  375. {
  376. $regionData['links'][] = array('fromKanban' => $fromKanbanID, 'toKanban' => $kanbanID, 'from' => $card['parent'], 'to' => $card['id']);
  377. }
  378. }
  379. }
  380. }
  381. /* 计算各个列上的卡片数量。 */
  382. $columnCount = array();
  383. $parentCols = array();
  384. foreach($cols as $col) $parentCols[$col['id']] = $col['parent'];
  385. foreach($items as $colGroup)
  386. {
  387. foreach($colGroup as $colID => $cards)
  388. {
  389. if(!isset($columnCount[$colID])) $columnCount[$colID] = 0;
  390. $columnCount[$colID] += count($cards);
  391. if(isset($parentCols[$colID]) && $parentCols[$colID] > 0)
  392. {
  393. if(!isset($columnCount[$parentCols[$colID]])) $columnCount[$parentCols[$colID]] = 0;
  394. $columnCount[$parentCols[$colID]] += count($cards);
  395. }
  396. }
  397. }
  398. foreach($cols as $colIndex => $col)
  399. {
  400. $cols[$colIndex]['cards'] = isset($columnCount[$col['id']]) ? $columnCount[$col['id']] : 0;
  401. $cols[$colIndex]['laneType'] = $lane['type'];
  402. }
  403. $lanes = array_values($lanes);
  404. $laneCount += count($lanes);
  405. $groupData['id'] = $group->id;
  406. $groupData['key'] = $kanbanID;
  407. $groupData['data']['lanes'] = $lanes;
  408. $groupData['data']['cols'] = $cols;
  409. $groupData['data']['items'] = $items;
  410. $regionData['items'][] = $groupData;
  411. }
  412. $regionData['laneCount'] = $laneCount;
  413. return $regionData;
  414. }
  415. /**
  416. * 初始化卡片数据。
  417. * Init card data.
  418. *
  419. * @param object $card
  420. * @param object $cell
  421. * @param int $order
  422. * @param array $avatarPairs
  423. * @param array $users
  424. * @access public
  425. * @return array
  426. */
  427. protected function initCardItem($card, $cell, $order, $avatarPairs, $users)
  428. {
  429. $item = array();
  430. $item['column'] = $cell->column;
  431. $item['lane'] = $cell->lane;
  432. $item['title'] = !empty($card->title) ? htmlspecialchars_decode($card->title) : htmlspecialchars_decode($card->name);
  433. $item['id'] = $card->id;
  434. $item['name'] = $card->id;
  435. $item['pri'] = $card->pri;
  436. $item['color'] = $card->color;
  437. $item['assignedTo'] = $card->assignedTo;
  438. $item['isParent'] = !empty($card->isParent) ? $card->isParent: 0;
  439. $item['progress'] = !empty($card->progress) ? $card->progress : 0;
  440. $item['group'] = !empty($card->group) ? $card->group : '';
  441. $item['region'] = !empty($card->region) ? $card->region : '';
  442. $item['begin'] = !empty($card->begin) ? $card->begin : '';
  443. $item['end'] = !empty($card->end) ? $card->end : '';
  444. $item['fromID'] = !empty($card->fromID) ? $card->fromID : 0;
  445. $item['fromType'] = !empty($card->fromType) ? $card->fromType : '';
  446. $item['desc'] = !empty($card->desc) ? $card->desc : '';
  447. $item['originDesc'] = !empty($card->originDesc) ? $card->originDesc : '';
  448. $item['delay'] = !empty($card->delay) ? $card->delay : 0;
  449. $item['status'] = !empty($card->status) ? $card->status : '';
  450. $item['objectStatus'] = !empty($card->objectStatus) ? $card->objectStatus : '';
  451. $item['deleted'] = !empty($card->deleted) ? $card->deleted : 0;
  452. $item['date'] = !empty($card->date) ? $card->date : '';
  453. $item['estimate'] = !empty($card->estimate) ? $card->estimate : 0;
  454. $item['deadline'] = !empty($card->deadline) ? $card->deadline : '';
  455. $item['severity'] = !empty($card->severity) ? $card->severity : '';
  456. $item['cardType'] = $cell->type;
  457. $item['avatarList'] = array();
  458. $item['realnames'] = '';
  459. $item['order'] = $order;
  460. $item['parent'] = 0;
  461. if(!empty($card->originParent)) $item['parent'] = $card->originParent;
  462. if(!empty($card->rawParent)) $item['parent'] = $card->rawParent;
  463. if($card->assignedTo)
  464. {
  465. $assignedToList = explode(',', $card->assignedTo);
  466. foreach($assignedToList as $account)
  467. {
  468. if(!$account) continue;
  469. $maxTextLen = 2;
  470. $realname = zget($users, $account, '');
  471. $mbLength = mb_strlen($realname, 'utf-8');
  472. $strLength = strlen($realname);
  473. $displayText = '';
  474. if($realname)
  475. {
  476. if(isset($realname[0]) && $strLength === $mbLength)
  477. {
  478. /* Pure alphabet or numbers 纯英文情况 */
  479. $displayText = strtoupper($realname[0]);
  480. }
  481. else if($strLength % $mbLength == 0 && $strLength % 3 == 0)
  482. {
  483. /* Pure chinese characters 纯中文的情况 */
  484. $displayText = $mbLength <= $maxTextLen ? $realname : mb_substr($realname, $mbLength - $maxTextLen, $mbLength, 'utf-8');
  485. }
  486. else
  487. {
  488. /* Mix of Chinese and English 中英文混合的情况 */
  489. $displayText = $mbLength <= $maxTextLen ? $realname : mb_substr($realname, 0, $maxTextLen, 'utf-8');
  490. }
  491. }
  492. $userAvatar = zget($avatarPairs, $account, '');
  493. $userAvatar = $userAvatar ? "<img src='$userAvatar'/>" : $displayText;
  494. $item['avatarList'][] = $userAvatar;
  495. $item['realnames'] .= $realname . ' ';
  496. }
  497. }
  498. if($cell->type == 'task')
  499. {
  500. $item['left'] = $card->left;
  501. $item['estStarted'] = $card->estStarted;
  502. $item['mode'] = $card->mode;
  503. $item['execution'] = $card->execution;
  504. $item['story'] = $card->story;
  505. $item['module'] = $card->module;
  506. $dbPrivs = array();
  507. $actions = array('edit', 'restart', 'pause', 'recordworkhour', 'activate', 'delete', 'cancel', 'assignto');
  508. foreach($actions as $action)
  509. {
  510. $dbPrivs[$action] = common::hasDBPriv($card, 'task', $action);
  511. }
  512. $item['dbPrivs'] = $dbPrivs;
  513. $item['canSplit'] = $this->loadModel('task')->isClickable($card, 'batchCreate') && !$card->mode;
  514. }
  515. return $item;
  516. }
  517. /**
  518. * 处理看板中多人任务的指派人。
  519. * Process assignedTo of multi tasks.
  520. *
  521. * @param array $cardList
  522. * @access public
  523. * @return array
  524. */
  525. protected function appendTeamMember($cardList)
  526. {
  527. $multiTasks = array();
  528. foreach($cardList as $id => $task)
  529. {
  530. if($task->mode == 'multi') $multiTasks[$id] = $task;
  531. }
  532. $taskTeams = $this->dao->select('t1.account,t1.task,t2.realname')->from(TABLE_TASKTEAM)->alias('t1')
  533. ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account = t2.account')
  534. ->where('t1.task')->in(array_keys($multiTasks))
  535. ->orderBy('t1.order')
  536. ->fetchGroup('task', 'account');
  537. foreach($multiTasks as $taskID => $task)
  538. {
  539. $teamPairs = array();
  540. foreach($taskTeams[$taskID] as $account => $team) $teamPairs[$account] = $team->realname;
  541. $task->teamMember = $teamPairs;
  542. $cardList[$taskID] = $task;
  543. }
  544. return $cardList;
  545. }
  546. /**
  547. * 构造看板的分组视图。
  548. * Build group view of kanban.
  549. *
  550. * @param array $lanes
  551. * @param array $columns
  552. * @param array $cardGroup
  553. * @param string $searchValue
  554. * @param string $groupBy
  555. * @param string $browseType
  556. * @param array $menus
  557. * @access public
  558. * @return array
  559. */
  560. protected function buildGroupKanban($lanes, $columns, $cardGroup, $searchValue, $groupBy, $browseType, $menus)
  561. {
  562. $columnList = array();
  563. $laneList = array();
  564. $cardList = array();
  565. $avatarPairs = $this->loadModel('user')->getAvatarPairs();
  566. $users = $this->loadModel('user')->getPairs('noletter');
  567. $module = $browseType == 'parentStory' ? 'story' : $browseType;
  568. $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;
  569. foreach($lanes as $laneID => $lane)
  570. {
  571. $laneData = array();
  572. $laneData['id'] = $groupBy . $laneID;
  573. $laneData['type'] = $browseType;
  574. $laneData['name'] = $laneData['id'];
  575. $laneData['region'] = $regionID ? $regionID : $lane->execution;
  576. $laneData['title'] = (($groupBy == 'pri' or $groupBy == 'severity') and $laneID) ? $this->lang->$module->$groupBy . ':' . $lane->name : $lane->name;
  577. $laneData['color'] = $lane->color;
  578. $laneData['order'] = $lane->order;
  579. if(empty($laneID) and !in_array($groupBy, array('module', 'story', 'pri', 'severity'))) $laneID = '';
  580. /* Construct kanban column data. */
  581. foreach($columns as $column)
  582. {
  583. $cardIdList = array_unique(array_filter(explode(',', $column->cards)));
  584. $columnData = $this->buildGroupColumn($columnList, $column, $laneData, $browseType);
  585. list($cardCount, $cardData) = $this->buildGroupCard($cardGroup, $cardIdList, $column, (string)$laneID, $groupBy, $browseType, $searchValue, $avatarPairs, $users, $menus);
  586. $columnData['cards'] += $cardCount;
  587. $cardList[$laneData['id']][$column->column] = $cardData;
  588. $columnList[$column->column] = $columnData;
  589. }
  590. $laneList[] = $laneData;
  591. }
  592. foreach($columnList as $column)
  593. {
  594. if(isset($column['parentName']) && isset($columnList[$column['parentName']])) $columnList[$column['parentName']]['cards'] += $column['cards'];
  595. }
  596. return array($laneList, $columnList, $cardList);
  597. }
  598. /**
  599. * 构造看板分组视图的列。
  600. * Build column of group view.
  601. *
  602. * @param array $columnsL
  603. * @param object $column
  604. * @param array $laneData
  605. * @param string $browseType
  606. * @access public
  607. * @return array
  608. * @param mixed[] $columnList
  609. */
  610. protected function buildGroupColumn($columnList, $column, $laneData, $browseType)
  611. {
  612. if(!isset($columnList[$column->column]))
  613. {
  614. $columnData = array();
  615. $columnData['id'] = $column->column;
  616. $columnData['type'] = $column->columnType;
  617. $columnData['name'] = $column->column;
  618. $columnData['title'] = $column->columnName;
  619. $columnData['color'] = $column->color;
  620. $columnData['limit'] = $column->limit;
  621. $columnData['region'] = $laneData['region'];
  622. $columnData['laneName'] = $column->lane;
  623. $columnData['group'] = $browseType;
  624. $columnData['cards'] = 0;
  625. $columnData['actionList'] = array('setColumn', 'setWIP');
  626. if($column->parent > 0) $columnData['parentName'] = $column->parent;
  627. }
  628. else
  629. {
  630. $columnData = $columnList[$column->column];
  631. }
  632. return $columnData;
  633. }
  634. /**
  635. * 构造看板分组视图的卡片。
  636. * Build card of group view.
  637. *
  638. * @param array $cardGroup
  639. * @param array $cardIdList
  640. * @param object $column
  641. * @param string $laneID
  642. * @param string $groupBy
  643. * @param string $browseType
  644. * @param string $searchValue
  645. * @param array $avatarPairs
  646. * @param array $users
  647. * @param array $menus
  648. * @access public
  649. * @return array
  650. */
  651. protected function buildGroupCard($cardGroup, $cardIdList, $column, $laneID, $groupBy, $browseType, $searchValue, $avatarPairs, $users, $menus)
  652. {
  653. $cardCount = 0;
  654. $cardList = array();
  655. $objects = zget($cardGroup, $column->columnType, array());
  656. foreach($cardIdList as $cardID)
  657. {
  658. $object = zget($objects, $cardID, array());
  659. if(empty($object)) continue;
  660. if(in_array($groupBy, array('module', 'story', 'pri', 'severity')) and $object->$groupBy != $laneID) continue;
  661. if(in_array($groupBy, array('type', 'category', 'source')) and $object->$groupBy != $laneID) continue;
  662. if($groupBy == 'assignedTo')
  663. {
  664. $laneID = (string)$laneID;
  665. if(empty($object->$groupBy)) $object->$groupBy = '';
  666. if(empty($object->teamMember) and $object->$groupBy != $laneID) continue;
  667. if(!empty($object->teamMember) and !in_array($laneID, array_keys($object->teamMember), true)) continue;
  668. }
  669. $cardData = $this->buildExecutionCard($object, $column, $browseType, $searchValue, $menus);
  670. if(empty($cardData)) continue;
  671. $cardData['cardType'] = $browseType;
  672. if($groupBy == 'assignedTo' && $object->$groupBy !== $laneID) $cardData['assignedTo'] = $laneID;
  673. if($cardData['assignedTo'])
  674. {
  675. $userAvatar = zget($avatarPairs, $cardData['assignedTo'], '');
  676. $userAvatar = $userAvatar ? "<img src='$userAvatar'/>" : strtoupper(mb_substr($cardData['assignedTo'], 0, 1, 'utf-8'));
  677. $cardData['avatarList'][] = $userAvatar;
  678. $cardData['realnames'] = zget($users, $cardData['assignedTo'], '');
  679. }
  680. $cardList[] = $cardData;
  681. $cardCount ++;
  682. }
  683. return array($cardCount, $cardList);
  684. }
  685. /**
  686. * 获取看板分组视图的对象键值对。
  687. * Get object pairs for group view.
  688. *
  689. * @param string $groupBy
  690. * @param array $groupByList
  691. * @param string $browseType
  692. * @param string $orderBy
  693. * @access public
  694. * @return array
  695. */
  696. protected function getObjectPairs($groupBy, $groupByList, $browseType, $orderBy)
  697. {
  698. if($browseType == 'parentStory') $browseType = 'story';
  699. $objectPairs = array();
  700. if(in_array($groupBy, array('module', 'story', 'assignedTo')))
  701. {
  702. if($groupBy == 'module')
  703. {
  704. $objectPairs += $this->dao->select('id,name')->from(TABLE_MODULE)->where('type')->in('story,task,bug')->andWhere('deleted')->eq('0')->andWhere('id')->in($groupByList)->fetchPairs();
  705. }
  706. elseif($groupBy == 'story')
  707. {
  708. $objectPairs += $this->dao->select('id,title')->from(TABLE_STORY)->where('deleted')->eq(0)->andWhere('id')->in($groupByList)->orderBy($orderBy)->fetchPairs();
  709. }
  710. else
  711. {
  712. $objectPairs += $this->dao->select('account,realname')->from(TABLE_USER)->where('account')->in($groupByList)->fetchPairs();
  713. if(isset($groupByList['closed'])) $objectPairs['closed'] = 'Closed';
  714. }
  715. }
  716. else
  717. {
  718. unset($this->lang->$browseType->{$groupBy . 'List'}[0]);
  719. unset($this->lang->$browseType->{$groupBy . 'List'}['']);
  720. $objectPairs += $this->lang->$browseType->{$groupBy . 'List'};
  721. }
  722. if(in_array($groupBy, array('module', 'story', 'pri', 'severity'))) $objectPairs[0] = $this->lang->$browseType->$groupBy . ': ' . $this->lang->kanban->noGroup;
  723. if(in_array($groupBy, array('assignedTo', 'source'))) $objectPairs[] = $this->lang->$browseType->$groupBy . ': ' . $this->lang->kanban->noGroup;
  724. if($browseType == 'bug' and $groupBy == 'type') $objectPairs[0] = $this->lang->$browseType->$groupBy . ': ' . $this->lang->kanban->noGroup;
  725. return $objectPairs;
  726. }
  727. /**
  728. * 更新专业研发看板中的用需、业需、父需求泳道上的卡片。
  729. * Update card of feature, epic, parent story lane in RD kanban.
  730. *
  731. * @param array $cardPairs
  732. * @param int $executionID
  733. * @param string $otherCardList
  734. * @param string $laneType
  735. * @access public
  736. * @return array
  737. */
  738. protected function refreshERURCards($cardPairs, $executionID, $otherCardList, $laneType = 'story')
  739. {
  740. $storyType = $laneType == 'parentStory' ? 'story' : $laneType;
  741. $stories = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory', '0', $storyType, $otherCardList);
  742. foreach($stories as $storyID => $story)
  743. {
  744. if($laneType == 'parentStory' && $story->isParent != '1') continue;
  745. foreach($this->lang->kanban->ERURColumn as $stage => $langItem)
  746. {
  747. if($story->stage != $stage and strpos((string)$cardPairs[$stage], ",$storyID,") !== false)
  748. {
  749. $cardPairs[$stage] = str_replace(",$storyID,", ',', $cardPairs[$stage]);
  750. }
  751. if($story->stage == $stage and strpos((string)$cardPairs[$stage], ",$storyID,") === false)
  752. {
  753. $cardPairs[$stage] = empty($cardPairs[$stage]) ? ",$storyID," : ",$storyID" . $cardPairs[$stage];
  754. }
  755. }
  756. }
  757. return $cardPairs;
  758. }
  759. /**
  760. * 更新专业研发看板中的需求泳道上的卡片。
  761. * Update card of story lane in RD kanban.
  762. *
  763. * @param array $cardPairs
  764. * @param int $executionID
  765. * @param string $otherCardList
  766. * @access public
  767. * @return array
  768. */
  769. protected function refreshStoryCards($cardPairs, $executionID, $otherCardList)
  770. {
  771. $stories = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory', '0', 'story', $otherCardList);
  772. foreach($stories as $storyID => $story)
  773. {
  774. foreach($this->config->kanban->storyColumnStageList as $colType => $stage)
  775. {
  776. if(!isset($cardPairs[$colType])) continue;
  777. if($story->stage != $stage and strpos($cardPairs[$colType], ",$storyID,") !== false)
  778. {
  779. $cardPairs[$colType] = str_replace(",$storyID,", ',', $cardPairs[$colType]);
  780. }
  781. if(strpos(',ready,backlog,design,develop,test,', $colType) !== false) continue;
  782. if($story->stage == $stage and strpos($cardPairs[$colType], ",$storyID,") === false)
  783. {
  784. $cardPairs[$colType] = empty($cardPairs[$colType]) ? ",$storyID," : ",$storyID" . $cardPairs[$colType];
  785. }
  786. }
  787. if(strpos('wait,projected', $story->stage) !== false and strpos($cardPairs['ready'], ",$storyID,") === false and strpos($cardPairs['backlog'], ",$storyID,") === false)
  788. {
  789. $cardPairs['backlog'] = empty($cardPairs['backlog']) ? ",$storyID," : ",$storyID" . $cardPairs['backlog'];
  790. }
  791. }
  792. return $cardPairs;
  793. }
  794. /**
  795. * 更新专业研发看板中的Bug泳道上的卡片。
  796. * Update card of bug lane in RD kanban.
  797. *
  798. * @param array $cardPairs
  799. * @param int $executionID
  800. * @param string $otherCardList
  801. * @access public
  802. * @return array
  803. */
  804. protected function refreshBugCards($cardPairs, $executionID, $otherCardList)
  805. {
  806. $bugs = $this->loadModel('bug')->getExecutionBugs($executionID, 0, 'all', '0', '', 0, 'id_desc', $otherCardList);
  807. foreach($bugs as $bugID => $bug)
  808. {
  809. foreach($this->config->kanban->bugColumnStatusList as $colType => $status)
  810. {
  811. if($bug->status != $status and strpos($cardPairs[$colType], ",$bugID,") !== false)
  812. {
  813. $cardPairs[$colType] = str_replace(",$bugID,", ',', $cardPairs[$colType]);
  814. }
  815. if(strpos(',resolving,test,testing,tested,', $colType) !== false) continue;
  816. 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)
  817. {
  818. $cardPairs['unconfirmed'] = empty($cardPairs['unconfirmed']) ? ",$bugID," : ",$bugID" . $cardPairs['unconfirmed'];
  819. if(strpos($cardPairs['closed'], ",$bugID,") !== false) $cardPairs['closed'] = str_replace(",$bugID,", ',', $cardPairs['closed']);
  820. }
  821. 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)
  822. {
  823. $cardPairs['confirmed'] = empty($cardPairs['confirmed']) ? ",$bugID," : ",$bugID" . $cardPairs['confirmed'];
  824. if(strpos($cardPairs['unconfirmed'], ",$bugID,") !== false) $cardPairs['unconfirmed'] = str_replace(",$bugID,", ',', $cardPairs['unconfirmed']);
  825. }
  826. elseif($colType == 'fixing' and $bug->status == $status and $bug->activatedCount > 0 and strpos($cardPairs['fixing'], ",$bugID,") === false)
  827. {
  828. $cardPairs['fixing'] = empty($cardPairs['fixing']) ? ",$bugID," : ",$bugID" . $cardPairs['fixing'];
  829. if(strpos($cardPairs['confirmed'], ",$bugID,") !== false) $cardPairs['confirmed'] = str_replace(",$bugID,", ',', $cardPairs['confirmed']);
  830. if(strpos($cardPairs['unconfirmed'], ",$bugID,") !== false) $cardPairs['unconfirmed'] = str_replace(",$bugID,", ',', $cardPairs['unconfirmed']);
  831. }
  832. 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)
  833. {
  834. $cardPairs['fixed'] = empty($cardPairs['fixed']) ? ",$bugID," : ",$bugID" . $cardPairs['fixed'];
  835. if(strpos($cardPairs['testing'], ",$bugID,") !== false) $cardPairs['testing'] = str_replace(",$bugID,", ',', $cardPairs['testing']);
  836. if(strpos($cardPairs['tested'], ",$bugID,") !== false) $cardPairs['tested'] = str_replace(",$bugID,", ',', $cardPairs['tested']);
  837. }
  838. elseif($colType == 'closed' and $bug->status == 'closed' and strpos($cardPairs[$colType], ",$bugID,") === false)
  839. {
  840. $cardPairs[$colType] = empty($cardPairs[$colType]) ? ",$bugID," : ",$bugID". $cardPairs[$colType];
  841. }
  842. }
  843. }
  844. return $cardPairs;
  845. }
  846. /**
  847. * 更新专业研发看板中的任务泳道上的卡片。
  848. * Update card of bug lane in RD kanban.
  849. *
  850. * @param array $cardPairs
  851. * @param int $executionID
  852. * @param string $otherCardList
  853. * @access public
  854. * @return array
  855. */
  856. protected function refreshTaskCards($cardPairs, $executionID, $otherCardList)
  857. {
  858. $tasks = $this->loadModel('execution')->getKanbanTasks($executionID, 'status_asc, id_desc', explode(',', $otherCardList));
  859. foreach($tasks as $taskID => $task)
  860. {
  861. $task->status = $task->status == 'changed' ? $task->rawStatus : $task->status;
  862. foreach($this->config->kanban->taskColumnStatusList as $colType => $status)
  863. {
  864. if($colType == 'develop') continue;
  865. if(!isset($cardPairs[$colType])) continue;
  866. if($task->status == $status and strpos($cardPairs[$colType], ",$taskID,") === false)
  867. {
  868. $cardPairs[$colType] = empty($cardPairs[$colType]) ? ",$taskID," : ",$taskID". $cardPairs[$colType];
  869. }
  870. elseif($task->status != $status and strpos($cardPairs[$colType], ",$taskID,") !== false)
  871. {
  872. $cardPairs[$colType] = str_replace(",$taskID,", ',', $cardPairs[$colType]);
  873. }
  874. }
  875. }
  876. return $cardPairs;
  877. }
  878. /**
  879. * 获取父需求类型的卡片操作菜单。
  880. * Get menu of story card.
  881. *
  882. * @param int $executionID
  883. * @param array $objects
  884. * @access public
  885. * @return array
  886. */
  887. protected function getERURCardMenu($executionID, $objects)
  888. {
  889. $execution = $this->loadModel('execution')->getByID($executionID);
  890. $menus = array();
  891. $objects = $this->loadModel('story')->mergeReviewer($objects);
  892. foreach($objects as $story)
  893. {
  894. $menu = array();
  895. 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');
  896. 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');
  897. 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');
  898. 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');
  899. 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"));
  900. 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"));
  901. $menus[$story->id] = $menu;
  902. }
  903. return $menus;
  904. }
  905. /**
  906. * 获取需求类型的卡片操作菜单。
  907. * Get menu of story card.
  908. *
  909. * @param object $execution
  910. * @param array $objects
  911. * @access public
  912. * @return array
  913. */
  914. protected function getStoryCardMenu($execution, $objects)
  915. {
  916. $menus = array();
  917. $objects = $this->loadModel('story')->mergeReviewer($objects);
  918. foreach($objects as $story)
  919. {
  920. $menu = array();
  921. $toTaskPriv = strpos('draft,reviewing,closed', $story->status) !== false ? false : true;
  922. 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');
  923. 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');
  924. 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');
  925. 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');
  926. 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');
  927. 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');
  928. 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"));
  929. 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"));
  930. $menus[$story->id] = $menu;
  931. }
  932. return $menus;
  933. }
  934. /**
  935. * 获取Bug类型的卡片操作菜单。
  936. * Get menu of bug card.
  937. *
  938. * @param array $objects
  939. * @access public
  940. * @return array
  941. */
  942. protected function getBugCardMenu($objects)
  943. {
  944. $menus = array();
  945. $this->loadModel('bug');
  946. foreach($objects as $bug)
  947. {
  948. $menu = array();
  949. 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');
  950. 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');
  951. 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');
  952. 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');
  953. 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');
  954. 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');
  955. 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');
  956. 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);
  957. $menus[$bug->id] = $menu;
  958. }
  959. return $menus;
  960. }
  961. /**
  962. * 获取任务类型的卡片操作菜单。
  963. * Get menu of bug card.
  964. *
  965. * @param array $objects
  966. * @param int $executionID
  967. * @access public
  968. * @return array
  969. */
  970. protected function getTaskCardMenu($objects, $executionID)
  971. {
  972. $menus = array();
  973. $canStartExecution = true;
  974. if($this->config->edition == 'ipd')
  975. {
  976. $executionStatus = $this->loadModel('execution')->checkStageStatus($executionID, 'start');
  977. $canStartExecution = empty($executionStatus['disabled']);
  978. }
  979. $this->loadModel('task');
  980. foreach($objects as $task)
  981. {
  982. $menu = array();
  983. 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');
  984. 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');
  985. 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');
  986. 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');
  987. 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');
  988. 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');
  989. 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');
  990. 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');
  991. 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');
  992. 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');
  993. 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);
  994. $menus[$task->id] = $menu;
  995. }
  996. return $menus;
  997. }
  998. /**
  999. * 获取风险看板操作菜单。
  1000. * Get the Kanban risk menu.
  1001. *
  1002. * @param int $executionID
  1003. * @access public
  1004. * @return array
  1005. */
  1006. public function getRiskCardMenu($risks)
  1007. {
  1008. $menus = array();
  1009. $riskModel = $this->loadModel('risk');
  1010. foreach($risks as $risk)
  1011. {
  1012. $menu = array();
  1013. 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');
  1014. 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');
  1015. 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');
  1016. 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');
  1017. 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');
  1018. 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');
  1019. $menus[$risk->id] = $menu;
  1020. }
  1021. return $menus;
  1022. }
  1023. }