Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

672 linhas
22KB

  1. /* kanban-board.js - grid rendering and drag-drop between cells */
  2. (function () {
  3. 'use strict';
  4. var boardId = KANBAN.boardId;
  5. var laneCollapseStorageKey = 'kanban_lane_collapsed_' + String(boardId);
  6. var collapsedLaneIds = loadCollapsedLaneIds();
  7. var columnShowCount = {};
  8. var columnShowExport = {};
  9. var laneShowExport = {};
  10. var columnAgeSettings = {};
  11. var laneAgeSettings = {};
  12. var searchState = {
  13. query: ''
  14. };
  15. var dragState = {
  16. active: false,
  17. x: 0,
  18. y: 0,
  19. rafId: 0
  20. };
  21. function post(url, data, cb) {
  22. var params = new URLSearchParams();
  23. Object.keys(data).forEach(function (k) { params.append(k, data[k]); });
  24. fetch(url, {
  25. method: 'POST',
  26. headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  27. body: params.toString()
  28. })
  29. .then(function (r) { return r.json(); })
  30. .then(cb)
  31. .catch(function (e) { console.error(url, e); });
  32. }
  33. function esc(s) {
  34. return String(s)
  35. .replace(/&/g, '&')
  36. .replace(/</g, '&lt;')
  37. .replace(/>/g, '&gt;')
  38. .replace(/"/g, '&quot;');
  39. }
  40. function nowDbString() {
  41. var d = new Date();
  42. function pad(n) { return n < 10 ? '0' + n : n; }
  43. return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' +
  44. pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
  45. }
  46. function ageInDays(cellEnteredAt) {
  47. if (!cellEnteredAt) return 0;
  48. var entered = new Date(String(cellEnteredAt).replace(' ', 'T'));
  49. if (isNaN(entered.getTime())) return 0;
  50. return (Date.now() - entered.getTime()) / (24 * 60 * 60 * 1000);
  51. }
  52. function formatAge(cellEnteredAt) {
  53. var days = ageInDays(cellEnteredAt);
  54. if (!cellEnteredAt || isNaN(days)) return null;
  55. if (days < 0) days = 0;
  56. var totalDays = Math.floor(days);
  57. if (totalDays >= 1) {
  58. return totalDays + ' day' + (totalDays === 1 ? '' : 's');
  59. }
  60. var totalHours = Math.floor(days * 24);
  61. if (totalHours >= 1) {
  62. return totalHours + ' hour' + (totalHours === 1 ? '' : 's');
  63. }
  64. return 'less than an hour';
  65. }
  66. function effectiveCardAgeSettings(card) {
  67. var colSettings = columnAgeSettings[String(card.column_id)] || { showCardAge: false, cardAgeWarningDays: 0 };
  68. var laneSettings = laneAgeSettings[String(card.swim_lane_id)] || { showCardAge: false, cardAgeWarningDays: 0 };
  69. var showCardAge = colSettings.showCardAge || laneSettings.showCardAge;
  70. var warningDays = 0;
  71. if (colSettings.showCardAge) {
  72. warningDays = colSettings.cardAgeWarningDays;
  73. } else if (laneSettings.showCardAge) {
  74. warningDays = laneSettings.cardAgeWarningDays;
  75. }
  76. return { showCardAge: showCardAge, cardAgeWarningDays: warningDays };
  77. }
  78. function applyCardAge(el, card) {
  79. var settings = effectiveCardAgeSettings(card);
  80. if (settings.showCardAge) {
  81. var age = formatAge(card.cell_entered_at);
  82. if (age) {
  83. el.title = 'In this column/lane for ' + age;
  84. } else {
  85. el.removeAttribute('title');
  86. }
  87. } else {
  88. el.removeAttribute('title');
  89. }
  90. var isOverdue = settings.cardAgeWarningDays > 0 &&
  91. ageInDays(card.cell_entered_at) >= settings.cardAgeWarningDays;
  92. el.classList.toggle('kanban-card-overdue', isOverdue);
  93. }
  94. function refreshAllCardAges() {
  95. document.querySelectorAll('.kanban-card').forEach(function (el) {
  96. var card = KANBAN.cards.find(function (c) { return String(c.id) === String(el.dataset.id); });
  97. if (card) applyCardAge(el, card);
  98. });
  99. }
  100. function applyGridTemplate() {
  101. var grid = document.getElementById('kanban-grid');
  102. var colHs = grid.querySelectorAll('.kanban-col-header');
  103. var cols = '240px';
  104. colHs.forEach(function () { cols += ' 230px'; });
  105. grid.style.gridTemplateColumns = cols;
  106. }
  107. function countCardsInColumn(colId) {
  108. var count = 0;
  109. KANBAN.cards.forEach(function (c) {
  110. if (String(c.column_id) === String(colId)) count++;
  111. });
  112. return count;
  113. }
  114. function refreshColumnCount(colId) {
  115. var header = document.querySelector('.kanban-col-header[data-col-id="' + colId + '"]');
  116. if (!header) return;
  117. var badge = header.querySelector('.col-count-badge');
  118. if (!columnShowCount[String(colId)]) {
  119. if (badge) badge.remove();
  120. return;
  121. }
  122. if (!badge) {
  123. badge = document.createElement('span');
  124. badge.className = 'col-count-badge';
  125. header.appendChild(badge);
  126. }
  127. badge.textContent = countCardsInColumn(colId);
  128. }
  129. function initColumnCounts() {
  130. KANBAN_COLS.forEach(function (col) {
  131. columnShowCount[String(col.id)] = !!col.show_card_count;
  132. refreshColumnCount(col.id);
  133. });
  134. }
  135. function refreshColumnExport(colId) {
  136. var header = document.querySelector('.kanban-col-header[data-col-id="' + colId + '"]');
  137. if (!header) return;
  138. var btn = header.querySelector('.col-export-btn');
  139. if (columnShowExport[String(colId)]) {
  140. if (!btn) {
  141. btn = document.createElement('a');
  142. btn.className = 'col-export-btn';
  143. btn.href = '/columns/' + colId + '/export';
  144. btn.title = 'Export column to CSV';
  145. btn.innerHTML = '<i class="bi bi-download"></i>';
  146. header.appendChild(btn);
  147. }
  148. } else {
  149. if (btn) btn.remove();
  150. }
  151. }
  152. function refreshLaneExport(laneId) {
  153. var header = document.querySelector('.kanban-lane-header[data-lane-id="' + laneId + '"]');
  154. if (!header) return;
  155. var btn = header.querySelector('.lane-export-btn');
  156. if (laneShowExport[String(laneId)]) {
  157. if (!btn) {
  158. btn = document.createElement('a');
  159. btn.className = 'lane-export-btn';
  160. btn.href = '/swimlanes/' + laneId + '/export';
  161. btn.title = 'Export swim lane to CSV';
  162. btn.innerHTML = '<i class="bi bi-download"></i>';
  163. header.appendChild(btn);
  164. }
  165. } else {
  166. if (btn) btn.remove();
  167. }
  168. }
  169. function initExportButtons() {
  170. KANBAN_COLS.forEach(function (col) {
  171. columnShowExport[String(col.id)] = !!col.show_export_button;
  172. refreshColumnExport(col.id);
  173. });
  174. KANBAN_LANES.forEach(function (lane) {
  175. laneShowExport[String(lane.id)] = !!lane.show_export_button;
  176. refreshLaneExport(lane.id);
  177. });
  178. }
  179. function initCardAgeSettings() {
  180. KANBAN_COLS.forEach(function (col) {
  181. columnAgeSettings[String(col.id)] = {
  182. showCardAge: !!col.show_card_age,
  183. cardAgeWarningDays: parseInt(col.card_age_warning_days, 10) || 0
  184. };
  185. });
  186. KANBAN_LANES.forEach(function (lane) {
  187. laneAgeSettings[String(lane.id)] = {
  188. showCardAge: !!lane.show_card_age,
  189. cardAgeWarningDays: parseInt(lane.card_age_warning_days, 10) || 0
  190. };
  191. });
  192. }
  193. function loadCollapsedLaneIds() {
  194. var laneMap = {};
  195. try {
  196. var raw = window.localStorage.getItem(laneCollapseStorageKey);
  197. if (!raw) return laneMap;
  198. var arr = JSON.parse(raw);
  199. if (!Array.isArray(arr)) return laneMap;
  200. arr.forEach(function (laneId) {
  201. laneMap[String(laneId)] = true;
  202. });
  203. } catch (e) {
  204. console.warn('Failed to load lane collapse state', e);
  205. }
  206. return laneMap;
  207. }
  208. function saveCollapsedLaneIds() {
  209. try {
  210. window.localStorage.setItem(laneCollapseStorageKey, JSON.stringify(Object.keys(collapsedLaneIds)));
  211. } catch (e) {
  212. console.warn('Failed to save lane collapse state', e);
  213. }
  214. }
  215. function setLaneCollapsed(laneId, isCollapsed) {
  216. var laneKey = String(laneId);
  217. var header = document.querySelector('.kanban-lane-header[data-lane-id="' + laneKey + '"]');
  218. if (!header) return;
  219. var laneCells = document.querySelectorAll('.kanban-cell[data-lane-id="' + laneKey + '"]');
  220. header.classList.toggle('lane-collapsed', isCollapsed);
  221. laneCells.forEach(function (cell) {
  222. cell.classList.toggle('lane-collapsed', isCollapsed);
  223. });
  224. var toggleBtn = header.querySelector('.lane-toggle');
  225. if (toggleBtn) {
  226. toggleBtn.setAttribute('aria-expanded', isCollapsed ? 'false' : 'true');
  227. toggleBtn.title = isCollapsed ? 'Expand swim lane' : 'Collapse swim lane';
  228. toggleBtn.setAttribute('aria-label', toggleBtn.title);
  229. }
  230. if (isCollapsed) {
  231. collapsedLaneIds[laneKey] = true;
  232. } else {
  233. delete collapsedLaneIds[laneKey];
  234. }
  235. saveCollapsedLaneIds();
  236. }
  237. function toggleLaneCollapsed(laneId) {
  238. var laneKey = String(laneId);
  239. setLaneCollapsed(laneKey, !collapsedLaneIds[laneKey]);
  240. }
  241. function bindLaneHeaderToggle(headerEl) {
  242. if (!headerEl) return;
  243. var toggleBtn = headerEl.querySelector('.lane-toggle');
  244. if (!toggleBtn) return;
  245. if (!toggleBtn.dataset.boundToggle) {
  246. toggleBtn.addEventListener('click', function (evt) {
  247. evt.preventDefault();
  248. evt.stopPropagation();
  249. toggleLaneCollapsed(headerEl.dataset.laneId);
  250. });
  251. toggleBtn.dataset.boundToggle = '1';
  252. }
  253. }
  254. function initLaneHeaderToggles() {
  255. document.querySelectorAll('.kanban-lane-header').forEach(function (headerEl) {
  256. bindLaneHeaderToggle(headerEl);
  257. if (collapsedLaneIds[String(headerEl.dataset.laneId)]) {
  258. setLaneCollapsed(headerEl.dataset.laneId, true);
  259. }
  260. });
  261. }
  262. function cardBodyHtml(card) {
  263. var html = '<div class="card-headline">' +
  264. '<span class="card-job-number">' + esc(card.job_number || '') + '</span>';
  265. if (card.customer_name) {
  266. html += '<span class="card-customer">' + esc(card.customer_name) + '</span>';
  267. }
  268. html += '</div>';
  269. return html;
  270. }
  271. function buildCardSearchText(card) {
  272. return [
  273. card.job_number || '',
  274. card.job_name || '',
  275. card.customer_name || '',
  276. card.notes || ''
  277. ].join(' ').toLowerCase();
  278. }
  279. function buildCardEl(card) {
  280. var div = document.createElement('div');
  281. div.className = 'kanban-card';
  282. div.dataset.id = card.id;
  283. div.dataset.columnId = card.column_id;
  284. div.dataset.laneId = card.swim_lane_id;
  285. div.dataset.searchText = buildCardSearchText(card);
  286. div.innerHTML = cardBodyHtml(card);
  287. applyCardAge(div, card);
  288. div.addEventListener('click', function () {
  289. var c = KANBAN.cards.find(function (x) { return String(x.id) === String(div.dataset.id); });
  290. if (!c) return;
  291. window.KanbanModal.openEdit(c.id, c.column_id, c.swim_lane_id, c.job_number, c.job_name, c.customer_name, c.delivery_date, c.quantity, c.notes, c.full_note);
  292. });
  293. return div;
  294. }
  295. function renderCards() {
  296. KANBAN.cards.forEach(function (card) {
  297. var cell = document.querySelector(
  298. '.kanban-cell[data-col-id="' + card.column_id + '"][data-lane-id="' + card.swim_lane_id + '"]'
  299. );
  300. if (cell) {
  301. cell.appendChild(buildCardEl(card));
  302. }
  303. });
  304. applyCardFilter();
  305. }
  306. function applyCardFilter() {
  307. var activeQuery = searchState.query;
  308. document.querySelectorAll('.kanban-card').forEach(function (el) {
  309. var searchableText = (el.dataset.searchText || '').toLowerCase();
  310. var isMatch = activeQuery === '' || searchableText.indexOf(activeQuery) > -1;
  311. el.classList.toggle('kanban-card-hidden', !isMatch);
  312. });
  313. }
  314. function initJobSearch() {
  315. var searchInput = document.getElementById('job-search-input');
  316. if (!searchInput) return;
  317. searchInput.addEventListener('input', function () {
  318. searchState.query = String(searchInput.value || '').toLowerCase().trim();
  319. applyCardFilter();
  320. });
  321. }
  322. function handleDragEnd(evt) {
  323. var cardId = evt.item.dataset.id;
  324. var newColId = evt.to.dataset.colId;
  325. var newLaneId = evt.to.dataset.laneId;
  326. var newPos = evt.newIndex;
  327. var siblings = [];
  328. evt.to.querySelectorAll('.kanban-card').forEach(function (el) {
  329. siblings.push(el.dataset.id);
  330. });
  331. var card = KANBAN.cards.find(function (c) { return String(c.id) === String(cardId); });
  332. var oldColId = card ? card.column_id : null;
  333. var oldLaneId = card ? card.swim_lane_id : null;
  334. if (card) {
  335. if (String(oldColId) !== String(newColId) || String(oldLaneId) !== String(newLaneId)) {
  336. card.cell_entered_at = nowDbString();
  337. }
  338. card.column_id = parseInt(newColId, 10);
  339. card.swim_lane_id = parseInt(newLaneId, 10);
  340. card.position = newPos;
  341. applyCardAge(evt.item, card);
  342. }
  343. evt.item.dataset.columnId = newColId;
  344. evt.item.dataset.laneId = newLaneId;
  345. if (oldColId !== null && String(oldColId) !== String(newColId)) {
  346. refreshColumnCount(oldColId);
  347. }
  348. refreshColumnCount(newColId);
  349. post('/cards/' + cardId + '/move', {
  350. column_id: newColId,
  351. swim_lane_id: newLaneId,
  352. position: newPos,
  353. sibling_ids: siblings.join(',')
  354. }, function (res) {
  355. if (!res.ok) console.error('Move failed', res);
  356. });
  357. }
  358. function createCellSortable(cell) {
  359. Sortable.create(cell, {
  360. group: 'cards',
  361. animation: 150,
  362. ghostClass: 'sortable-ghost',
  363. chosenClass: 'sortable-chosen',
  364. handle: '.kanban-card',
  365. delayOnTouchOnly: true,
  366. delay: 120,
  367. touchStartThreshold: 3,
  368. fallbackTolerance: 4,
  369. scroll: true,
  370. bubbleScroll: true,
  371. scrollSensitivity: 140,
  372. scrollSpeed: 32,
  373. onStart: function () { startEdgeAutoScroll(); },
  374. onEnd: function (evt) {
  375. stopEdgeAutoScroll();
  376. handleDragEnd(evt);
  377. }
  378. });
  379. }
  380. function updatePointerFromEvent(evt) {
  381. if (!evt) return;
  382. if (evt.touches && evt.touches.length > 0) {
  383. dragState.x = evt.touches[0].clientX;
  384. dragState.y = evt.touches[0].clientY;
  385. return;
  386. }
  387. if (evt.clientX !== undefined && evt.clientY !== undefined) {
  388. dragState.x = evt.clientX;
  389. dragState.y = evt.clientY;
  390. }
  391. }
  392. function edgeScrollStep() {
  393. if (!dragState.active) return;
  394. var wrapper = document.querySelector('.kanban-wrapper');
  395. if (wrapper) {
  396. var rect = wrapper.getBoundingClientRect();
  397. var edge = 110;
  398. var maxStep = 40;
  399. var dx = 0;
  400. var dy = 0;
  401. if (dragState.x > 0 && dragState.x < rect.left + edge) {
  402. dx = -Math.min(maxStep, Math.ceil((rect.left + edge - dragState.x) / 3));
  403. } else if (dragState.x > rect.right - edge && dragState.x < rect.right + edge) {
  404. dx = Math.min(maxStep, Math.ceil((dragState.x - (rect.right - edge)) / 3));
  405. }
  406. if (dragState.y > 0 && dragState.y < rect.top + edge) {
  407. dy = -Math.min(maxStep, Math.ceil((rect.top + edge - dragState.y) / 3));
  408. } else if (dragState.y > rect.bottom - edge && dragState.y < rect.bottom + edge) {
  409. dy = Math.min(maxStep, Math.ceil((dragState.y - (rect.bottom - edge)) / 3));
  410. }
  411. if (dx !== 0) wrapper.scrollLeft += dx;
  412. if (dy !== 0) wrapper.scrollTop += dy;
  413. }
  414. dragState.rafId = window.requestAnimationFrame(edgeScrollStep);
  415. }
  416. function startEdgeAutoScroll() {
  417. if (dragState.active) return;
  418. dragState.active = true;
  419. document.addEventListener('pointermove', updatePointerFromEvent, { passive: true });
  420. document.addEventListener('touchmove', updatePointerFromEvent, { passive: true });
  421. document.addEventListener('dragover', updatePointerFromEvent, { passive: true });
  422. dragState.rafId = window.requestAnimationFrame(edgeScrollStep);
  423. }
  424. function stopEdgeAutoScroll() {
  425. if (!dragState.active) return;
  426. dragState.active = false;
  427. if (dragState.rafId) {
  428. window.cancelAnimationFrame(dragState.rafId);
  429. dragState.rafId = 0;
  430. }
  431. document.removeEventListener('pointermove', updatePointerFromEvent);
  432. document.removeEventListener('touchmove', updatePointerFromEvent);
  433. document.removeEventListener('dragover', updatePointerFromEvent);
  434. }
  435. function initSortables() {
  436. document.querySelectorAll('.kanban-cell').forEach(createCellSortable);
  437. }
  438. document.getElementById('btn-add-card').addEventListener('click', function () {
  439. window.KanbanModal.openCreate(boardId, null, null);
  440. });
  441. window.KanbanBoard = {
  442. onCardCreated: function (card) {
  443. KANBAN.cards.push(card);
  444. var cell = document.querySelector(
  445. '.kanban-cell[data-col-id="' + card.column_id + '"][data-lane-id="' + card.swim_lane_id + '"]'
  446. );
  447. if (cell) {
  448. cell.appendChild(buildCardEl(card));
  449. }
  450. applyCardFilter();
  451. refreshColumnCount(card.column_id);
  452. },
  453. onCardUpdated: function (id, data) {
  454. var card = KANBAN.cards.find(function (c) { return String(c.id) === String(id); });
  455. if (card) {
  456. card.job_number = data.job_number || '';
  457. card.job_name = data.job_name || '';
  458. card.customer_name = data.customer_name || '';
  459. card.delivery_date = data.delivery_date || null;
  460. card.quantity = data.quantity || '';
  461. card.notes = data.notes || '';
  462. card.full_note = data.full_note !== undefined ? data.full_note : (card.full_note || '');
  463. }
  464. var el = document.querySelector('.kanban-card[data-id="' + id + '"]');
  465. if (el && card) {
  466. el.innerHTML = cardBodyHtml(card);
  467. el.dataset.searchText = buildCardSearchText(card);
  468. }
  469. applyCardFilter();
  470. },
  471. onCardDeleted: function (id) {
  472. var card = KANBAN.cards.find(function (c) { return String(c.id) === String(id); });
  473. var colId = card ? card.column_id : null;
  474. KANBAN.cards = KANBAN.cards.filter(function (c) { return String(c.id) !== String(id); });
  475. var el = document.querySelector('.kanban-card[data-id="' + id + '"]');
  476. if (el) el.remove();
  477. applyCardFilter();
  478. if (colId !== null) refreshColumnCount(colId);
  479. },
  480. addColumn: function (col) {
  481. var grid = document.getElementById('kanban-grid');
  482. var headers = grid.querySelectorAll('.kanban-col-header');
  483. var refNode = headers.length ? headers[headers.length - 1].nextSibling : null;
  484. var hdr = document.createElement('div');
  485. hdr.className = 'kanban-col-header';
  486. hdr.dataset.colId = col.id;
  487. hdr.innerHTML = '<span class="col-label">' + esc(col.name) + '</span>';
  488. grid.insertBefore(hdr, refNode);
  489. columnShowCount[String(col.id)] = false;
  490. columnShowExport[String(col.id)] = false;
  491. columnAgeSettings[String(col.id)] = { showCardAge: false, cardAgeWarningDays: 0 };
  492. var laneHeaders = grid.querySelectorAll('.kanban-lane-header');
  493. laneHeaders.forEach(function (lh) {
  494. var laneId = lh.dataset.laneId;
  495. var cell = document.createElement('div');
  496. cell.className = 'kanban-cell';
  497. cell.dataset.colId = col.id;
  498. cell.dataset.laneId = laneId;
  499. var row = lh.parentNode;
  500. row.appendChild(cell);
  501. createCellSortable(cell);
  502. });
  503. applyGridTemplate();
  504. },
  505. removeColumn: function (colId) {
  506. document.querySelector('.kanban-col-header[data-col-id="' + colId + '"]').remove();
  507. document.querySelectorAll('.kanban-cell[data-col-id="' + colId + '"]').forEach(function (el) { el.remove(); });
  508. KANBAN.cards = KANBAN.cards.filter(function (c) { return String(c.column_id) !== String(colId); });
  509. delete columnShowCount[String(colId)];
  510. delete columnShowExport[String(colId)];
  511. delete columnAgeSettings[String(colId)];
  512. applyGridTemplate();
  513. },
  514. addLane: function (lane) {
  515. var grid = document.getElementById('kanban-grid');
  516. var colHeaders = grid.querySelectorAll('.kanban-col-header');
  517. var lh = document.createElement('div');
  518. lh.className = 'kanban-lane-header';
  519. lh.dataset.laneId = lane.id;
  520. lh.innerHTML =
  521. '<button type="button" class="lane-toggle" title="Collapse swim lane" aria-label="Collapse swim lane" aria-expanded="true">' +
  522. '<i class="bi bi-chevron-down" aria-hidden="true"></i>' +
  523. '</button>' +
  524. '<span class="lane-label">' + esc(lane.name) + '</span>';
  525. grid.appendChild(lh);
  526. bindLaneHeaderToggle(lh);
  527. laneShowExport[String(lane.id)] = false;
  528. laneAgeSettings[String(lane.id)] = { showCardAge: false, cardAgeWarningDays: 0 };
  529. colHeaders.forEach(function (ch) {
  530. var cell = document.createElement('div');
  531. cell.className = 'kanban-cell';
  532. cell.dataset.colId = ch.dataset.colId;
  533. cell.dataset.laneId = lane.id;
  534. grid.appendChild(cell);
  535. createCellSortable(cell);
  536. });
  537. if (collapsedLaneIds[String(lane.id)]) {
  538. setLaneCollapsed(lane.id, true);
  539. }
  540. applyGridTemplate();
  541. },
  542. removeLane: function (laneId) {
  543. document.querySelector('.kanban-lane-header[data-lane-id="' + laneId + '"]').remove();
  544. document.querySelectorAll('.kanban-cell[data-lane-id="' + laneId + '"]').forEach(function (el) { el.remove(); });
  545. KANBAN.cards = KANBAN.cards.filter(function (c) { return String(c.swim_lane_id) !== String(laneId); });
  546. delete laneShowExport[String(laneId)];
  547. delete laneAgeSettings[String(laneId)];
  548. if (collapsedLaneIds[String(laneId)]) {
  549. delete collapsedLaneIds[String(laneId)];
  550. saveCollapsedLaneIds();
  551. }
  552. },
  553. renameColumn: function (colId, name) {
  554. var hdr = document.querySelector('.kanban-col-header[data-col-id="' + colId + '"] .col-label');
  555. if (hdr) hdr.textContent = name;
  556. },
  557. renameLane: function (laneId, name) {
  558. var hdr = document.querySelector('.kanban-lane-header[data-lane-id="' + laneId + '"] .lane-label');
  559. if (hdr) hdr.textContent = name;
  560. },
  561. setColumnShowCount: function (colId, show) {
  562. columnShowCount[String(colId)] = show;
  563. refreshColumnCount(colId);
  564. },
  565. setColumnShowExport: function (colId, show) {
  566. columnShowExport[String(colId)] = !!show;
  567. refreshColumnExport(colId);
  568. },
  569. setLaneShowExport: function (laneId, show) {
  570. laneShowExport[String(laneId)] = !!show;
  571. refreshLaneExport(laneId);
  572. },
  573. setColumnCardAge: function (colId, showCardAge, cardAgeWarningDays) {
  574. columnAgeSettings[String(colId)] = {
  575. showCardAge: !!showCardAge,
  576. cardAgeWarningDays: parseInt(cardAgeWarningDays, 10) || 0
  577. };
  578. refreshAllCardAges();
  579. },
  580. setLaneCardAge: function (laneId, showCardAge, cardAgeWarningDays) {
  581. laneAgeSettings[String(laneId)] = {
  582. showCardAge: !!showCardAge,
  583. cardAgeWarningDays: parseInt(cardAgeWarningDays, 10) || 0
  584. };
  585. refreshAllCardAges();
  586. }
  587. };
  588. applyGridTemplate();
  589. initCardAgeSettings();
  590. initExportButtons();
  591. renderCards();
  592. initSortables();
  593. initJobSearch();
  594. initLaneHeaderToggles();
  595. initColumnCounts();
  596. })();

Powered by TurnKey Linux.