zen.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. <?php
  2. /**
  3. * The zen file of program module of ZenTaoPMS.
  4. *
  5. * @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
  6. * @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
  7. * @author dingguodong <dingguodong@easycorp.ltd>
  8. * @link https://www.zentao.net
  9. */
  10. class programZen extends program
  11. {
  12. /**
  13. * 追加额外的数据到提交的表单数据。
  14. * Append extras data to post data.
  15. *
  16. * @param object $postData
  17. * @access protected
  18. * @return object
  19. */
  20. protected function prepareStartExtras($postData)
  21. {
  22. return $postData->add('status', 'doing')
  23. ->add('lastEditedBy', $this->app->user->account)
  24. ->add('lastEditedDate', helper::now())
  25. ->get();
  26. }
  27. /**
  28. * 获取创建项目集的数据。
  29. * Build program data for create.
  30. *
  31. * @access protected
  32. * @return object
  33. */
  34. protected function buildProgramForCreate()
  35. {
  36. $fields = $this->config->program->form->create;
  37. if($this->post->longTime) $fields['end']['skipRequired'] = true;
  38. foreach(explode(',', trim($this->config->program->create->requiredFields, ',')) as $field)
  39. {
  40. if($field == 'end') continue;
  41. $fields[$field]['required'] = true;
  42. }
  43. $this->app->loadConfig('project');
  44. $program = form::data($fields)
  45. ->setDefault('openedBy', $this->app->user->account)
  46. ->setDefault('openedDate', helper::now())
  47. ->setDefault('code', '')
  48. ->setIF($this->post->longTime, 'end', LONG_TIME)
  49. ->setIF($this->post->acl == 'open', 'whitelist', '')
  50. ->add('type', 'program')
  51. ->join('whitelist', ',')
  52. ->get();
  53. return $this->loadModel('file')->processImgURL($program, $this->config->program->editor->create['id'], $this->post->uid);
  54. }
  55. /**
  56. * 获取编辑项目集的数据。
  57. * Build program for edit.
  58. *
  59. * @param int $programID
  60. * @access protected
  61. * @return object
  62. */
  63. protected function buildProgramForEdit($programID)
  64. {
  65. $oldProgram = $this->program->fetchByID($programID);
  66. $fields = $this->config->program->form->edit;
  67. if($this->post->longTime) $fields['end']['skipRequired'] = true;
  68. foreach(explode(',', trim($this->config->program->edit->requiredFields, ',')) as $field)
  69. {
  70. if($field == 'end') continue;
  71. $fields[$field]['required'] = true;
  72. }
  73. $this->app->loadConfig('project');
  74. $program = form::data($fields, $programID)
  75. ->setDefault('id', $programID)
  76. ->setDefault('lastEditedBy', $this->app->user->account)
  77. ->setDefault('lastEditedDate', helper::now())
  78. ->setIF(helper::isZeroDate($this->post->begin), 'begin', '')
  79. ->setIF(helper::isZeroDate($this->post->end), 'end', '')
  80. ->setIF($this->post->realBegan != '' and $oldProgram->status == 'wait', 'status', 'doing')
  81. ->setIF(!isset($_POST['budgetUnit']), 'budgetUnit', $oldProgram->budgetUnit)
  82. ->setIF($this->post->longTime, 'end', LONG_TIME)
  83. ->setIF($this->post->acl == 'open', 'whitelist', '')
  84. ->join('whitelist', ',')
  85. ->get();
  86. return $this->loadModel('file')->processImgURL($program, $this->config->program->editor->edit['id'], $this->post->uid);
  87. }
  88. /**
  89. * @param mixed[] $parents
  90. * @param int $programID
  91. */
  92. protected function removeSubjectToCurrent($parents, $programID)
  93. {
  94. $children = $this->dao->select('*')->from(TABLE_PROGRAM)->where('path')->like("%,$programID,%")->fetchPairs('id', 'id');
  95. foreach($children as $childID) unset($parents[$childID]);
  96. return $parents;
  97. }
  98. /**
  99. * 根据条件获取项目集。
  100. * Get programs by type.
  101. *
  102. * @param string $status
  103. * @param string $orderBy
  104. * @param int $param
  105. * @param object|null $pager
  106. * @access protected
  107. * @return array
  108. */
  109. protected function getProgramsByType($status, $orderBy, $param = 0, $pager = null)
  110. {
  111. $status = strtolower($status);
  112. $this->view->summary = '';
  113. if(strpos($orderBy, 'order') !== false) $orderBy = "grade,{$orderBy}";
  114. if(strtolower($status) == 'bysearch')
  115. {
  116. $programs = $this->program->getListBySearch($orderBy, $param, false, $pager);
  117. }
  118. else
  119. {
  120. /* Get top programs and projects. */
  121. $topObjects = $this->program->getList($status == 'unclosed' ? 'doing,suspended,wait' : $status, $orderBy, 'top', array(), $pager);
  122. if(!$topObjects) $topObjects = array(0);
  123. $programs = $this->program->getList($status, $orderBy, 'child', array_keys($topObjects));
  124. }
  125. /* Get summary. */
  126. $topCount = $indCount = 0;
  127. foreach($programs as $program)
  128. {
  129. if($program->type == 'program' and $program->parent == 0) $topCount ++;
  130. if($program->type == 'project' and $program->parent == 0) $indCount ++;
  131. }
  132. $this->view->summary = sprintf($this->lang->program->summary, $topCount, $indCount);
  133. return $programs;
  134. }
  135. /**
  136. * 根据项目集,获取产品经理列表。
  137. * Get PM list by programs.
  138. *
  139. * @param array $programs
  140. * @access protected
  141. * @return array
  142. */
  143. protected function getPMListByPrograms($programs)
  144. {
  145. $accounts = array();
  146. $hasProject = false;
  147. foreach($programs as $program)
  148. {
  149. if(!empty($program->PM) and !in_array($program->PM, $accounts)) $accounts[] = $program->PM;
  150. if($hasProject === false and $program->type != 'program') $hasProject = true;
  151. }
  152. $this->view->hasProject = $hasProject;
  153. return $this->loadModel('user')->getListByAccounts($accounts, 'account');
  154. }
  155. /**
  156. * 获取项目集看板。
  157. * Get kanban list for product.
  158. *
  159. * @param string $browseType
  160. * @access protected
  161. * @return array
  162. */
  163. protected function getKanbanList($browseType = 'my')
  164. {
  165. $kanbanList = array();
  166. $programPairs = array(0 => $this->lang->project->noProgram) + $this->program->getPairs(true, 'order_asc');
  167. $programList = $this->getProgramList4Kanban($browseType);
  168. foreach($programList as $programID => $productList)
  169. {
  170. $region = array();
  171. $heading = new stdclass();
  172. $heading->title = zget($programPairs, $programID, $programID);
  173. $region['key'] = $programID;
  174. $region['id'] = $programID;
  175. $region['heading'] = $heading;
  176. $lanes = array();
  177. $items = array();
  178. $columnCards = array();
  179. foreach($productList as $laneKey => $laneData)
  180. {
  181. $lanes[] = array('name' => $laneKey, 'title' => $laneData->name);
  182. $columns = array();
  183. foreach(array('unexpiredPlans', 'waitingProjects', 'doing', 'doingProjects', 'doingExecutions', 'normalReleases') as $columnKey)
  184. {
  185. $colTitle = $columnKey == 'doing' ? $this->lang->program->statusList['doing'] : $this->lang->program->kanban->{$columnKey};
  186. $column = array('name' => $columnKey, 'title' => $colTitle);
  187. if($columnKey == 'doingExecutions' || $columnKey == 'doingProjects') $column['parentName'] = 'doing';
  188. $columns[] = $column;
  189. $cardList = !empty($laneData->{$columnKey}) ? $laneData->{$columnKey} : array();
  190. foreach($cardList as $card)
  191. {
  192. $items[$laneKey][$columnKey][] = array('id' => $card->id, 'name' => $card->id, 'title' => isset($card->name) ? $card->name : $card->title, 'status' => isset($card->status) ? $card->status : '', 'cardType' => $columnKey, 'delay' => !empty($card->delay) ? $card->delay : 0, 'progress' => isset($card->progress) ? $card->progress : 0, 'marker' => isset($card->marker) ? $card->marker : 0);
  193. if(!isset($columnCards[$columnKey])) $columnCards[$columnKey] = 0;
  194. $columnCards[$columnKey] ++;
  195. if($columnKey == 'doingProjects')
  196. {
  197. if(!empty($latestExecutions[$card->id]))
  198. {
  199. $execution = $latestExecutions[$card->id];
  200. $items[$laneKey]['doingExecutions'][] = array('id' => $execution->id, 'name' => $execution->id, 'title' => $execution->name, 'status' => $execution->status, 'cardType' => 'doingExecution', 'delay' => !empty($execution->delay) ? $execution->delay : 0, 'progress' => $execution->progress);
  201. if(!isset($columnCards['doingExecutions'])) $columnCards['doingExecutions'] = 0;
  202. $columnCards['doingExecutions'] ++;
  203. }
  204. }
  205. }
  206. }
  207. }
  208. foreach($columns as $key => $column) $columns[$key]['cards'] = !empty($columnCards[$column['name']]) ? $columnCards[$column['name']] : 0;
  209. $groupData['key'] = $programID;
  210. $groupData['data']['lanes'] = $lanes;
  211. $groupData['data']['cols'] = $columns;
  212. $groupData['data']['items'] = $items;
  213. $region['items'] = array($groupData);
  214. $kanbanList[] = $region;
  215. }
  216. return $kanbanList;
  217. }
  218. /**
  219. * 获取项目集看板的数据。
  220. * Get kanban list data for product.
  221. *
  222. * @param string $browseType
  223. * @access protected
  224. * @return array
  225. */
  226. protected function getProgramList4Kanban($browseType = 'my')
  227. {
  228. $programs = $this->program->getTopPairs('noclosed');
  229. $involvedPrograms = $this->program->getInvolvedPrograms($this->app->user->account);
  230. foreach($programs as $programID => $programName)
  231. {
  232. if($browseType == 'my')
  233. {
  234. if(!in_array($programID, $involvedPrograms)) unset($programs[$programID]);
  235. }
  236. else
  237. {
  238. if(in_array($programID, $involvedPrograms)) unset($programs[$programID]);
  239. }
  240. }
  241. list($productGroup, $planGroup, $releaseGroup, $projectGroup, $doingExecutions) = $this->program->getKanbanStatisticData(array(0 => '') + $programs);
  242. $programList = array();
  243. $today = helper::today();
  244. foreach($productGroup as $programID => $productList)
  245. {
  246. foreach($productList as $product)
  247. {
  248. $product->unexpiredPlans = isset($planGroup[$product->id]) ? $planGroup[$product->id] : array();
  249. if(isset($projectGroup[$product->id]))
  250. {
  251. foreach($projectGroup[$product->id] as $project)
  252. {
  253. if(helper::diffDate($today, $project->end) > 0) $project->delay = helper::diffDate($today, $project->end);
  254. if($project->status == 'wait')
  255. {
  256. $product->waitingProjects[$project->id] = $project;
  257. }
  258. elseif($project->status == 'doing')
  259. {
  260. $product->doingProjects[$project->id] = $project;
  261. if(isset($doingExecutions[$project->id]))
  262. {
  263. $doingExecution = $doingExecutions[$project->id];
  264. if(helper::diffDate($today, $doingExecution->end) > 0) $doingExecution->delay = helper::diffDate($today, $doingExecution->end);
  265. $product->doingExecutions[$doingExecution->id] = $doingExecution;
  266. }
  267. }
  268. }
  269. }
  270. $product->normalReleases = isset($releaseGroup[$product->id]) ? $releaseGroup[$product->id] : array();
  271. $programList[$programID][$product->id] = $product;
  272. }
  273. }
  274. return $programList;
  275. }
  276. /**
  277. * 构造1.5级导航的数据。
  278. * Build the data of 1.5 level navigation.
  279. *
  280. * 当列表中缺少上级项目集时,将仍挂在该上级下的子项目集作为根节点展示,避免仅含子项目集时树为空。
  281. * When parent programs are missing from the list, child programs whose parent is absent are shown as roots.
  282. *
  283. * @param array $programs
  284. * @param int $parentID
  285. * @access protected
  286. * @return array
  287. */
  288. protected function buildTree($programs, $parentID = 0)
  289. {
  290. $programList = array();
  291. foreach($programs as $program)
  292. {
  293. if($program->type != 'program') continue;
  294. $programList[] = $program;
  295. }
  296. if(empty($programList)) return array();
  297. $ids = array();
  298. foreach($programList as $program) $ids[$program->id] = true;
  299. $names = array_column($programList, 'name');
  300. $pinyinMap = common::convert2Pinyin($names);
  301. return $this->buildTreeNodes($programList, $parentID, $ids, $pinyinMap);
  302. }
  303. /**
  304. * 递归构造项目集树节点。
  305. * Build program tree nodes recursively.
  306. *
  307. * @param array $programs
  308. * @param int $parentID
  309. * @param array $ids program id => true
  310. * @param array $pinyinMap name => pinyin (from batch convert2Pinyin)
  311. * @access protected
  312. * @return array
  313. */
  314. protected function buildTreeNodes($programs, $parentID, $ids, $pinyinMap)
  315. {
  316. $result = array();
  317. foreach($programs as $program)
  318. {
  319. if($parentID === 0)
  320. {
  321. $match = ($program->parent == $parentID) || !isset($ids[$program->parent]);
  322. }
  323. else
  324. {
  325. $match = ($program->parent == $parentID);
  326. }
  327. if(!$match) continue;
  328. $itemArray = array
  329. (
  330. 'id' => $program->id,
  331. 'text' => $program->name,
  332. 'label' => false,
  333. 'keys' => zget($pinyinMap, $program->name, ''),
  334. 'items' => $this->buildTreeNodes($programs, $program->id, $ids, $pinyinMap)
  335. );
  336. if(empty($itemArray['items'])) unset($itemArray['items']);
  337. $result[] = $itemArray;
  338. }
  339. return $result;
  340. }
  341. /**
  342. * 获取下拉树菜单的链接。
  343. * Get link for drop tree menu.
  344. *
  345. * @param string $moduleName
  346. * @param string $methodName
  347. * @param string $programID
  348. * @param string $vars
  349. * @param string $from
  350. * @access protected
  351. * @return string
  352. */
  353. protected function getLink($moduleName, $methodName, $programID, $vars = '', $from = 'program')
  354. {
  355. if($from != 'program') return helper::createLink('product', 'all', "programID={$programID}" . $vars);
  356. if($this->config->edition != 'open')
  357. {
  358. $flow = $this->loadModel('workflow')->getByModule($moduleName);
  359. if($flow && in_array($flow->app, array('scrum', 'waterfall', 'kanbanProject'))) $flow->app = 'project';
  360. if(!empty($flow) && $flow->buildin == '0') return helper::createLink('flow', 'ajaxSwitchBelong', "objectID=$programID&moduleName=$moduleName") . "#app=$flow->app";
  361. }
  362. if($moduleName == 'project')
  363. {
  364. $moduleName = 'program';
  365. $methodName = 'project';
  366. }
  367. if($moduleName == 'product')
  368. {
  369. $moduleName = 'program';
  370. $methodName = 'product';
  371. }
  372. return helper::createLink($moduleName, $methodName, "programID={$programID}");
  373. }
  374. /**
  375. * 在产品视角查看项目时通过浏览类型获取产品列表。
  376. * Get products by browse type of the program for product view in program list.
  377. *
  378. * @param string $browseType
  379. * @param array $products
  380. * @return array
  381. */
  382. protected function getProductsByBrowseType($browseType, $products)
  383. {
  384. /* Filter the program by browse type. */
  385. foreach($products as $index => $product)
  386. {
  387. $programID = $product->program;
  388. /* The product associated with the program. */
  389. if(!empty($programID))
  390. {
  391. $program = $this->program->getByID($programID);
  392. if(!empty($program) && in_array($browseType, array('all', 'unclosed', 'wait', 'doing', 'suspended', 'closed')))
  393. {
  394. if($browseType == 'unclosed' && $program->status == 'closed')
  395. unset($products[$index]);
  396. elseif($browseType != 'unclosed' && $browseType != 'all' && $program->status != $browseType)
  397. unset($products[$index]);
  398. }
  399. }
  400. else
  401. {
  402. /* The product without program only can be viewed when browse type is all and not closed. */
  403. if($browseType != 'all' and $browseType != 'unclosed') unset($products[$index]);
  404. }
  405. }
  406. return $products;
  407. }
  408. }