view.html.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. <?php
  2. /**
  3. * The view file of project 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 Yanyi Cao <caoyanyi@easycorp.ltd>
  7. * @package project
  8. * @link https://www.zentao.net
  9. */
  10. namespace zin;
  11. include($this->app->getModuleRoot() . 'ai/ui/promptmenu.html.php');
  12. $isEn = $app->getClientLang() == 'en';
  13. $storyTitle = $isEn ? $lang->common->stories : $lang->common->story;
  14. $taskTitle = $isEn ? $lang->task->plural : $lang->task->common;
  15. $bugTitle = $isEn ? $lang->bug->plural : $lang->bug->common;
  16. jsVar('confirmDeleteTip', $lang->project->confirmDelete);
  17. $programDom = array();
  18. if(helper::hasFeature('program'))
  19. {
  20. $hasProgramPriv = common::hasPriv('program', 'product');
  21. foreach(toIntArray($project->path) as $programID)
  22. {
  23. if(!isset($programList[$programID])) continue;
  24. $name = $programList[$programID];
  25. $programLink = $hasProgramPriv ? $this->createLink('program', 'product', "programID={$programID}") : '';
  26. $programName = $hasProgramPriv ? "<a href='{$programLink}' title='{$name}'>{$name}</a>" : "<span title='{$name}'>{$name}</span>";
  27. $programDom[] = $programName;
  28. }
  29. if($programDom) $programDom = div(icon('program mr-2 text-primary'), html(implode('/ ', $programDom)));
  30. }
  31. $status = $this->processStatus('project', $project);
  32. $relatedProducts = null;
  33. if($project->hasProduct)
  34. {
  35. foreach($products as $productID => $product)
  36. {
  37. $productDom = null;
  38. $branches = array();
  39. foreach($product->branches as $branchID)
  40. {
  41. $branchName = isset($branchGroups[$productID][$branchID]) ? '/' . $branchGroups[$productID][$branchID] : '';
  42. $branches[] = div(
  43. setClass('flex clip w-full items-center'),
  44. icon('product mr-2'),
  45. a(
  46. setClass('flex'),
  47. set::title($product->name . $branchName),
  48. hasPriv('product', 'browse') ? set::href(createLink('product', 'browse', "productID={$productID}&branch={$branchID}")) : null,
  49. span(setClass('flex-1'), setStyle('width', '0'), $product->name . $branchName)
  50. )
  51. );
  52. }
  53. $productDom = h::td(div(setClass('flex flex-wrap'), $branches));
  54. $plans = array();
  55. $planDom = null;
  56. foreach($product->plans as $planIDList)
  57. {
  58. $planIDList = explode(',', $planIDList);
  59. $planIDList = array_filter($planIDList);
  60. foreach($planIDList as $planID)
  61. {
  62. if(!isset($planGroup[$productID][$planID])) continue;
  63. $class = 'clip';
  64. if(count($planIDList) <= 2) $class .= ' flex flex-1 w-0 items-center';
  65. if(count($planIDList) > 2) $class .= ' flex-none w-1/3';
  66. if(count($plans) > 2) $class .= ' mt-2';
  67. if(count($plans) % 3 != 0) $class .= ' pl-6';
  68. $plans[] = div
  69. (
  70. setClass($class),
  71. icon('productplan mr-2 '),
  72. a(
  73. set::title($planGroup[$productID][$planID]),
  74. hasPriv('productplan', 'view') ? set::href(createLink('productplan', 'view', "planID={$planID}")) : null,
  75. span($planGroup[$productID][$planID])
  76. )
  77. );
  78. }
  79. }
  80. $planDom[] = h::td(div(setClass('flex flex-wrap'), $plans));
  81. $relatedProducts[] = h::tr(setClass('border-r'), $productDom, $planDom);
  82. }
  83. }
  84. $membersDom = array();
  85. if(!empty($project->PM))
  86. {
  87. $user = isset($userList[$project->PM]) ? $userList[$project->PM] : null;
  88. if($user)
  89. {
  90. $membersDom[] = div
  91. (
  92. setClass('w-1/6 center-y'),
  93. avatar(setClass('primary-outline'), set::text($user->realname), set::src($user->avatar)),
  94. span(setClass('my-2'), $user->realname),
  95. span(setClass('text-gray'), $lang->project->PM)
  96. );
  97. }
  98. unset($teamMembers[$project->PM]);
  99. }
  100. $memberCount = count($membersDom);
  101. foreach($teamMembers as $teamMember)
  102. {
  103. if($memberCount >= 7) break;
  104. $user = isset($userList[$teamMember->account]) ? $userList[$teamMember->account] : null;
  105. if(!$user) continue;
  106. $membersDom[] = div
  107. (
  108. setClass('w-1/6 center-y'),
  109. avatar(set::text($user->realname), set::src($user->avatar)),
  110. span(setClass('my-2'), $user->realname),
  111. span(setClass('text-gray'), $lang->project->member)
  112. );
  113. $memberCount ++;
  114. }
  115. if(common::hasPriv('project', 'manageMembers'))
  116. {
  117. $membersDom[] = a
  118. (
  119. setClass('w-1/6 center-y cursor-pointer'),
  120. set::href(createLink('project', 'manageMembers', "projectID={$project->id}")),
  121. avatar
  122. (
  123. setClass('mb-2'),
  124. set::foreColor('var(--color-primary-500-rgb)'),
  125. set::background('var(--color-primary-50)'),
  126. icon('plus')
  127. ),
  128. $lang->project->manage
  129. );
  130. }
  131. row
  132. (
  133. setClass('w-full'),
  134. cell
  135. (
  136. setClass('main w-2/3'),
  137. div
  138. (
  139. setClass('flex-auto canvas flex p-4 basic'),
  140. div
  141. (
  142. setClass('text-center w-1/3 flex flex-col justify-center items-center progressBox'),
  143. div
  144. (
  145. set('class', 'chart pie-chart'),
  146. echarts
  147. (
  148. set::color(array('#2B80FF', '#E3E4E9')),
  149. set::width(120),
  150. set::height(120),
  151. set::series(array(
  152. array
  153. (
  154. 'type' => 'pie',
  155. 'radius' => array('80%', '90%'),
  156. 'itemStyle' => array('borderRadius' => '40'),
  157. 'label' => array('show' => false),
  158. 'data' => array($project->progress, 100 - $project->progress)
  159. )))
  160. ),
  161. div
  162. (
  163. set::className('pie-chart-title text-center'),
  164. div(span(set::className('text-2xl font-bold'), $project->progress . '%')),
  165. div
  166. (
  167. span
  168. (
  169. setClass('text-sm text-gray'),
  170. $lang->allProgress,
  171. btn
  172. (
  173. set::size('sm'),
  174. set::icon('help'),
  175. setClass('ghost form-label-hint text-gray-300 ml-2'),
  176. toggle::tooltip(array('title' => $lang->execution->progressTip, 'className' => 'text-gray border border-gray-300', 'type' => 'white', 'placement' => 'right'))
  177. )
  178. )
  179. )
  180. )
  181. ),
  182. div
  183. (
  184. setClass('border w-3/4 flex justify-center items-center pl-4 py-2 statistics'),
  185. div
  186. (
  187. setClass('w-1/3 storyCount'),
  188. div(setClass('text-md font-bold'), $statData->storyCount),
  189. span(setClass('text-gray'), $storyTitle)
  190. ),
  191. div
  192. (
  193. setClass('w-1/3 taskCount'),
  194. div(setClass('text-md font-bold'), $statData->taskCount),
  195. span(setClass('text-gray'), $taskTitle)
  196. ),
  197. div
  198. (
  199. setClass('w-1/3 bugCount'),
  200. div(setClass('text-md font-bold'), $statData->bugCount),
  201. span(setClass('text-gray'), $bugTitle)
  202. )
  203. )
  204. ),
  205. div
  206. (
  207. setClass('flex-none w-2/3'),
  208. div
  209. (
  210. setClass('flex items-center'),
  211. label
  212. (
  213. setClass('label rounded-full ring-gray-400 gray-300-pale'),
  214. $project->id
  215. ),
  216. span
  217. (
  218. setClass('text-md font-bold ml-2 clip'),
  219. set::title($project->name),
  220. $project->name
  221. ),
  222. !empty($config->setCode) && !empty($project->code) ? label
  223. (
  224. setClass('label gray-400-pale ring-gray-400 ml-2 clip justify-start'),
  225. set::title($project->code),
  226. $project->code
  227. ) : null,
  228. $config->edition != 'open' ? null : label
  229. (
  230. setClass('label warning-pale ring-warning rounded-full ml-2 flex-none projectType'),
  231. $lang->project->projectTypeList[$project->hasProduct]
  232. ),
  233. $project->deleted ? label
  234. (
  235. setClass('danger-outline text-danger flex-none ml-2'),
  236. $lang->project->deleted
  237. ) : null,
  238. isset($project->delay) ? label
  239. (
  240. setClass("ml-2 flex-none danger-pale"),
  241. $lang->execution->delayed
  242. ) : label
  243. (
  244. setClass("ml-2 flex-none bg-white status status-{$project->status}"),
  245. $status
  246. ),
  247. span
  248. (
  249. setClass('ml-2 text-gray flex-none acl'),
  250. $lang->project->shortAclList[$project->acl],
  251. btn
  252. (
  253. set::size('sm'),
  254. set::icon('help'),
  255. setClass('ghost form-label-hint text-gray-300 ml-2'),
  256. toggle::tooltip(array('title' => $lang->project->subAclList[$project->acl], 'className' => 'text-gray border border-gray-300', 'type' => 'white', 'placement' => 'right'))
  257. )
  258. ),
  259. div(setClass('ai-menu-box flex-auto'))
  260. ),
  261. div
  262. (
  263. setClass('flex mt-4 program'),
  264. $programDom ? div(setClass('clip programBox w-1/2'), $programDom) : null,
  265. $config->edition != 'open' && !empty($project->workflowGroup) ? div
  266. (
  267. setClass('clip w-1/2'),
  268. set::title($lang->project->workflowGroup),
  269. icon('flow', setClass('pr-1')),
  270. $workflowGroup->name
  271. ) : null
  272. ),
  273. div
  274. (
  275. set::className('detail-content mt-4 overflow-hidden desc-box overflow-y-scroll scrollbar-thin scrollbar-hover'),
  276. set::title(strip_tags($project->desc)),
  277. html($project->desc)
  278. )
  279. )
  280. ),
  281. div
  282. (
  283. setClass('flex flex-auto p-4 mt-4 canvas'),
  284. div
  285. (
  286. setClass('w-full'),
  287. /* Linked product and plan. */
  288. $project->hasProduct ? h::table
  289. (
  290. setID('products'),
  291. setClass('table condensed bordered productsBox'),
  292. h::thead
  293. (
  294. h::tr
  295. (
  296. h::th
  297. (
  298. setClass('w-1/3'),
  299. div
  300. (
  301. setClass('flex items-center justify-between'),
  302. span
  303. (
  304. setClass('leading-8 flex'),
  305. img(set('src', 'static/svg/product.svg'), setClass('mr-2')),
  306. $lang->project->manageProducts
  307. ),
  308. common::hasPriv('project', 'manageproducts') ? btn
  309. (
  310. setClass('ghost text-gray'),
  311. set::url(createLink('project', 'manageproducts', "projectID={$project->id}")),
  312. icon('link', setClass('text-primary')),
  313. span($lang->more, setClass('font-normal'))
  314. ) : null
  315. )
  316. ),
  317. h::th
  318. (
  319. setClass('th-plan'),
  320. span
  321. (
  322. setClass('flex'),
  323. img(set('src', 'static/svg/productplan.svg'), setClass('mr-2')),
  324. $lang->execution->linkPlan
  325. )
  326. )
  327. )
  328. ),
  329. h::tbody($relatedProducts)
  330. ) : null,
  331. /* Project team. */
  332. h::table
  333. (
  334. setID('teams'),
  335. setClass('table condensed bordered teams ' . ($project->hasProduct ? 'mt-4' : '')),
  336. h::thead
  337. (
  338. h::tr
  339. (
  340. h::th
  341. (
  342. div
  343. (
  344. setClass('flex items-center justify-between'),
  345. span($lang->projectCommon . ($isEn ? ' ' : '') . $lang->project->team),
  346. hasPriv('project', 'team') ? btn
  347. (
  348. setClass('ghost text-gray'),
  349. set::trailingIcon('caret-right pb-0.5'),
  350. set::url(createLink('project', 'team', "projectID={$project->id}")),
  351. span($lang->more, setClass('font-normal'))
  352. ) : null
  353. )
  354. )
  355. )
  356. ),
  357. h::tbody
  358. (
  359. h::tr
  360. (
  361. h::td(div(setClass('flex flex-wrap member-list pt-2'), $membersDom))
  362. )
  363. )
  364. ),
  365. /* Estimate statistics. */
  366. h::table
  367. (
  368. setClass('table condensed bordered mt-4 duration'),
  369. h::thead
  370. (
  371. h::tr
  372. (
  373. h::th
  374. (
  375. div(setClass('flex items-center justify-between'), span($lang->execution->DurationStats))
  376. )
  377. )
  378. ),
  379. h::tbody
  380. (
  381. h::tr
  382. (
  383. h::td
  384. (
  385. div
  386. (
  387. setClass('flex flex-wrap pt-2 mx-4'),
  388. div
  389. (
  390. setClass('w-1/4'),
  391. span(setClass('text-gray'), $lang->project->begin),
  392. span(setClass('ml-2'), $project->begin)
  393. ),
  394. div
  395. (
  396. setClass('w-1/4'),
  397. span(setClass('text-gray'), $lang->project->end),
  398. span
  399. (
  400. setClass('ml-2'),
  401. $project->end = $project->end == LONG_TIME ? $this->lang->project->longTime : $project->end
  402. )
  403. ),
  404. div
  405. (
  406. setClass('w-1/4'),
  407. span(setClass('text-gray'), $lang->project->realBeganAB),
  408. span
  409. (
  410. setClass('ml-2'),
  411. helper::isZeroDate($project->realBegan) ? '' : $project->realBegan
  412. )
  413. ),
  414. div
  415. (
  416. setClass('w-1/4'),
  417. span(setClass('text-gray'), $lang->project->realEndAB),
  418. span
  419. (
  420. setClass('ml-2'),
  421. helper::isZeroDate($project->realEnd) ? '' : $project->realEnd
  422. )
  423. )
  424. )
  425. )
  426. )
  427. )
  428. ),
  429. h::table
  430. (
  431. setClass('table condensed bordered mt-4 estimate'),
  432. h::thead
  433. (
  434. h::tr
  435. (
  436. h::th
  437. (
  438. div(setClass('flex items-center justify-between'), span($lang->execution->lblStats))
  439. )
  440. )
  441. ),
  442. h::tbody
  443. (
  444. h::tr
  445. (
  446. h::td
  447. (
  448. div
  449. (
  450. setClass('flex flex-wrap pt-2 mx-4'),
  451. div
  452. (
  453. setClass('w-1/3'),
  454. span(setClass('text-gray'), $lang->execution->estimateHours),
  455. span(setClass('ml-2'), helper::formatHours($project->estimate) . 'h')
  456. ),
  457. div
  458. (
  459. setClass('w-1/3'),
  460. span(setClass('text-gray'), $lang->execution->consumedHours),
  461. span(setClass('ml-2'), helper::formatHours($project->consumed) . 'h')
  462. ),
  463. div
  464. (
  465. setClass('w-1/3'),
  466. span(setClass('text-gray'), $lang->execution->leftHours),
  467. span(setClass('ml-2'), helper::formatHours($project->left) . 'h')
  468. ),
  469. div
  470. (
  471. setClass('w-1/3 mt-4'),
  472. span(setClass('text-gray'), $lang->execution->totalDays),
  473. span(setClass('ml-2'), $project->days)
  474. ),
  475. div
  476. (
  477. setClass('w-1/3 mt-4'),
  478. span(setClass('text-gray'), $lang->execution->totalHours),
  479. span(setClass('ml-2'), helper::formatHours($project->left) . 'h')
  480. ),
  481. div
  482. (
  483. setClass('w-1/3 mt-4'),
  484. span(setClass('text-gray'), $lang->project->budget),
  485. span
  486. (
  487. setClass('ml-2'),
  488. $project->budget ? zget($lang->project->currencySymbol, $project->budgetUnit) . $project->budget : $lang->project->future
  489. )
  490. )
  491. )
  492. )
  493. )
  494. )
  495. ),
  496. html($this->printExtendFields($project, 'html', 'position=info', false))
  497. )
  498. )
  499. ),
  500. cell
  501. (
  502. setClass('side ml-4'),
  503. $project->isTpl ? null : panel
  504. (
  505. setID('dynamicBlock'),
  506. to::heading
  507. (
  508. div
  509. (
  510. set('class', 'panel-title text-md font-bold'),
  511. $lang->execution->latestDynamic
  512. )
  513. ),
  514. to::headingActions
  515. (
  516. ($project->model != 'kanban' and common::hasPriv('project', 'dynamic')) ? btn
  517. (
  518. setClass('ghost text-gray'),
  519. set::url(createLink('project', 'dynamic', "projectID={$projectID}&type=all")),
  520. $lang->more
  521. ) : null
  522. ),
  523. set::bodyClass('pt-0 h-80 overflow-y-auto'),
  524. set::shadow(false),
  525. dynamic()
  526. ),
  527. html($this->printExtendFields($project, 'html', 'position=basic', false)),
  528. div
  529. (
  530. setID('historyBlock'),
  531. setClass('mt-4'),
  532. history
  533. (
  534. set::objectID($project->id),
  535. set::commentUrl(createLink('action', 'comment', array('objectType' => 'project', 'objectID' => $project->id))),
  536. set::bodyClass('maxh-80 overflow-y-auto')
  537. )
  538. )
  539. )
  540. );
  541. $config->project->actionList['edit']['url']['params'] = 'projectID={id}&from=view';
  542. $actions = $this->loadModel('common')->buildOperateMenu($project);
  543. div
  544. (
  545. setClass('w-2/3 center fixed actions-menu'),
  546. floatToolbar
  547. (
  548. isAjaxRequest('modal') ? null : to::prefix(backBtn(set::icon('back'), $lang->goback)),
  549. set::main($actions['mainActions']),
  550. set::suffix($actions['suffixActions']),
  551. set::object($project)
  552. )
  553. );
  554. /* ====== Render page ====== */
  555. render();