kanban.html.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. <?php if($extView = $this->getExtViewFile(__FILE__)){include $extView; return helper::cd();}?>
  2. <?php css::import($jsRoot . 'zui/kanban/min.css'); ?>
  3. <?php js::import($jsRoot . 'zui/kanban/min.js'); ?>
  4. <?php js::set('delayText', $lang->delayed); ?>
  5. <style>
  6. #kanbanList .panel-heading {padding: 10px;}
  7. #kanbanList .panel-body {padding: 0 10px 10px;}
  8. #kanbanList .kanban {min-height: 120px; overflow: visible;}
  9. #kanbanList .kanban-card {border-radius: 2px; padding: 10px;}
  10. #kanbanList .kanban-card:hover {border: 1px solid #ccc;}
  11. #kanbanList .kanban-header,
  12. #kanbanList .kanban-lane {border-bottom: none; margin-bottom: 0;}
  13. #kanbanList .kanban-sub-lane {border-bottom: 0;}
  14. #kanbanList .kanban-lane {margin-top: 2px; min-height: 100px;}
  15. #kanbanList .kanban-lane.has-sub-lane {background-color: transparent;}
  16. #kanbanList .kanban-lane + .kanban-lane {margin-top: 10px;}
  17. #kanbanList .kanban-sub-lane + .kanban-sub-lane {margin-top: 2px;}
  18. #kanbanList .kanban-header-col > .title > .text {max-width: 200px; max-width: calc(100% - 50px);}
  19. #kanbanList .kanban-header + .kanban-lane > .kanban-lane-name {margin-top: 0;}
  20. #kanbanList .kanban-header {position: relative;}
  21. #kanbanList .kanban-item.link-block {padding: 0;}
  22. #kanbanList .kanban-item.link-block a {padding: 10px; display: block;}
  23. #kanbanList .kanban-card {display: flex;}
  24. #kanbanList .kanban-card > .title {white-space: nowrap; overflow: hidden;}
  25. #kanbanList .kanban-card.has-progress {padding-right: 40px; position: relative;}
  26. #kanbanList .kanban-card.has-progress > .progress-pie,
  27. #kanbanList .kanban-card.has-progress > .ring {position: absolute; right: 7px; top: 7px; width: 24px; height: 24px;}
  28. #kanbanList .kanban-card.has-left-border {border-left: 2px solid #838a9d;}
  29. #kanbanList .kanban-card.has-left-border.border-left-green {border-left-color: #0bd986;}
  30. #kanbanList .kanban-card.has-left-border.border-left-red {border-left-color: #ff5d5d;}
  31. #kanbanList .kanban-card.has-left-border.border-left-blue {border-left-color: #0991ff;}
  32. #kanbanList .no-flex .kanban-lane > .kanban-sub-lanes[data-sub-lanes-count="1"] > .kanban-sub-lane {min-height: 90px;}
  33. #kanbanList .no-flex .kanban-lane > .kanban-sub-lanes[data-sub-lanes-count="2"] > .kanban-sub-lane {min-height: 45px;}
  34. .kanban-affixed {padding-top: 72px;}
  35. .kanban-affixed > .kanban-header {position: fixed!important; top: 0; color: #fff; z-index: 100; overflow: hidden; min-width: 0; background: none;}
  36. .kanban-affixed > .kanban-header > .kanban-header-cols {position: absolute; height: 100%; top: 0; background: rgba(80,80,80,.9);}
  37. .kanban-affixed > .kanban-header > .kanban-group-header {display: none;}
  38. .kanban-affixed > .kanban-header > .kanban-header-cols:before {display: block; content: ' '; position: absolute; left: -20px; top: 0; bottom: 0; background-color: inherit; width: 20px;}
  39. #kanbanList .kanban-col[data-type="unclosedProduct"] .kanban-item {padding: 0;}
  40. #kanbanList .kanban-col[data-type="unclosedProduct"] .kanban-lane-items {height: 100%; display: flex; flex-direction: column; justify-content: center; padding: 0; overflow: hidden;}
  41. #kanbanList .kanban-card.kanban-card-span,
  42. #kanbanList .kanban-col[data-type="unclosedProduct"] .kanban-card {background-color: transparent; border: none; padding: 0; text-align: center; box-shadow: none!important; margin: 0; height: auto!important;}
  43. #kanbanList .kanban-card.kanban-card-span:hover,
  44. #kanbanList .kanban-col[data-type="unclosedProduct"] .kanban-card:hover {box-shadow: none;}
  45. #kanbanList .kanban-card.kanban-card-span > .title,
  46. #kanbanList .kanban-col[data-type="unclosedProduct"] .kanban-card > .title {white-space: nowrap; line-height: 1;}
  47. #kanbanList .kanban-col[data-type="normalRelease"] .kanban-card > .title {display: flex; flex-direction: row; flex-wrap: nowrap; align-items: center;}
  48. #kanbanList .kanban-col[data-type="normalRelease"] .kanban-card > .title > .text {display: block; white-space: nowrap; overflow: hidden;}
  49. #kanbanList .kanban-col[data-type="normalRelease"] .kanban-card > .title.has-icon > .text {margin-right: 5px; max-width: calc(100% - 20px);}
  50. #kanbanList .no-flex .kanban-col[data-type="normalRelease"] .kanban-card > .title {display: block; height: 38px;}
  51. #kanbanList .no-flex .kanban-col[data-type="normalRelease"] .kanban-card > .title > .text {display: inline-block;}
  52. #kanbanList .no-flex .kanban-col[data-type="normalRelease"] .kanban-card > .title > .icon {position: relative; top: -5px}
  53. #kanbanList .kanban-affixed .kanban-header-col[data-type="doingProject"]:after {background-color: #606060;}
  54. /* Show project and execution in one row */
  55. #kanbanList .kanban-lane-col[data-type="doingProject"] + .kanban-lane-col {border-left: none; box-shadow: inset 2px 0 0 #fff;}
  56. #kanbanList .kanban-lane-col[data-type="doingProject"] > .kanban-lane-items {padding: 0!important; overflow: visible!important; max-height: none!important;}
  57. #kanbanList .kanban-item-span {padding: 0!important;}
  58. #kanbanList .project-row {position: relative; width: 200%; width: calc(200% + 2px); height: 62px!important;}
  59. #kanbanList .kanban-item-span + .kanban-item-span > .project-row {border-top: 2px solid #fff;}
  60. #kanbanList .project-row > .project-col {float: left; width: 50%; padding: 10px;}
  61. #kanbanList .project-row > .execution-item {position: absolute!important; left: 100%; top: 0}
  62. .region-affixed-scrollbar {position: fixed; bottom: 0; overflow-x: auto; z-index: 100; background-color: rgba(255,255,255,.85); display: none;}
  63. .has-affixed-region .region-affixed-scrollbar {display: block;}
  64. .region-affixed-holder {height: 1px;}
  65. </style>
  66. <script>
  67. /**
  68. * Check the given date whether it is earlier than today
  69. * @param {Date|string} date Date or date string
  70. * @returns {boolean}
  71. */
  72. function isEarlierThanToday(date)
  73. {
  74. if(!window.todayBegin)
  75. {
  76. var now = new Date();
  77. now.setHours(0);
  78. now.setMinutes(0);
  79. now.setSeconds(0);
  80. now.setMilliseconds(0);
  81. window.todayBegin = now.getTime();
  82. }
  83. return $.zui.createDate(date).getTime() < window.todayBegin;
  84. }
  85. /**
  86. * Render normal text span item
  87. * @param {Object} item Product item object
  88. * @param {JQuery} $item Kanban item element
  89. * @param {Object} col Column object
  90. * @returns {JQuery} $item Kanban item element
  91. */
  92. function renderSpanItem(item, $item)
  93. {
  94. var $title = $item.find('.title');
  95. if(!$title.length)
  96. {
  97. $title = $('<div class="title" />').appendTo($item);
  98. }
  99. $title.text(item.name).attr('title', item.name);
  100. return $item.addClass('kanban-card-span');
  101. }
  102. /**
  103. * Render product item
  104. * @param {Object} item Product item object
  105. * @param {JQuery} $item Kanban item element
  106. * @param {Object} col Column object
  107. * @returns {JQuery} $item Kanban item element
  108. */
  109. function renderProductItem(item, $item)
  110. {
  111. var $title = $item.find('.title');
  112. var isShadow = (typeof(item.shadow) != 'undefined' && item.shadow == '1') ? true : false;
  113. if(!$title.length)
  114. {
  115. if(window.userPrivs.product && !isShadow)
  116. {
  117. $title = $('<a class="title" />')
  118. .attr('href', $.createLink('product', 'browse', 'productID=' + item._id));
  119. }
  120. else
  121. {
  122. $title = $('<div class="title" />');
  123. }
  124. $title.appendTo($item);
  125. }
  126. $title.text(item.name).attr('title', item.name);
  127. return $item;
  128. }
  129. /**
  130. * Render plan item
  131. * @param {Object} item Plan item object
  132. * @param {JQuery} $item Kanban item element
  133. * @param {Object} col Column object
  134. * @returns {JQuery} $item Kanban item element
  135. */
  136. function renderPlanItem(item, $item)
  137. {
  138. var $title = $item.find('.title');
  139. if(!$title.length)
  140. {
  141. if(window.userPrivs.productplan)
  142. {
  143. $item.addClass('link-block');
  144. $title = $('<a class="title" />')
  145. .attr('href', $.createLink('productplan', 'view', 'planID=' + item._id));
  146. }
  147. else
  148. {
  149. $title = $('<div class="title" />');
  150. }
  151. $title.appendTo($item);
  152. }
  153. $title.text(item.title).attr('title', item.title);
  154. return $item;
  155. }
  156. /**
  157. * Render project item
  158. * @param {Object} item Project item object
  159. * @param {JQuery} $item Kanban item element
  160. * @param {Object} col Column object
  161. * @returns {JQuery} $item Kanban item element
  162. */
  163. function renderProjectItem(item, $item)
  164. {
  165. var $title = $item.find('.title');
  166. if(!$title.length)
  167. {
  168. if(window.userPrivs.project)
  169. {
  170. $item.addClass('link-block');
  171. $title = $('<a class="title" />')
  172. .attr('href', $.createLink('project', 'index', 'projectID=' + item._id));
  173. }
  174. else
  175. {
  176. $title = $('<div class="title" />');
  177. }
  178. $title.appendTo($item);
  179. if(item.delay) $title.after("&nbsp;<div><span class='label label-danger label-badge'>" + delayText + "</span></div>");
  180. }
  181. $title.text(item.name).attr('title', item.name);
  182. if(item.status === 'doing')
  183. {
  184. var $progress = $item.find('.ring');
  185. if(!$progress.length)
  186. {
  187. $progress = $('<div class="ring"><span></span></div>').appendTo($item);
  188. }
  189. var progress = Math.max(0, Math.min(100, Math.round(item.progress ? Math.round(item.progress || 0) : 0)));
  190. $progress.find('span').text(progress);
  191. $progress.css('background-position-x', -Math.ceil(progress / 2) * 24);
  192. $item.addClass('has-progress');
  193. }
  194. return $item.addClass('has-left-border')
  195. .toggleClass('border-left-green', item.status === 'doing' && !item.delay)
  196. .toggleClass('border-left-red', item.status === 'doing' && !!item.delay)
  197. .toggleClass('border-left-gray', item.status === 'closed')
  198. .toggleClass('border-left-blue', item.status === 'wait');
  199. }
  200. /**
  201. * Render execution item
  202. * @param {Object} item Execution item object
  203. * @param {JQuery} $item Kanban item element
  204. * @param {Object} col Column object
  205. * @returns {JQuery} $item Kanban item element
  206. */
  207. function renderExecutionItem(item, $item)
  208. {
  209. var $title = $item.find('.title');
  210. if(!$title.length)
  211. {
  212. if(!item.hasOwnProperty('children') && ((item.type == 'kanban' && window.userPrivs.kanban) || (item.type != 'kanban' && window.userPrivs.execution)))
  213. {
  214. var method = item.type == 'kanban' ? 'kanban' : 'task';
  215. $item.addClass('link-block');
  216. $title = $('<a class="title" />')
  217. .attr('href', $.createLink('execution', method, 'executionID=' + item._id));
  218. }
  219. else
  220. {
  221. $title = $('<div class="title" />');
  222. }
  223. $title.appendTo($item);
  224. if(item.delay) $title.after("&nbsp;<div><span class='label label-danger label-badge'>" + delayText + "</span></div>");
  225. }
  226. $title.text(item.name).attr('title', item.name);
  227. if(window.statusColorList && window.statusColorList[item.status])
  228. {
  229. $item.css('borderLeftColor', window.statusColorList[item.status]);
  230. }
  231. var progress = item.progress || (item.hours && !Array.isArray(item.hours) ? Math.round(item.hours.progress) : undefined);
  232. if(progress === undefined && window.hourList)
  233. {
  234. var hoursInfo = window.hourList[item._id];
  235. progress = hoursInfo ? Math.round(hoursInfo.progress) : undefined;
  236. }
  237. if(progress !== undefined)
  238. {
  239. var $progress = $item.find('.ring');
  240. if(!$progress.length)
  241. {
  242. $progress = $('<div class="ring"><span></span></div>').appendTo($item);
  243. }
  244. progress = Math.max(0, Math.min(100, Math.round(progress)));
  245. $progress.find('span').text(progress);
  246. $progress.css('background-position-x', -Math.ceil(progress / 2) * 24);
  247. $item.addClass('has-progress');
  248. }
  249. var isDelay = item.end && isEarlierThanToday(item.end);
  250. return $item.addClass('has-progress has-left-border')
  251. .toggleClass('border-left-green', !isDelay)
  252. .toggleClass('border-left-red', !!isDelay);
  253. }
  254. /**
  255. * Render release item
  256. * @param {Object} item Release item object
  257. * @param {JQuery} $item Kanban item element
  258. * @param {Object} col Column object
  259. * @returns {JQuery} $item Kanban item element
  260. */
  261. function renderReleaseItem(item, $item)
  262. {
  263. var $title = $item.find('.title');
  264. if(!$title.length)
  265. {
  266. if(window.userPrivs.release)
  267. {
  268. $item.addClass('link-block');
  269. $title = $('<a class="title" />')
  270. .attr('href', $.createLink('release', 'view', 'releaseID=' + item._id));
  271. }
  272. else
  273. {
  274. $title = $('<div class="title" />');
  275. }
  276. $title.appendTo($item);
  277. }
  278. $title.html('<span class="text">' + item.name + '</span>')
  279. .attr('title', item.name);
  280. if(item.marker === '1')
  281. {
  282. if(!$title.find('.icon').length)
  283. {
  284. $title.addClass('has-icon').append('<i class="icon icon-flag text-red"></i>');
  285. }
  286. }
  287. else
  288. {
  289. $title.find('.icon').remove();
  290. }
  291. return $item;
  292. }
  293. /**
  294. * Render project item
  295. * @param {Object} item Project item object
  296. * @param {JQuery} $item Kanban item element
  297. * @param {Object} col Column object
  298. * @returns {JQuery} $item Kanban item element
  299. */
  300. function renderDoingProjectItem(item, $item)
  301. {
  302. $item.closest('.kanban-item').addClass('kanban-item-span');
  303. $item.removeClass('kanban-card').addClass('project-row clearfix').empty();
  304. var $projectCol = $('<div class="project-col"></div>').appendTo($item);
  305. var $projectItem = $('<div class="kanban-card project-item"></div>').appendTo($projectCol);
  306. renderProjectItem(item, $projectItem);
  307. var $executionCol = $('<div class="project-col"></div>').appendTo($item);
  308. if(item.execution)
  309. {
  310. var $executionItem = $('<div class="kanban-card execution-item"></div>').appendTo($executionCol);
  311. renderExecutionItem(item.execution, $executionItem);
  312. }
  313. return $item;
  314. }
  315. /** All build-in columns renderers */
  316. if(!window.columnRenderers) window.columnRenderers =
  317. {
  318. span: renderSpanItem,
  319. execution: renderExecutionItem,
  320. unclosedProduct: renderProductItem,
  321. unexpiredPlan: renderPlanItem,
  322. waitProject: renderProjectItem,
  323. closedProject: renderProjectItem,
  324. doingProject: renderDoingProjectItem,
  325. doingExecution: renderExecutionItem,
  326. normalRelease: renderReleaseItem,
  327. };
  328. /** User privs map */
  329. if(!window.userPrivs) window.userPrivs = {};
  330. /**
  331. * Add column renderer
  332. * @params {string} columnType Column type
  333. * @params {function} renderer Renderer function
  334. */
  335. function addColumnRenderer(columnType, renderer)
  336. {
  337. if(typeof columnType === 'object') $.extend(window.columnRenderers[columnType], columnType);
  338. else window.columnRenderers[columnType] = renderer;
  339. }
  340. /**
  341. * Render kanban item
  342. * @param {Object} item Kanban item object
  343. * @param {JQuery} $item Kanban item element
  344. * @param {Object} col Column object
  345. * @returns {JQuery} $item Kanban item element
  346. */
  347. function renderKanbanItem(item, $item, col, lane, kanban)
  348. {
  349. var columnRenderers = window.columnRenderers;
  350. var renderer = columnRenderers[col.cardType] || columnRenderers[col.type] || columnRenderers[lane.defaultCardType || kanban.defaultCardType];
  351. if(renderer) return renderer(item, $item, col);
  352. return $item;
  353. }
  354. /**
  355. * Affix kanban board header
  356. * @param {JQuery} $kanbanBoard Kanban board element
  357. * @param {boolean} affixed Whether to affix the given board
  358. */
  359. function affixKanbanHeader($kanbanBoard, affixed)
  360. {
  361. var $header = $kanbanBoard.children('.kanban-header');
  362. var $headerCols = $header.children('.kanban-header-cols');
  363. var headerStyle = {width: '', left: 0};
  364. var headerColsStyle = {width: '', marginLeft: ''};
  365. if(affixed)
  366. {
  367. var $kanban = $kanbanBoard.closest('.kanban');
  368. var kanbanBounding = $kanban[0].getBoundingClientRect();
  369. var kanbanBoardBounding = $kanbanBoard[0].getBoundingClientRect();
  370. var laneNameWidth = +$headerCols.css('left').replace('px', '');
  371. headerStyle.width = kanbanBounding.width;
  372. headerStyle.left = kanbanBounding.left;
  373. headerColsStyle.width = kanbanBoardBounding.width - laneNameWidth;
  374. headerColsStyle.marginLeft = kanbanBoardBounding.left - kanbanBounding.left;
  375. }
  376. $header.css(headerStyle);
  377. $headerCols.css(headerColsStyle);
  378. $kanbanBoard.toggleClass('kanban-affixed', !!affixed);
  379. $kanbanBoard.css('padding-top', affixed ? $header.outerHeight() : '');
  380. }
  381. function affixRegionScrollbar($region)
  382. {
  383. $region.addClass('region-affixed');
  384. var $container = $region.parent();
  385. var $scrollbar = $container.find('.region-affixed-scrollbar');
  386. if(!$scrollbar.length)
  387. {
  388. $scrollbar = $('<div class="region-affixed-scrollbar"><div class="region-affixed-holder"></div></div>').css('height', $.zui.getScrollbarSize() + 1).appendTo($container).on('scroll', function()
  389. {
  390. $('#kanban .region-affixed .kanban').scrollLeft($scrollbar.scrollLeft());
  391. });
  392. }
  393. var $kanban = $region.find('.kanban');
  394. $scrollbar.width($region.outerWidth());
  395. $scrollbar.find('.region-affixed-holder').width($kanban[0].scrollWidth);
  396. var scrollLeft = $kanban.scrollLeft();
  397. if(scrollLeft !== $scrollbar.scrollLeft()) $scrollbar.scrollLeft(scrollLeft);
  398. }
  399. /** Update kanban affix state for all boards in page */
  400. function updateKanbanAffixState()
  401. {
  402. var $boards = $('.kanban-board');
  403. var $lastAffixedBoard = $boards.filter('.kanban-affixed');
  404. var containerTop = window.kanbanAffixContainer ? $(window.kanbanAffixContainer)[0].getBoundingClientRect().top : 0;
  405. var $currentAffixedBoard;
  406. $boards.each(function()
  407. {
  408. var $board = $(this);
  409. var bounds = $board[0].getBoundingClientRect();
  410. if(bounds.top < containerTop && bounds.bottom > (containerTop))
  411. {
  412. $currentAffixedBoard = $board;
  413. }
  414. });
  415. if($lastAffixedBoard.length && (!$currentAffixedBoard || $lastAffixedBoard[0] !== $currentAffixedBoard[0]))
  416. {
  417. affixKanbanHeader($lastAffixedBoard, false);
  418. }
  419. if($currentAffixedBoard) affixKanbanHeader($currentAffixedBoard, true);
  420. var $kanban = $('#kanban');
  421. var $regions = $kanban.children('.region');
  422. if(!$regions.length) return;
  423. var $lastAffixedRegion = $regions.filter('.region-affixed');
  424. var $currentAffixedRegion = [];
  425. var winHeight = $(window).height();
  426. $regions.each(function()
  427. {
  428. var $region = $(this);
  429. var bounds = this.getBoundingClientRect();
  430. if(bounds.bottom > winHeight && bounds.top < (winHeight - 64))
  431. {
  432. $currentAffixedRegion = $region;
  433. return false;
  434. }
  435. });
  436. $kanban.toggleClass('has-affixed-region', !!$currentAffixedRegion.length);
  437. if($lastAffixedRegion.length && $currentAffixedRegion[0] !== $lastAffixedRegion[0]) $lastAffixedRegion.removeClass('region-affixed');
  438. if($currentAffixedRegion.length) affixRegionScrollbar($currentAffixedRegion);
  439. }
  440. /** Try to update kanban affix state */
  441. function tryUpdateKanbanAffix()
  442. {
  443. if(window.updateKanbanAffixTimer) $.zui.clearAsap(window.updateKanbanAffixTimer);
  444. window.updateKanbanAffixTimer = $.zui.asap(function()
  445. {
  446. updateKanbanAffixState();
  447. window.updateKanbanAffixTimer = null;
  448. });
  449. }
  450. /* Kanban color list for lane name */
  451. if(!window.kanbanColorList) window.kanbanColorList = ['#32C5FF', '#006AF1', '#9D28B2', '#FF8F26', '#7FBB00', '#424BAC', '#66c5f8', '#EC2761'];
  452. /* Set default options to kanban component */
  453. $.extend($.fn.kanban.Constructor.DEFAULTS,
  454. {
  455. readonly: true,
  456. maxColHeight: 260,
  457. itemRender: renderKanbanItem,
  458. showCount: true,
  459. showZeroCount: true,
  460. fluidBoardWidth: true,
  461. onRenderLaneName: function($name, lane, $kanban, columns, kanban)
  462. {
  463. var color = kanbanColorList[lane.$index % kanbanColorList.length];
  464. $name.css('background-color', color);
  465. },
  466. onCreate: function(kanban)
  467. {
  468. kanban.$.on('scroll', tryUpdateKanbanAffix);
  469. tryUpdateKanbanAffix();
  470. }
  471. });
  472. $(function()
  473. {
  474. $(window.kanbanAffixContainer || window).on('scroll resize', tryUpdateKanbanAffix);
  475. });
  476. </script>