25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.

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

Powered by TurnKey Linux.