Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

719 lignes
24KB

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

Powered by TurnKey Linux.