view.html.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731
  1. <?php
  2. /**
  3. * The view view file of execution module of ZenTaoPMS.
  4. * @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
  5. * @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
  6. * @author Shujie Tian<tianshujie@easycorp.ltd>
  7. * @package execution
  8. * @link https://www.zentao.net
  9. */
  10. namespace zin;
  11. include($this->app->getModuleRoot() . 'ai/ui/promptmenu.html.php');
  12. $progress = ($execution->totalConsumed + $execution->totalLeft) ? floor($execution->totalConsumed / ($execution->totalConsumed + $execution->totalLeft) * 1000) / 1000 * 100 : 0;
  13. $isKanban = isset($execution->type) && $execution->type == 'kanban';
  14. $chartURL = createLink('execution', $isKanban ? 'ajaxGetCFD' : 'ajaxGetBurn', "executionID={$execution->id}");
  15. $isEn = $app->getClientLang() == 'en';
  16. $storyTitle = $isEn ? $lang->common->stories : $lang->common->story;
  17. $taskTitle = $isEn ? $lang->task->plural : $lang->task->common;
  18. $bugTitle = $isEn ? $lang->bug->plural : $lang->bug->common;
  19. /* Construct suitable actions for the current execution. */
  20. $execution->rawID = $execution->id;
  21. $programDom = null;
  22. if($config->systemMode == 'ALM' && isset($execution->projectInfo->grade) && $execution->projectInfo->grade > 1 && helper::hasFeature('program'))
  23. {
  24. foreach($programList as $programID => $name)
  25. {
  26. if(common::hasPriv('program', 'product'))
  27. {
  28. $programLink = $this->createLink('program', 'product', "programID={$programID}");
  29. $programList[$programID] = "<a href='{$programLink}' title='{$name}'>{$name}</a>";
  30. }
  31. else
  32. {
  33. $programList[$programID] = "<span title='{$name}'>{$name}</span>";
  34. }
  35. }
  36. $programDom = div
  37. (
  38. icon('program mr-2'),
  39. html(implode('/ ', $programList))
  40. );
  41. }
  42. $relatedProducts = null;
  43. if(!empty($execution->projectInfo->hasProduct) || $features['plan'])
  44. {
  45. foreach($products as $productID => $product)
  46. {
  47. $productDom = null;
  48. $planDom = null;
  49. $branches = array();
  50. if(!empty($execution->projectInfo->hasProduct))
  51. {
  52. foreach($product->branches as $branchID)
  53. {
  54. $branchName = isset($branchGroups[$productID][$branchID]) ? '/' . $branchGroups[$productID][$branchID] : '';
  55. $branches[] = div(
  56. setClass('flex clip w-full items-center'),
  57. icon('product mr-2'),
  58. a
  59. (
  60. setClass('flex'),
  61. set::title($product->name . $branchName),
  62. hasPriv('product', 'browse') ? set::href(createLink('product', 'browse', "productID={$productID}&branch={$branchID}")) : null,
  63. span
  64. (
  65. setClass('flex-1'),
  66. setStyle('width', '0'),
  67. $product->name . $branchName
  68. )
  69. )
  70. );
  71. }
  72. $productDom = h::td
  73. (
  74. div
  75. (
  76. setClass('flex flex-wrap'),
  77. $branches
  78. )
  79. );
  80. }
  81. if($features['plan'])
  82. {
  83. $plans = array();
  84. foreach($product->plans as $planIDList)
  85. {
  86. $planIDList = explode(',', $planIDList);
  87. $planIDList = array_filter($planIDList);
  88. foreach($planIDList as $planID)
  89. {
  90. if(!isset($planGroups[$productID][$planID])) continue;
  91. $class = 'clip';
  92. if(count($planIDList) <= 2) $class .= ' flex flex-1 w-0 items-center';
  93. if(count($planIDList) > 2) $class .= ' flex-none w-1/3';
  94. if(count($plans) > 2) $class .= ' mt-2';
  95. if(count($plans) % 3 != 0) $class .= ' pl-6';
  96. $plans[] = div
  97. (
  98. setClass($class),
  99. icon('calendar mr-2'),
  100. a
  101. (
  102. set::title($product->name . '/' . $planGroups[$productID][$planID]),
  103. set('data-app', $execution->projectInfo->hasProduct ? '' : 'project'),
  104. hasPriv('productplan', 'view') ? set::href(createLink('productplan', 'view', "planID={$planID}")) : null,
  105. span($product->name . '/' . $planGroups[$productID][$planID])
  106. )
  107. );
  108. }
  109. }
  110. $planDom[] = h::td
  111. (
  112. div
  113. (
  114. setClass('flex flex-wrap'),
  115. $plans
  116. )
  117. );
  118. }
  119. $relatedProducts[] = h::tr(setClass('border-r'), $productDom, $planDom);
  120. }
  121. }
  122. div
  123. (
  124. setClass('flex w-full'),
  125. div
  126. (
  127. setClass('flex-auto canvas flex p-4 w-1/2'),
  128. div
  129. (
  130. setClass('text-center w-1/3 flex flex-col justify-center items-center'),
  131. div
  132. (
  133. set('class', 'chart pie-chart'),
  134. echarts
  135. (
  136. set::color(array('#2B80FF', '#E3E4E9')),
  137. set::width(120),
  138. set::height(120),
  139. set::series
  140. (
  141. array
  142. (
  143. array
  144. (
  145. 'type' => 'pie',
  146. 'radius' => array('80%', '90%'),
  147. 'itemStyle' => array('borderRadius' => '40'),
  148. 'label' => array('show' => false),
  149. 'data' => array($progress, 100 - $progress)
  150. )
  151. )
  152. )
  153. ),
  154. div
  155. (
  156. set::className('pie-chart-title text-center'),
  157. div(span(set::className('text-2xl font-bold'), $progress . '%')),
  158. div
  159. (
  160. span
  161. (
  162. setClass('text-sm text-gray'),
  163. $lang->allProgress,
  164. icon
  165. (
  166. 'help ml-1',
  167. toggle::tooltip(array('title' => $lang->execution->progressTip)),
  168. set('data-placement', 'bottom'),
  169. set('data-type', 'white'),
  170. set('data-class-name', 'text-gray border border-light'),
  171. setClass('text-gray')
  172. )
  173. )
  174. )
  175. )
  176. ),
  177. div
  178. (
  179. setClass('border w-3/4 flex justify-center items-center pl-4 py-2'),
  180. $features['story'] ? div
  181. (
  182. setClass('w-1/3'),
  183. div
  184. (
  185. setClass('text-lg font-bold'),
  186. $statData->storyCount
  187. ),
  188. $storyTitle
  189. ) : null,
  190. div
  191. (
  192. setClass('w-1/3'),
  193. div
  194. (
  195. setClass('text-lg font-bold'),
  196. $statData->taskCount
  197. ),
  198. $taskTitle
  199. ),
  200. $features['qa'] ? div
  201. (
  202. setClass('w-1/3'),
  203. div
  204. (
  205. setClass('text-lg font-bold'),
  206. $statData->bugCount
  207. ),
  208. $bugTitle
  209. ) : null
  210. )
  211. ),
  212. div
  213. (
  214. setClass('flex-none w-2/3'),
  215. div
  216. (
  217. setClass('flex items-center'),
  218. label
  219. (
  220. setClass('rounded-full'),
  221. $execution->id
  222. ),
  223. div
  224. (
  225. setClass('text-md font-bold ml-2 clip'),
  226. set::title($execution->name),
  227. $execution->name
  228. ),
  229. !empty($config->setCode) && !empty($execution->code) ? label
  230. (
  231. setClass('gray-300-outline mx-2 flex-none'),
  232. $execution->code
  233. ) : null,
  234. $execution->deleted ? label
  235. (
  236. setClass('danger-outline text-dange flex-noner'),
  237. $lang->execution->deleted
  238. ) : null,
  239. isset($execution->delay) ? label
  240. (
  241. setClass('danger-pale circle size-sm nowrap ml-2 flex-none'),
  242. sprintf($lang->project->delayInfo, $execution->delay)
  243. ) : label
  244. (
  245. setClass("status-{$execution->status} ml-2 flex-none"),
  246. $this->processStatus('execution', $execution)
  247. ),
  248. span
  249. (
  250. setClass('ml-2 flex-none mb-1'),
  251. $lang->execution->kanbanAclList[$execution->acl],
  252. icon
  253. (
  254. 'help',
  255. toggle::tooltip(array('title' => $lang->execution->aclList[$execution->acl])),
  256. set('data-placement', 'right'),
  257. set('data-type', 'white'),
  258. set('data-class-name', 'text-gray border border-light'),
  259. setClass('ml-2 mt-2 text-gray')
  260. )
  261. ),
  262. div(setClass('ai-menu-box flex-auto'))
  263. ),
  264. div
  265. (
  266. setClass('flex mt-4'),
  267. $programDom ? div
  268. (
  269. setClass('clip w-1/2'),
  270. $programDom
  271. ) : null,
  272. div
  273. (
  274. setClass('clip w-1/2'),
  275. icon('project mr-1'),
  276. common::hasPriv('project', 'index') ? a
  277. (
  278. setClass('clip'),
  279. set::href($this->createLink('project', 'index', "projectID={$execution->project}")),
  280. set::title(empty($execution->projectInfo->name) ? '' : $execution->projectInfo->name),
  281. empty($execution->projectInfo->name) ? '' : $execution->projectInfo->name
  282. ) : span
  283. (
  284. setClass('clip w-full'),
  285. set::title(empty($execution->projectInfo->name) ? '' : $execution->projectInfo->name),
  286. empty($execution->projectInfo->name) ? '' : $execution->projectInfo->name
  287. )
  288. )
  289. ),
  290. div
  291. (
  292. set::className('detail-content mt-4 overflow-hidden desc-box'),
  293. set::title(strip_tags($execution->desc)),
  294. html($execution->desc)
  295. )
  296. )
  297. ),
  298. div
  299. (
  300. setClass('flex-none w-1/3 canvas ml-4'),
  301. $isKanban ? to::heading
  302. (
  303. div
  304. (
  305. setClass('panel-title nowrap overflow-hidden'),
  306. set::title($execution->name . $lang->execution->CFD),
  307. $execution->name . $lang->execution->CFD
  308. )
  309. ) : null,
  310. div
  311. (
  312. setID('chartLine'),
  313. on::init()->call('loadTarget', $chartURL, '#chartLine')
  314. )
  315. )
  316. );
  317. $membersDom = array();
  318. foreach(array('PM', 'PO', 'QD', 'RD') as $field)
  319. {
  320. if(empty($execution->$field)) continue;
  321. $user = isset($userList[$execution->$field]) ? $userList[$execution->$field] : null;
  322. if(empty($user)) continue;
  323. $membersDom[] = div
  324. (
  325. setClass('w-1/6 center-y'),
  326. avatar
  327. (
  328. setClass('primary-outline'),
  329. set::size('36'),
  330. set::text($user->realname),
  331. set::src($user->avatar)
  332. ),
  333. span(setClass('my-2'), $user->realname),
  334. span(setClass('text-gray'), $lang->execution->$field)
  335. );
  336. unset($teamMembers[$execution->$field]);
  337. }
  338. $memberCount = count($membersDom);
  339. foreach($teamMembers as $teamMember)
  340. {
  341. if($memberCount >= 7) break;
  342. $user = isset($userList[$teamMember->account]) ? $userList[$teamMember->account] : null;
  343. if(!$user) continue;
  344. $membersDom[] = div
  345. (
  346. setClass('w-1/6 center-y'),
  347. avatar
  348. (
  349. set::size('36'),
  350. set::text($user->realname),
  351. set::src($user->avatar)
  352. ),
  353. span
  354. (
  355. setClass('my-2'),
  356. $user->realname
  357. ),
  358. span
  359. (
  360. setClass('text-gray'),
  361. $lang->execution->member
  362. )
  363. );
  364. $memberCount ++;
  365. }
  366. if(common::hasPriv('execution', 'manageMembers'))
  367. {
  368. $membersDom[] = a
  369. (
  370. set::href(createLink('execution', 'manageMembers', "executionID={$execution->id}")),
  371. setClass('w-1/6 center-y'),
  372. avatar
  373. (
  374. setClass('mb-2'),
  375. set::size('36'),
  376. set::foreColor('var(--color-primary-500-rgb)'),
  377. set::background('var(--color-primary-50)'),
  378. icon('plus')
  379. ),
  380. $lang->project->manage
  381. );
  382. }
  383. $docLibDom = array();
  384. if(common::hasPriv('execution', 'doc') && $execution->type != 'kanban')
  385. {
  386. $docLibCount = 0;
  387. foreach($docLibs as $libID => $docLib)
  388. {
  389. if($docLibCount > 4) break;
  390. $docLibDom[] = div
  391. (
  392. setClass('flex-none w-1/5 py-1 clip pl-4'),
  393. icon('wiki-lib mr-1'),
  394. a
  395. (
  396. $docLib->name,
  397. set('data-app', $app->tab == 'search' ? 'execution' : $app->tab),
  398. set::href($libID == 'files' ? $this->createLink('doc', 'showFiles', "type=execution&objectID={$execution->id}") : $this->createLink('execution', 'doc', "objectID={$execution->id}&libID={$libID}"))
  399. )
  400. );
  401. $docLibCount ++;
  402. }
  403. }
  404. if($canBeChanged && common::hasPriv('doc', 'createLib') && $execution->type != 'kanban')
  405. {
  406. $docLibDom[] = div
  407. (
  408. setClass('flex-none w-1/5 py-1 pl-4'),
  409. a
  410. (
  411. setClass('ghost text-gray'),
  412. icon('plus', setClass('bg-primary-50 text-primary mr-2')),
  413. span($lang->doc->createLib),
  414. set::href(createLink('doc', 'createLib', "type=execution&objectID={$execution->id}")),
  415. set('data-toggle', 'modal'),
  416. set('data-app', $app->tab)
  417. )
  418. );
  419. }
  420. div
  421. (
  422. setClass('my-4 flex w-full items-start'),
  423. div
  424. (
  425. setClass('w-2/3 canvas p-4 flex-auto'),
  426. div
  427. (
  428. /* Linked product and plan. */
  429. (!empty($execution->projectInfo->hasProduct) || $features['plan']) ? h::table
  430. (
  431. setClass('table condensed bordered'),
  432. setID('products'),
  433. h::thead
  434. (
  435. h::tr
  436. (
  437. !empty($execution->projectInfo->hasProduct) ? h::th
  438. (
  439. setClass('w-1/3'),
  440. div
  441. (
  442. setClass('flex items-center justify-between'),
  443. div
  444. (
  445. setClass('flex'),
  446. h::img(set::src('static/svg/product.svg'), setClass('mr-2')),
  447. $lang->execution->manageProducts
  448. ),
  449. common::hasPriv('execution', 'manageproducts') && $execution->type != 'stage' && $project->model != 'waterfallplus' ? btn
  450. (
  451. setClass('ghost text-gray font-normal'),
  452. set::trailingIcon('caret-right pb-0.5'),
  453. set::url(createLink('execution', 'manageproducts', "projectID={$execution->id}")),
  454. $lang->more
  455. ) : null
  456. )
  457. ) : null,
  458. $features['plan'] ? h::th
  459. (
  460. div
  461. (
  462. setClass('flex'),
  463. h::img(set::src('static/svg/productplan.svg'), setClass('mr-2')),
  464. $lang->execution->linkPlan
  465. )
  466. ) : null
  467. )
  468. ),
  469. h::tbody($relatedProducts)
  470. ) : null,
  471. /* Execution team. */
  472. h::table
  473. (
  474. setClass('table condensed bordered mt-4'),
  475. h::thead
  476. (
  477. h::tr
  478. (
  479. h::th
  480. (
  481. div
  482. (
  483. setClass('flex items-center justify-between'),
  484. span($lang->execution->team),
  485. hasPriv('execution', 'team') ? btn
  486. (
  487. setClass('ghost text-gray font-normal'),
  488. set::trailingIcon('caret-right pb-0.5'),
  489. set::url(createLink('execution', 'team', "executionID={$execution->id}")),
  490. $lang->more
  491. ) : null
  492. )
  493. )
  494. )
  495. ),
  496. h::tbody
  497. (
  498. h::tr
  499. (
  500. h::td(div(setClass('flex flex-wrap member-list pt-2'), $membersDom))
  501. )
  502. )
  503. ),
  504. /* Estimate statistics. */
  505. h::table
  506. (
  507. setClass('table condensed bordered mt-4'),
  508. h::thead
  509. (
  510. h::tr
  511. (
  512. h::th(div(setClass('flex items-center justify-between'), span($lang->execution->DurationStats)))
  513. )
  514. ),
  515. h::tbody
  516. (
  517. h::tr
  518. (
  519. h::td
  520. (
  521. div
  522. (
  523. setClass('flex flex-wrap pt-2 mx-4'),
  524. div
  525. (
  526. setClass('w-1/4'),
  527. span(setClass('text-gray'), $lang->execution->begin),
  528. span(setClass('ml-2'), $execution->begin)
  529. ),
  530. div
  531. (
  532. setClass('w-1/4'),
  533. span(setClass('text-gray'), $lang->execution->end),
  534. span(setClass('ml-2'), $execution->end)
  535. ),
  536. div
  537. (
  538. setClass('w-1/4'),
  539. span(setClass('text-gray'), $lang->execution->realBeganAB),
  540. span(setClass('ml-2'), helper::isZeroDate($execution->realBegan) ? '' : $execution->realBegan)
  541. ),
  542. div
  543. (
  544. setClass('w-1/4'),
  545. span(setClass('text-gray'), $lang->execution->realEndAB),
  546. span(setClass('ml-2'), helper::isZeroDate($execution->realEnd) ? '' : $execution->realEnd)
  547. )
  548. )
  549. )
  550. )
  551. )
  552. ),
  553. h::table
  554. (
  555. setClass('table condensed bordered mt-4'),
  556. h::thead
  557. (
  558. h::tr
  559. (
  560. h::th(div(setClass('flex items-center justify-between'), span($lang->execution->lblStats)))
  561. )
  562. ),
  563. h::tbody
  564. (
  565. h::tr
  566. (
  567. h::td
  568. (
  569. div
  570. (
  571. setClass('flex flex-wrap pt-2 mx-4'),
  572. div
  573. (
  574. setClass('w-1/3'),
  575. span(setClass('text-gray'), $lang->execution->estimateHours),
  576. span(setClass('ml-2'), helper::formatHours($execution->totalEstimate) . $lang->execution->workHourUnit)
  577. ),
  578. div
  579. (
  580. setClass('w-1/3'),
  581. span(setClass('text-gray'), $lang->execution->consumedHours),
  582. span(setClass('ml-2'), helper::formatHours($execution->totalConsumed) . $lang->execution->workHourUnit)
  583. ),
  584. div
  585. (
  586. setClass('w-1/3'),
  587. span(setClass('text-gray'), $lang->execution->leftHours),
  588. span(setClass('ml-2'), helper::formatHours($execution->totalLeft) . $lang->execution->workHourUnit)
  589. ),
  590. div
  591. (
  592. setClass('w-1/3 mt-4'),
  593. span(setClass('text-gray'), $lang->execution->totalDays),
  594. span(setClass('ml-2'), $execution->days)
  595. ),
  596. div
  597. (
  598. setClass('w-1/3 mt-4'),
  599. span(setClass('text-gray'), $lang->execution->totalHours),
  600. span(setClass('ml-2'), $execution->totalHours . $lang->execution->workHourUnit)
  601. )
  602. )
  603. )
  604. )
  605. )
  606. ),
  607. /* Execution doc lib. */
  608. hasPriv('execution', 'doc') && !$isKanban ? h::table
  609. (
  610. setClass('table condensed bordered mt-4'),
  611. h::thead
  612. (
  613. h::tr
  614. (
  615. h::th
  616. (
  617. div
  618. (
  619. setClass('flex items-center justify-between'),
  620. span($lang->execution->doclib),
  621. hasPriv('execution', 'doc') ? btn
  622. (
  623. setClass('ghost text-gray font-normal'),
  624. set::trailingIcon('caret-right pb-0.5'),
  625. set::url(createLink('execution', 'doc', "executionID={$execution->id}")),
  626. $lang->more
  627. ) : null
  628. )
  629. )
  630. )
  631. ),
  632. h::tbody
  633. (
  634. h::tr
  635. (
  636. h::td(div(setClass('flex flex-wrap pt-2'), $docLibDom))
  637. )
  638. )
  639. ) : null,
  640. html($this->printExtendFields($execution, 'html', 'position=info', false))
  641. )
  642. ),
  643. div
  644. (
  645. setClass('ml-4 w-1/3 flex-none'),
  646. panel
  647. (
  648. to::heading
  649. (
  650. div(set('class', 'panel-title'), $lang->execution->latestDynamic)
  651. ),
  652. to::headingActions
  653. (
  654. common::hasPriv('execution', 'dynamic') ? btn
  655. (
  656. setClass('ghost text-gray font-normal'),
  657. set::url(createLink('execution', 'dynamic', "executionID={$execution->id}&type=all")),
  658. $lang->more
  659. ) : null
  660. ),
  661. set::bodyClass('h-80 overflow-y-auto pt-0'),
  662. set::shadow(false),
  663. dynamic()
  664. ),
  665. html($this->printExtendFields($execution, 'html', 'position=basic', false)),
  666. div
  667. (
  668. setClass('mt-4'),
  669. history
  670. (
  671. set::commentUrl(createLink('action', 'comment', array('objectType' => 'execution', 'objectID' => $execution->id))),
  672. set::bodyClass('maxh-72 overflow-y-auto')
  673. )
  674. )
  675. )
  676. );
  677. $actions = $this->loadModel('common')->buildOperateMenu($execution);
  678. if($config->systemMode == 'PLM' && $config->edition == 'ipd')
  679. {
  680. $canStart = $this->execution->checkStageStatus($execution->id, 'start');
  681. $canClose = $this->execution->checkStageStatus($execution->id, 'close');
  682. foreach($actions['mainActions'] as $key => $action)
  683. {
  684. if(isset($action['icon']) && $action['icon'] == 'start' && $canStart['disabled']) unset($actions['mainActions'][$key]);
  685. if(isset($action['icon']) && $action['icon'] == 'off' && $canClose['disabled']) unset($actions['mainActions'][$key]);
  686. }
  687. }
  688. div
  689. (
  690. setClass('w-2/3 center fixed actions-menu'),
  691. floatToolbar
  692. (
  693. isAjaxRequest('modal') ? null : to::prefix(backBtn(set::icon('back'), $lang->goback)),
  694. set::main($actions['mainActions']),
  695. set::suffix($actions['suffixActions']),
  696. set::object($execution)
  697. )
  698. );