view.ui.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  1. window.getLane = function(lane)
  2. {
  3. /* 看板只有一个泳道时,铺满全屏。 */
  4. if(laneCount < 2) lane.minHeight = window.innerHeight - 235;
  5. }
  6. $('#kanbanList').on('enterFullscreen', () => {
  7. $('#kanbanList > div').css('height', '100%');
  8. $('.card-heading a').each(function()
  9. {
  10. const $this = $(this);
  11. const newDiv = $('<div></div>').addClass($this.attr('class')).html($this.html());
  12. $this.data('relatedDiv', newDiv).before(newDiv).hide();
  13. });
  14. });
  15. $('#kanbanList').on('exitFullscreen', () => {
  16. $('#kanbanList > div').css('height', 'calc(100vh - 120px)');
  17. $('.card-heading a').each(function()
  18. {
  19. const $this = $(this);
  20. const relatedDiv = $this.data('relatedDiv');
  21. if(relatedDiv)
  22. {
  23. relatedDiv.remove();
  24. $this.show().removeData('relatedDiv');
  25. }
  26. });
  27. });
  28. /*
  29. * 构造看板泳道上的操作按钮。
  30. * Build action buttons on the kanban lane.
  31. */
  32. window.getLaneActions = function(lane)
  33. {
  34. if(typeof lane.actionList == 'undefined') return [];
  35. let actions = [];
  36. if(lane.actionList.includes('editLaneName')) actions.push({text: kanbanLang.editLaneName, key: 'editLaneName', icon: 'edit', url: $.createLink('kanban', 'editLaneName', 'id=' + lane.id), 'data-toggle': 'modal'});
  37. if(lane.actionList.includes('editLaneColor')) actions.push({text: kanbanLang.editLaneColor, key: 'editLaneColor', icon: 'color', url: $.createLink('kanban', 'editLaneColor', 'id=' + lane.id), 'data-toggle': 'modal'});
  38. if(lane.actionList.includes('sortLane')) actions.push({text: kanbanLang.sortLane, key: 'sortLane', icon: 'move', url: 'javascript:;', 'data-on': 'click', 'data-call': 'sortItems', 'data-params': 'event', 'data-type': 'lane', 'data-id': lane.id, 'data-region': lane.region});
  39. if(lane.actionList.includes('deleteLane')) actions.push({text: kanbanLang.deleteLane, key: 'deleteLane', icon: 'trash', url: $.createLink('kanban', 'deleteLane', 'regionID=' + lane.region + '&id=' + lane.id), 'data-confirm': laneLang.confirmDelete, 'innerClass': 'ajax-submit'});
  40. return [{
  41. type: 'dropdown',
  42. icon: 'ellipsis-v',
  43. caret: false,
  44. items: actions
  45. }];
  46. }
  47. window.getCol = function(col)
  48. {
  49. /* 计算WIP。*/
  50. if(kanban.showWIP != '1') return false;
  51. const limit = col.limit == -1 ? "<i class='icon icon-md icon-infinite'></i>" : col.limit;
  52. const cards = col.cards;
  53. col.subtitleClass = 'ml-1';
  54. let wip = `(${cards} / ${limit})`;
  55. if(col.limit != -1 && cards > col.limit)
  56. {
  57. col.subtitleClass += ' text-danger';
  58. wip += ' <i class="icon icon-exclamation-sign" data-toggle="tooltip" data-title="' + kanbanLang.limitExceeded + '"></i>';
  59. }
  60. col.subtitle = {html: wip};
  61. }
  62. window.getColActions = function(col)
  63. {
  64. if(!canModify) return [];
  65. let actionList = [];
  66. /* 父列不需要创建卡片相关的操作按钮。 */
  67. if(col.parent != '-1') actionList.push(
  68. {
  69. type: 'dropdown',
  70. icon: 'expand-alt text-primary',
  71. caret: false,
  72. items: buildColCardActions(col),
  73. }
  74. );
  75. actionList.push(
  76. {
  77. type: 'dropdown',
  78. icon: 'ellipsis-v',
  79. caret: false,
  80. items: buildColActions(col),
  81. }
  82. );
  83. return actionList;
  84. }
  85. /*
  86. * 构造看板上的创建卡片相关操作按钮。
  87. * Build create card related action buttons on the kanban.
  88. */
  89. window.buildColCardActions = function(col)
  90. {
  91. let actions = [];
  92. if(col.actionList.includes('createCard')) actions.push({text: kanbanLang.createCard, url: $.createLink('kanban', 'createCard', `kanbanID=${kanbanID}&reginID=${col.region}&groupID=${col.group}&columnID=${col.id}`), 'data-toggle': 'modal'});
  93. if(col.actionList.includes('batchCreateCard')) actions.push({text: kanbanLang.batchCreateCard, url: $.createLink('kanban', 'batchCreateCard', `kanbanID=${kanbanID}&reginID=${col.region}&groupID=${col.group}&columnID=${col.id}`), 'data-toggle': 'modal', 'data-size': 'lg'});
  94. if(kanban.object.indexOf('cards') != -1) actions.push({text: kanbanLang.importCard, url: $.createLink('kanban', 'importCard', `kanbanID=${kanbanID}&regionID=${col.region}&groupID=${col.group}&columnID=${col.id}`), 'data-toggle': 'modal', 'data-size': 'lg'});
  95. if(kanban.object && kanban.object != 'cards' && vision != 'lite')
  96. {
  97. actions.push({type: 'divider'});
  98. let items = [];
  99. if(kanban.object.indexOf('plans') != -1) items.push({text: kanbanLang.importPlan, url: $.createLink('kanban', 'importPlan', `kanbanID=${kanbanID}&regionID=${col.region}&groupID=${col.group}&columnID=${col.id}`), 'data-toggle': 'modal', 'data-size': 'lg'});
  100. if(kanban.object.indexOf('releases') != -1) items.push({text: kanbanLang.importRelease, url: $.createLink('kanban', 'importRelease', `kanbanID=${kanbanID}&regionID=${col.region}&groupID=${col.group}&columnID=${col.id}`), 'data-toggle': 'modal', 'data-size': 'lg'});
  101. if(kanban.object.indexOf('executions') != -1) items.push({text: kanbanLang.importExecution, url: $.createLink('kanban', 'importExecution', `kanbanID=${kanbanID}&regionID=${col.region}&groupID=${col.group}&columnID=${col.id}`), 'data-toggle': 'modal', 'data-size': 'lg'});
  102. if(kanban.object.indexOf('builds') != -1) items.push({text: kanbanLang.importBuild, url: $.createLink('kanban', 'importBuild', `kanbanID=${kanbanID}&regionID=${col.region}&groupID=${col.group}&columnID=${col.id}`), 'data-toggle': 'modal', 'data-size': 'lg'});
  103. if(kanban.object.indexOf('tickets') != -1) items.push({text: kanbanLang.importTicket, url: $.createLink('kanban', 'importTicket', `kanbanID=${kanbanID}&regionID=${col.region}&groupID=${col.group}&columnID=${col.id}`), 'data-toggle': 'modal', 'data-type': 'iframe'});
  104. actions.push(
  105. {
  106. text: kanbanLang.importAB,
  107. caret: true,
  108. items: items
  109. }
  110. );
  111. }
  112. return actions;
  113. }
  114. /*
  115. * 构造看板上的创建列、拆分列等操作按钮。
  116. * Build create column, split column and other action buttons on the kanban.
  117. */
  118. window.buildColActions = function(col)
  119. {
  120. if(!col.actionList) return [];
  121. let actions = [];
  122. if(col.actionList.includes('setColumn')) actions.push({text: kanbanLang.setColumn, url: $.createLink('kanban', 'setColumn', `columnID=${col.id}`), 'data-toggle': 'modal', 'icon': 'edit'});
  123. if(col.actionList.includes('setWIP')) actions.push({text: kanbanLang.setWIP, url: $.createLink('kanban', 'setWIP', `columnID=${col.id}`), 'data-toggle': 'modal', 'icon': 'alert'});
  124. if(col.actionList.includes('sortColumn')) actions.push({text: kanbanLang.sortColumn, url: 'javascript:;', 'icon': 'move', 'data-on': 'click', 'data-call': 'sortItems', 'data-params': 'event', 'data-type': 'column', 'data-id': col.id, 'data-region': col.region});
  125. actions.push({type: 'divider'});
  126. if(col.actionList.includes('splitColumn')) actions.push({text: kanbanLang.splitColumn, url: $.createLink('kanban', 'splitColumn', `columnID=${col.id}`), 'data-toggle': 'modal', 'icon': 'col-split'});
  127. if(col.actionList.includes('createColumn')) actions.push({text: kanbanLang.createColumnOnLeft, url: $.createLink('kanban', 'createColumn', `columnID=${col.id}&position=left`), 'data-toggle': 'modal', 'icon': 'col-add-left'});
  128. if(col.actionList.includes('createColumn')) actions.push({text: kanbanLang.createColumnOnRight, url: $.createLink('kanban', 'createColumn', `columnID=${col.id}&position=right`), 'data-toggle': 'modal', 'icon': 'col-add-right'});
  129. actions.push({type: 'divider'});
  130. if(col.actionList.includes('archiveColumn')) actions.push({text: kanbanLang.archiveColumn, url: $.createLink('kanban', 'archiveColumn', `columnID=${col.id}`), 'icon': 'card-archive', 'data-confirm': columnLang.confirmArchive, 'innerClass': 'ajax-submit'});
  131. if(col.actionList.includes('deleteColumn')) actions.push({text: kanbanLang.deleteColumn, url: $.createLink('kanban', 'deleteColumn', `columnID=${col.id}`), 'icon': 'trash', 'innerClass': 'ajax-submit', 'data-confirm': columnLang.confirmDelete});
  132. return actions;
  133. }
  134. /**
  135. * 渲染卡片内容。
  136. * Render card content.
  137. */
  138. window.getItem = function(info)
  139. {
  140. info.item.titleAttrs = {};
  141. if(info.item.fromType == 'execution')
  142. {
  143. renderExecutionItem(info);
  144. }
  145. else if(info.item.fromType == 'release')
  146. {
  147. renderReleaseItem(info);
  148. }
  149. else if(info.item.fromType == 'build')
  150. {
  151. renderBuildItem(info);
  152. }
  153. else if(info.item.fromType == 'productplan')
  154. {
  155. renderProductplanItem(info);
  156. }
  157. else if(info.item.fromType == 'ticket')
  158. {
  159. renderTicketItem(info);
  160. }
  161. else
  162. {
  163. renderGeneralItem(info);
  164. }
  165. if(info.item.color && info.item.color != '#fff') appendCardClass(info.item, 'color-' + info.item.color.replace('#', ''));
  166. appendCardClass(info.item, getCardReminderClass(info.item));
  167. info.item.titleAttrs.class = 'card-title clip';
  168. }
  169. function appendCardClass(item, className)
  170. {
  171. if(!className) return;
  172. item.className = item.className ? `${item.className} ${className}` : className;
  173. }
  174. function getCardReminderClass(item)
  175. {
  176. if(item.progress == 100) return '';
  177. const deadline = normalizeCardDeadline(item.deadline || item.end);
  178. if(!deadline) return '';
  179. if(deadline < today) return 'card-remind-overdue';
  180. const diffDays = Math.floor((new Date(`${deadline}T00:00:00`) - new Date(`${today}T00:00:00`)) / 86400000);
  181. if(expireDays > 0 && diffDays >= 0 && diffDays < expireDays) return 'card-remind-soon';
  182. return '';
  183. }
  184. function normalizeCardDeadline(date)
  185. {
  186. if(!date || date === '0000-00-00' || date < '1970-01-01') return '';
  187. return date.slice(0, 10);
  188. }
  189. window.renderGeneralItem = function(info)
  190. {
  191. let begin = info.item.begin;
  192. let end = info.item.end;
  193. let beginAndEnd = '';
  194. if(begin < '1970-01-01' && end > '1970-01-01')
  195. {
  196. beginAndEnd = end + ' ' + cardLang.deadlineAB;
  197. }
  198. else if(end < '1970-01-01' && begin > '1970-01-01')
  199. {
  200. beginAndEnd = begin + ' ' + cardLang.beginAB;
  201. }
  202. else if(begin > '1970-01-01' && end > '1970-01-01')
  203. {
  204. beginAndEnd = formatDate(begin) + ' ~ ' + formatDate(end);
  205. }
  206. const avatar = renderAvatar(info.item.avatarList);
  207. const content = `
  208. <div class='flex items-center'>
  209. <span class='pri-${info.item.pri}'>${info.item.pri}</span>
  210. <span class='date ml-1'>${beginAndEnd}</span>
  211. <div class='flex-1 flex justify-end' title='${info.item.realnames}'>${avatar}</div>
  212. </div>
  213. `;
  214. info.item.titleUrl = $.createLink('kanban', 'viewCard', `id=${info.item.id}`);
  215. info.item.titleAttrs = {'data-toggle': 'modal', 'data-size' : 'lg', 'title' : info.item.title};
  216. info.item.content = {html: content};
  217. if(kanban.performable == 1)
  218. {
  219. info.item.footer = {html: "<div class='flex'><div class='circle progress mt-3 flex-1' style='width:80%'><div class='progress-bar' style='width: " + info.item.progress + '%\'></div></div><div class="mt-2 ml-2">' + info.item.progress + '%' + '</div></div>'};
  220. }
  221. }
  222. window.renderExecutionItem = function(info)
  223. {
  224. info.item.icon = 'run';
  225. info.item.titleUrl = canViewExecution ? $.createLink('execution', 'task', `id=${info.item.fromID}`) : null;
  226. info.item.titleAttrs = {'class': 'card-title clip', 'title' : info.item.title};
  227. if(info.item.delay)
  228. {
  229. info.item.suffix = executionLang.delayed;
  230. info.item.suffixClass = 'label danger rounded-xl';
  231. }
  232. if(info.item.deleted == '0')
  233. {
  234. statusBox = '<span class="label label-' + info.item.objectStatus + '">' + executionLang.statusList[info.item.objectStatus] + '</span>';
  235. }
  236. else
  237. {
  238. statusBox = '<span class="label label-deleted">' + executionLang.deleted + '</span>';
  239. }
  240. const date = '<span class="ml-2 label ' + (info.item.delay ? 'danger' : 'ghost') + '">' + info.item.end.slice(5) + ' ' + cardLang.deadlineAB + '</span>';
  241. info.item.content = {html: statusBox + date}
  242. if(kanban.performable == 1)
  243. {
  244. info.item.footer = {html: "<div class='flex'><div class='circle progress mt-3 flex-1' style='width:80%'><div class='progress-bar' style='width: " + info.item.progress + '%\'></div></div><div class="mt-2 ml-2">' + info.item.progress + '%' + '</div></div>'};
  245. }
  246. }
  247. window.renderReleaseItem = function(info)
  248. {
  249. info.item.icon = 'publish';
  250. info.item.titleUrl = canViewRelease ? $.createLink('release', 'view', `id=${info.item.fromID}`) : '';
  251. info.item.titleAttrs = {'class': 'card-title clip', 'title' : info.item.title};
  252. if(info.item.deleted == '0')
  253. {
  254. statusBox = '<span class="label label-' + info.item.objectStatus + '">' + releaseLang.statusList[info.item.objectStatus] + '</span>';
  255. }
  256. else
  257. {
  258. statusBox = '<span class="label label-deleted">' + releaseLang.deleted + '</span>';
  259. }
  260. const date = '<span class="ml-2 label gray-pale">' + info.item.date + '</span>';
  261. info.item.content = {html: statusBox + date}
  262. }
  263. window.renderBuildItem = function(info)
  264. {
  265. info.item.icon = 'ver';
  266. info.item.titleUrl = canViewBuild ? $.createLink('build', 'view', `id=${info.item.fromID}`) : '';
  267. info.item.titleAttrs = {'class': 'card-title clip', 'title': info.item.title, 'data-app': 'project'};
  268. const date = '<span class="label gray-pale">' + info.item.date + '</span>';
  269. info.item.content = {html: date}
  270. }
  271. window.renderProductplanItem = function(info)
  272. {
  273. info.item.icon = 'delay';
  274. info.item.titleUrl = canViewPlan ? $.createLink('productplan', 'view', `id=${info.item.fromID}`) : '';
  275. info.item.titleAttrs = {'class': 'card-title clip', 'title': info.item.title};
  276. if(info.item.deleted == '0')
  277. {
  278. statusBox = '<span class="label label-' + info.item.objectStatus + '">' + productplanLang.statusList[info.item.objectStatus] + '</span>';
  279. }
  280. else
  281. {
  282. statusBox = '<span class="label label-deleted">' + productplanLang.deleted + '</span>';
  283. }
  284. const today = zui.formatDate(new Date(), 'yyyy-MM-dd');
  285. const labelType = (info.item.begin <= today && info.item.end >= today) ? 'danger' : 'ghost';
  286. info.item.begin = info.item.begin == futureDate ? productplanLang.future : info.item.begin.slice(5);
  287. info.item.end = info.item.end == futureDate ? productplanLang.future : info.item.end.slice(5);
  288. const date = '<span class="ml-2 label ' + labelType + '">' + info.item.begin + ' ' + productplanLang.to + ' ' + info.item.end + '</span>';
  289. info.item.content = {html: `<div title='${info.item.originDesc}'>${info.item.desc}</div>`};
  290. info.item.contentClass = 'text-gray clip mr-2';
  291. info.item.footer = {html: statusBox + date}
  292. }
  293. window.renderTicketItem = function(info)
  294. {
  295. info.item.icon = 'ticket';
  296. info.item.titleUrl = canViewTicket ? $.createLink('ticket', 'view', `id=${info.item.fromID}`) : '';
  297. info.item.titleAttrs = {'class': 'card-title clip', 'title': info.item.title};
  298. let statusBox = '';
  299. if(info.item.deleted == '0')
  300. {
  301. statusBox = '<span class="label label-' + info.item.objectStatus + '">' + ticketLang.statusList[info.item.objectStatus] + '</span>';
  302. }
  303. else
  304. {
  305. statusBox = '<span class="label label-deleted">' + ticketLang.deleted + '</span>';
  306. }
  307. const date = info.item.deadline && info.item.deadline != '0000-00-00' ? '<span class="ml-2 label gray-pale">' + info.item.deadline.slice(5) + ' ' + cardLang.deadlineAB + '</span>' : '';
  308. let avatar = '';
  309. if(info.item.assignedTo && typeof userList[info.item.assignedTo] != 'undefined')
  310. {
  311. const user = userList[info.item.assignedTo];
  312. const assignedTo = user.realname;
  313. const userAvatar = user.avatar ? "<img src='" + user.avatar + "' />" : info.item.assignedTo.substr(0, 1).toUpperCase();
  314. avatar = "<span class='avatar rounded-full size-xs ml-1 primary' title=" + assignedTo + '>' + userAvatar + '</span>';
  315. }
  316. const content = `
  317. <div class='flex items-center'>
  318. ${statusBox}
  319. ${date}
  320. <div class='flex-1 flex justify-end'>${avatar}</div>
  321. </div>
  322. `;
  323. info.item.content = {html: content};
  324. }
  325. function renderAvatar(avatarList)
  326. {
  327. if(avatarList.length == 0)
  328. {
  329. return '<div class="avatar rounded-full size-xs ml-1 primary" title="' + cardLang.noAssigned + '" style="background: #ccc"><i class="icon icon-person"></i></div>';
  330. }
  331. let assignees = '<div class="avatar-group size-sm">';
  332. let count = 0;
  333. for(let avatar of avatarList)
  334. {
  335. if(count > 2) break;
  336. assignees += `<div class="avatar rounded-full size-xs ml-1 primary" style="font-size: .5rem">${avatar}</div>`;
  337. count ++;
  338. }
  339. if(count > 2) assignees += '<div class="avatar circle size-xs gray-pale">...</div>';
  340. assignees += '</div>';
  341. return assignees;
  342. }
  343. window.getItemActions = function(item)
  344. {
  345. if(!canModify) return [];
  346. return [{
  347. type: 'dropdown',
  348. icon: 'ellipsis-v',
  349. caret: false,
  350. items: buildCardActions(item),
  351. }];
  352. }
  353. window.buildCardActions = function(item)
  354. {
  355. let actions = [];
  356. if(item.actionList.includes('editCard') && item.fromType == '') actions.push({text: kanbanLang.editCard, url: $.createLink('kanban', 'editCard', `id=${item.id}`), 'data-toggle': 'modal', 'icon': 'edit'});
  357. if(item.actionList.includes('deleteCard')) actions.push({text: item.fromType == '' ? kanbanLang.deleteCard : kanbanLang.removeCard, url: $.createLink('kanban', 'deleteCard', `id=${item.id}`), 'data-confirm': item.fromType == '' ? cardLang.confirmDelete : cardLang.confirmRemove, 'innerClass': 'ajax-submit', 'icon': 'trash'});
  358. if(kanban.performable == 1 && item.fromType == '')
  359. {
  360. if(item.status == 'done')
  361. {
  362. if(item.actionList.includes('activateCard')) actions.push({text: kanbanLang.activateCard, url: $.createLink('kanban', 'activateCard', `id=${item.id}`), 'icon': 'magic', 'data-toggle': 'modal'});
  363. }
  364. else
  365. {
  366. if(item.actionList.includes('finishCard')) actions.push({text: kanbanLang.finishCard, url: $.createLink('kanban', 'finishCard', `id=${item.id}`), 'icon': 'checked', 'innerClass': 'ajax-submit'});
  367. }
  368. }
  369. if(kanban.archived == '1' && item.actionList.includes('archiveCard')) actions.push({text: kanbanLang.archiveCard, url: $.createLink('kanban', 'archiveCard', `id=${item.id}`), 'icon': 'card-archive', 'data-confirm': cardLang.confirmArchive, 'innerClass': 'ajax-submit'});
  370. /* Append divider. */
  371. const editCardAction = (item.actionList.includes('editCard') && item.fromType == '') ? true : false;
  372. const deleteCardAction = item.actionList.includes('deleteCard');
  373. const archiveCardAction = (item.actionList.includes('archiveCard') && kanban.archived == '1') ? true : false;
  374. const performable = kanban.performable == 1 ? true : false;
  375. const moveCardAction = item.actionList.includes('moveCard');
  376. const setCardColorAction = item.actionList.includes('setCardColor');
  377. const basicActions = (editCardAction || deleteCardAction || archiveCardAction) ? true : false;
  378. const otherActions = (moveCardAction || setCardColorAction) ? true : false;
  379. if((performable || basicActions) && otherActions) actions.push({type: 'divider'});
  380. if(item.actionList.includes('moveCard')) actions.push({text: kanbanLang.moveCard, url: $.createLink('kanban', 'moveCard', `cardID=${item.id}&fromColID=0&toColID=0&fromLaneID=0&toLaneID=0&kanbanID=${kanbanID}&showModal=true`), 'data-toggle': 'modal', 'icon': 'move', 'data-size': 'sm'});
  381. if(item.actionList.includes('setCardColor'))
  382. {
  383. actions.push({text: kanbanLang.cardColor, 'icon': 'color', items: buildColorItems(item)});
  384. }
  385. return actions;
  386. }
  387. window.buildColorItems = function(card)
  388. {
  389. let items = [];
  390. for (let index in colorList)
  391. {
  392. let color = colorList[index];
  393. let icon = (card.color == color) || (!card.color && color == '#fff') ? "<i class='icon icon-check ml-1' style='position: relative; right: 3px'></i>" : '';
  394. items.push({text: {html: `<div class="colorbox" onclick="changCardColor('${card.id}', '${color}');"><div class='cardcolor' style='background:${color}'></div>` + cardLang.colorList[color] + ' ' + icon + '</div>'}});
  395. }
  396. return items;
  397. }
  398. window.changCardColor = function(cardID, color)
  399. {
  400. color = color.replace('#', '');
  401. $.ajaxSubmit({url: $.createLink('kanban', 'setCardColor', 'cardID=' + cardID + '&color=' + color + '&kanbanID=' + kanbanID)});
  402. }
  403. window.canDrop = function(dragInfo, dropInfo)
  404. {
  405. if(!dragInfo || $.getFullscreenElement()) return false;
  406. const column = this.getCol(dropInfo.col);
  407. const lane = this.getLane(dropInfo.lane);
  408. if(!column || !lane) return false;
  409. return true;
  410. }
  411. window.onDrop = function(changes, dropInfo)
  412. {
  413. if(!dropInfo || !canMoveCard) return false;
  414. const item = dropInfo['drag']['item'];
  415. const toColID = dropInfo['drop']['col']
  416. const toLaneID = dropInfo['drop']['lane']
  417. if(item.col == toColID && item.lane == toLaneID)
  418. {
  419. let sortList = '';
  420. for(let i = 0; i < dropInfo['data']['list'].length; i++) sortList += dropInfo['data']['list'][i] + ',';
  421. url = $.createLink('kanban', 'sortCard', `kanbanID=${kanbanID}&laneID=${toLaneID}&columnID=${toColID}&cards=${sortList}`);
  422. }
  423. else
  424. {
  425. url = $.createLink('kanban', 'moveCard', `cardID=${item.id}&fromColID=${item.column}&toColID=${toColID}&fromLaneID=${item.lane}&toLaneID=${toLaneID}&kanbanID=${kanbanID}`);
  426. }
  427. $.ajaxSubmit({url});
  428. }
  429. window.clickRegionMenu = function(event)
  430. {
  431. $('.regionMenu li').removeClass('active');
  432. $(event.target).closest('li').addClass('active');
  433. const regionID = $(event.target).closest('li').data('region');
  434. const url = $.createLink('kanban', 'view', 'kanbanID=' + kanbanID + '&regionID=' + regionID);
  435. loadPartial(url, '#kanbanList');
  436. }
  437. function formatDate(inputDate)
  438. {
  439. const date = new Date(inputDate);
  440. const formattedDate = `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')}`;
  441. return formattedDate;
  442. }
  443. window.loadMore = function(type, regionID)
  444. {
  445. const method = 'viewArchived' + type;
  446. const selector = '#archived' + type + 's';
  447. const link = $.createLink('kanban', method, 'regionID=' + regionID);
  448. $(selector).load(link, function()
  449. {
  450. const height = $(window).height() - $('#header').height();
  451. $(selector + ' .panel').css('height', height);
  452. });
  453. }
  454. window.sortItems = function(event)
  455. {
  456. const title = $(event.target).closest('a').text();
  457. const objectType = $(event.target).closest('a').data('type');
  458. const objectID = $(event.target).closest('a').data('id');
  459. const regionID = $(event.target).closest('a').data('region');
  460. const url = $.createLink('kanban', 'ajaxGetSortItems', 'objectType=' + objectType + '&objectID=' + objectID);
  461. $.getJSON(url, function(items)
  462. {
  463. zui.Modal.sortView({
  464. title,
  465. items,
  466. }).then(orders => {
  467. if(orders)
  468. {
  469. if(objectType == 'region') link = $.createLink('kanban', 'sortRegion', 'regions=' + orders);
  470. if(objectType == 'column') link = $.createLink('kanban', 'sortColumn', 'regionID=' + regionID + '&columns=' + orders);
  471. if(objectType == 'lane') link = $.createLink('kanban', 'sortLane', 'regionID=' + regionID + '&lanes=' + orders);
  472. $.getJSON(link, function()
  473. {
  474. loadCurrentPage();
  475. });
  476. }
  477. });
  478. });
  479. }