|
- /* kanban-board.js - grid rendering and drag-drop between cells */
- (function () {
- 'use strict';
-
- var boardId = KANBAN.boardId;
- var laneCollapseStorageKey = 'kanban_lane_collapsed_' + String(boardId);
- var collapsedLaneIds = loadCollapsedLaneIds();
- var columnShowCount = {};
- var laneShowCount = {};
- var columnShowExport = {};
- var laneShowExport = {};
- var columnAgeSettings = {};
- var laneAgeSettings = {};
- var searchState = {
- query: ''
- };
- var dragState = {
- active: false,
- x: 0,
- y: 0,
- rafId: 0
- };
-
- function post(url, data, cb) {
- var params = new URLSearchParams();
- Object.keys(data).forEach(function (k) { params.append(k, data[k]); });
- fetch(url, {
- method: 'POST',
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
- body: params.toString()
- })
- .then(function (r) { return r.json(); })
- .then(cb)
- .catch(function (e) { console.error(url, e); });
- }
-
- function esc(s) {
- return String(s)
- .replace(/&/g, '&')
- .replace(/</g, '<')
- .replace(/>/g, '>')
- .replace(/"/g, '"');
- }
-
- function nowDbString() {
- var d = new Date();
- function pad(n) { return n < 10 ? '0' + n : n; }
- return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' +
- pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
- }
-
- function ageInDays(cellEnteredAt) {
- if (!cellEnteredAt) return 0;
- var entered = new Date(String(cellEnteredAt).replace(' ', 'T'));
- if (isNaN(entered.getTime())) return 0;
- return (Date.now() - entered.getTime()) / (24 * 60 * 60 * 1000);
- }
-
- function formatAge(cellEnteredAt) {
- var days = ageInDays(cellEnteredAt);
- if (!cellEnteredAt || isNaN(days)) return null;
- if (days < 0) days = 0;
-
- var totalDays = Math.floor(days);
- if (totalDays >= 1) {
- return totalDays + ' day' + (totalDays === 1 ? '' : 's');
- }
-
- var totalHours = Math.floor(days * 24);
- if (totalHours >= 1) {
- return totalHours + ' hour' + (totalHours === 1 ? '' : 's');
- }
-
- return 'less than an hour';
- }
-
- function effectiveCardAgeSettings(card) {
- var colSettings = columnAgeSettings[String(card.column_id)] || { showCardAge: false, cardAgeWarningDays: 0 };
- var laneSettings = laneAgeSettings[String(card.swim_lane_id)] || { showCardAge: false, cardAgeWarningDays: 0 };
-
- var showCardAge = colSettings.showCardAge || laneSettings.showCardAge;
-
- var warningDays = 0;
- if (colSettings.showCardAge) {
- warningDays = colSettings.cardAgeWarningDays;
- } else if (laneSettings.showCardAge) {
- warningDays = laneSettings.cardAgeWarningDays;
- }
-
- return { showCardAge: showCardAge, cardAgeWarningDays: warningDays };
- }
-
- function applyCardAge(el, card) {
- var settings = effectiveCardAgeSettings(card);
-
- if (settings.showCardAge) {
- var age = formatAge(card.cell_entered_at);
- if (age) {
- el.title = 'In this column/lane for ' + age;
- } else {
- el.removeAttribute('title');
- }
- } else {
- el.removeAttribute('title');
- }
-
- var isOverdue = settings.cardAgeWarningDays > 0 &&
- ageInDays(card.cell_entered_at) >= settings.cardAgeWarningDays;
- el.classList.toggle('kanban-card-overdue', isOverdue);
- }
-
- function refreshAllCardAges() {
- document.querySelectorAll('.kanban-card').forEach(function (el) {
- var card = KANBAN.cards.find(function (c) { return String(c.id) === String(el.dataset.id); });
- if (card) applyCardAge(el, card);
- });
- }
-
- function applyGridTemplate() {
- var grid = document.getElementById('kanban-grid');
- var colHs = grid.querySelectorAll('.kanban-col-header');
- var cols = '240px';
- colHs.forEach(function () { cols += ' 230px'; });
- grid.style.gridTemplateColumns = cols;
- }
-
- function countCardsInColumn(colId) {
- var count = 0;
- KANBAN.cards.forEach(function (c) {
- if (String(c.column_id) === String(colId)) count++;
- });
- return count;
- }
-
- function refreshColumnCount(colId) {
- var header = document.querySelector('.kanban-col-header[data-col-id="' + colId + '"]');
- if (!header) return;
- var badge = header.querySelector('.col-count-badge');
- if (!columnShowCount[String(colId)]) {
- if (badge) badge.remove();
- return;
- }
- if (!badge) {
- badge = document.createElement('span');
- badge.className = 'col-count-badge';
- header.appendChild(badge);
- }
- badge.textContent = countCardsInColumn(colId);
- }
-
- function initColumnCounts() {
- KANBAN_COLS.forEach(function (col) {
- columnShowCount[String(col.id)] = !!col.show_card_count;
- refreshColumnCount(col.id);
- });
- }
-
- function countCardsInLane(laneId) {
- var count = 0;
- KANBAN.cards.forEach(function (c) {
- if (String(c.swim_lane_id) === String(laneId)) count++;
- });
- return count;
- }
-
- function refreshLaneCount(laneId) {
- var header = document.querySelector('.kanban-lane-header[data-lane-id="' + laneId + '"]');
- if (!header) return;
- var badge = header.querySelector('.lane-count-badge');
- if (!laneShowCount[String(laneId)]) {
- if (badge) badge.remove();
- return;
- }
- if (!badge) {
- badge = document.createElement('span');
- badge.className = 'lane-count-badge';
- header.appendChild(badge);
- }
- badge.textContent = countCardsInLane(laneId);
- }
-
- function initLaneCounts() {
- KANBAN_LANES.forEach(function (lane) {
- laneShowCount[String(lane.id)] = !!lane.show_card_count;
- refreshLaneCount(lane.id);
- });
- }
-
- function refreshColumnExport(colId) {
- var header = document.querySelector('.kanban-col-header[data-col-id="' + colId + '"]');
- if (!header) return;
- var btn = header.querySelector('.col-export-btn');
- if (columnShowExport[String(colId)]) {
- if (!btn) {
- btn = document.createElement('a');
- btn.className = 'col-export-btn';
- btn.href = '/columns/' + colId + '/export';
- btn.title = 'Export column to CSV';
- btn.innerHTML = '<i class="bi bi-download"></i>';
- header.appendChild(btn);
- }
- } else {
- if (btn) btn.remove();
- }
- }
-
- function refreshLaneExport(laneId) {
- var header = document.querySelector('.kanban-lane-header[data-lane-id="' + laneId + '"]');
- if (!header) return;
- var btn = header.querySelector('.lane-export-btn');
- if (laneShowExport[String(laneId)]) {
- if (!btn) {
- btn = document.createElement('a');
- btn.className = 'lane-export-btn';
- btn.href = '/swimlanes/' + laneId + '/export';
- btn.title = 'Export swim lane to CSV';
- btn.innerHTML = '<i class="bi bi-download"></i>';
- header.appendChild(btn);
- }
- } else {
- if (btn) btn.remove();
- }
- }
-
- function initExportButtons() {
- KANBAN_COLS.forEach(function (col) {
- columnShowExport[String(col.id)] = !!col.show_export_button;
- refreshColumnExport(col.id);
- });
- KANBAN_LANES.forEach(function (lane) {
- laneShowExport[String(lane.id)] = !!lane.show_export_button;
- refreshLaneExport(lane.id);
- });
- }
-
- function initCardAgeSettings() {
- KANBAN_COLS.forEach(function (col) {
- columnAgeSettings[String(col.id)] = {
- showCardAge: !!col.show_card_age,
- cardAgeWarningDays: parseInt(col.card_age_warning_days, 10) || 0
- };
- });
- KANBAN_LANES.forEach(function (lane) {
- laneAgeSettings[String(lane.id)] = {
- showCardAge: !!lane.show_card_age,
- cardAgeWarningDays: parseInt(lane.card_age_warning_days, 10) || 0
- };
- });
- }
-
- function loadCollapsedLaneIds() {
- var laneMap = {};
- try {
- var raw = window.localStorage.getItem(laneCollapseStorageKey);
- if (!raw) return laneMap;
- var arr = JSON.parse(raw);
- if (!Array.isArray(arr)) return laneMap;
- arr.forEach(function (laneId) {
- laneMap[String(laneId)] = true;
- });
- } catch (e) {
- console.warn('Failed to load lane collapse state', e);
- }
- return laneMap;
- }
-
- function saveCollapsedLaneIds() {
- try {
- window.localStorage.setItem(laneCollapseStorageKey, JSON.stringify(Object.keys(collapsedLaneIds)));
- } catch (e) {
- console.warn('Failed to save lane collapse state', e);
- }
- }
-
- function setLaneCollapsed(laneId, isCollapsed) {
- var laneKey = String(laneId);
- var header = document.querySelector('.kanban-lane-header[data-lane-id="' + laneKey + '"]');
- if (!header) return;
-
- var laneCells = document.querySelectorAll('.kanban-cell[data-lane-id="' + laneKey + '"]');
- header.classList.toggle('lane-collapsed', isCollapsed);
- laneCells.forEach(function (cell) {
- cell.classList.toggle('lane-collapsed', isCollapsed);
- });
-
- var toggleBtn = header.querySelector('.lane-toggle');
- if (toggleBtn) {
- toggleBtn.setAttribute('aria-expanded', isCollapsed ? 'false' : 'true');
- toggleBtn.title = isCollapsed ? 'Expand swim lane' : 'Collapse swim lane';
- toggleBtn.setAttribute('aria-label', toggleBtn.title);
- }
-
- if (isCollapsed) {
- collapsedLaneIds[laneKey] = true;
- } else {
- delete collapsedLaneIds[laneKey];
- }
- saveCollapsedLaneIds();
- }
-
- function toggleLaneCollapsed(laneId) {
- var laneKey = String(laneId);
- setLaneCollapsed(laneKey, !collapsedLaneIds[laneKey]);
- }
-
- function bindLaneHeaderToggle(headerEl) {
- if (!headerEl) return;
- var toggleBtn = headerEl.querySelector('.lane-toggle');
- if (!toggleBtn) return;
-
- if (!toggleBtn.dataset.boundToggle) {
- toggleBtn.addEventListener('click', function (evt) {
- evt.preventDefault();
- evt.stopPropagation();
- toggleLaneCollapsed(headerEl.dataset.laneId);
- });
- toggleBtn.dataset.boundToggle = '1';
- }
- }
-
- function initLaneHeaderToggles() {
- document.querySelectorAll('.kanban-lane-header').forEach(function (headerEl) {
- bindLaneHeaderToggle(headerEl);
- if (collapsedLaneIds[String(headerEl.dataset.laneId)]) {
- setLaneCollapsed(headerEl.dataset.laneId, true);
- }
- });
- }
-
- function cardBodyHtml(card) {
- var html = '<div class="card-headline">' +
- '<span class="card-job-number">' + esc(card.job_number || '') + '</span>';
-
- if (card.customer_name) {
- html += '<span class="card-customer">' + esc(card.customer_name) + '</span>';
- }
-
- html += '</div>';
-
- return html;
- }
-
- function buildCardSearchText(card) {
- return [
- card.job_number || '',
- card.job_name || '',
- card.customer_name || '',
- card.notes || ''
- ].join(' ').toLowerCase();
- }
-
- function buildCardEl(card) {
- var div = document.createElement('div');
- div.className = 'kanban-card';
- div.dataset.id = card.id;
- div.dataset.columnId = card.column_id;
- div.dataset.laneId = card.swim_lane_id;
- div.dataset.searchText = buildCardSearchText(card);
- div.innerHTML = cardBodyHtml(card);
- applyCardAge(div, card);
- div.addEventListener('click', function () {
- var c = KANBAN.cards.find(function (x) { return String(x.id) === String(div.dataset.id); });
- if (!c) return;
- 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);
- });
- return div;
- }
-
- function renderCards() {
- KANBAN.cards.forEach(function (card) {
- var cell = document.querySelector(
- '.kanban-cell[data-col-id="' + card.column_id + '"][data-lane-id="' + card.swim_lane_id + '"]'
- );
- if (cell) {
- cell.appendChild(buildCardEl(card));
- }
- });
- applyCardFilter();
- }
-
- function applyCardFilter() {
- var activeQuery = searchState.query;
- document.querySelectorAll('.kanban-card').forEach(function (el) {
- var searchableText = (el.dataset.searchText || '').toLowerCase();
- var isMatch = activeQuery === '' || searchableText.indexOf(activeQuery) > -1;
- el.classList.toggle('kanban-card-hidden', !isMatch);
- });
- }
-
- function initJobSearch() {
- var searchInput = document.getElementById('job-search-input');
- if (!searchInput) return;
-
- searchInput.addEventListener('input', function () {
- searchState.query = String(searchInput.value || '').toLowerCase().trim();
- applyCardFilter();
- });
- }
-
- function handleDragEnd(evt) {
- var cardId = evt.item.dataset.id;
- var newColId = evt.to.dataset.colId;
- var newLaneId = evt.to.dataset.laneId;
- var newPos = evt.newIndex;
-
- var siblings = [];
- evt.to.querySelectorAll('.kanban-card').forEach(function (el) {
- siblings.push(el.dataset.id);
- });
-
- var card = KANBAN.cards.find(function (c) { return String(c.id) === String(cardId); });
- var oldColId = card ? card.column_id : null;
- var oldLaneId = card ? card.swim_lane_id : null;
- if (card) {
- if (String(oldColId) !== String(newColId) || String(oldLaneId) !== String(newLaneId)) {
- card.cell_entered_at = nowDbString();
- }
- card.column_id = parseInt(newColId, 10);
- card.swim_lane_id = parseInt(newLaneId, 10);
- card.position = newPos;
- applyCardAge(evt.item, card);
- }
-
- evt.item.dataset.columnId = newColId;
- evt.item.dataset.laneId = newLaneId;
-
- if (oldColId !== null && String(oldColId) !== String(newColId)) {
- refreshColumnCount(oldColId);
- }
- refreshColumnCount(newColId);
-
- if (oldLaneId !== null && String(oldLaneId) !== String(newLaneId)) {
- refreshLaneCount(oldLaneId);
- }
- refreshLaneCount(newLaneId);
-
- post('/cards/' + cardId + '/move', {
- column_id: newColId,
- swim_lane_id: newLaneId,
- position: newPos,
- sibling_ids: siblings.join(',')
- }, function (res) {
- if (!res.ok) console.error('Move failed', res);
- });
- }
-
- function createCellSortable(cell) {
- Sortable.create(cell, {
- group: 'cards',
- animation: 150,
- ghostClass: 'sortable-ghost',
- chosenClass: 'sortable-chosen',
- handle: '.kanban-card',
- delayOnTouchOnly: true,
- delay: 120,
- touchStartThreshold: 3,
- fallbackTolerance: 4,
- scroll: true,
- bubbleScroll: true,
- scrollSensitivity: 140,
- scrollSpeed: 32,
- onStart: function () { startEdgeAutoScroll(); },
- onEnd: function (evt) {
- stopEdgeAutoScroll();
- handleDragEnd(evt);
- }
- });
- }
-
- function updatePointerFromEvent(evt) {
- if (!evt) return;
- if (evt.touches && evt.touches.length > 0) {
- dragState.x = evt.touches[0].clientX;
- dragState.y = evt.touches[0].clientY;
- return;
- }
- if (evt.clientX !== undefined && evt.clientY !== undefined) {
- dragState.x = evt.clientX;
- dragState.y = evt.clientY;
- }
- }
-
- function edgeScrollStep() {
- if (!dragState.active) return;
-
- var wrapper = document.querySelector('.kanban-wrapper');
- if (wrapper) {
- var rect = wrapper.getBoundingClientRect();
- var edge = 110;
- var maxStep = 40;
- var dx = 0;
- var dy = 0;
-
- if (dragState.x > 0 && dragState.x < rect.left + edge) {
- dx = -Math.min(maxStep, Math.ceil((rect.left + edge - dragState.x) / 3));
- } else if (dragState.x > rect.right - edge && dragState.x < rect.right + edge) {
- dx = Math.min(maxStep, Math.ceil((dragState.x - (rect.right - edge)) / 3));
- }
-
- if (dragState.y > 0 && dragState.y < rect.top + edge) {
- dy = -Math.min(maxStep, Math.ceil((rect.top + edge - dragState.y) / 3));
- } else if (dragState.y > rect.bottom - edge && dragState.y < rect.bottom + edge) {
- dy = Math.min(maxStep, Math.ceil((dragState.y - (rect.bottom - edge)) / 3));
- }
-
- if (dx !== 0) wrapper.scrollLeft += dx;
- if (dy !== 0) wrapper.scrollTop += dy;
- }
-
- dragState.rafId = window.requestAnimationFrame(edgeScrollStep);
- }
-
- function startEdgeAutoScroll() {
- if (dragState.active) return;
- dragState.active = true;
- document.addEventListener('pointermove', updatePointerFromEvent, { passive: true });
- document.addEventListener('touchmove', updatePointerFromEvent, { passive: true });
- document.addEventListener('dragover', updatePointerFromEvent, { passive: true });
- dragState.rafId = window.requestAnimationFrame(edgeScrollStep);
- }
-
- function stopEdgeAutoScroll() {
- if (!dragState.active) return;
- dragState.active = false;
- if (dragState.rafId) {
- window.cancelAnimationFrame(dragState.rafId);
- dragState.rafId = 0;
- }
- document.removeEventListener('pointermove', updatePointerFromEvent);
- document.removeEventListener('touchmove', updatePointerFromEvent);
- document.removeEventListener('dragover', updatePointerFromEvent);
- }
-
- function initSortables() {
- document.querySelectorAll('.kanban-cell').forEach(createCellSortable);
- }
-
- document.getElementById('btn-add-card').addEventListener('click', function () {
- window.KanbanModal.openCreate(boardId, null, null);
- });
-
- window.KanbanBoard = {
- onCardCreated: function (card) {
- KANBAN.cards.push(card);
- var cell = document.querySelector(
- '.kanban-cell[data-col-id="' + card.column_id + '"][data-lane-id="' + card.swim_lane_id + '"]'
- );
- if (cell) {
- cell.appendChild(buildCardEl(card));
- }
- applyCardFilter();
- refreshColumnCount(card.column_id);
- refreshLaneCount(card.swim_lane_id);
- },
- onCardUpdated: function (id, data) {
- var card = KANBAN.cards.find(function (c) { return String(c.id) === String(id); });
- if (card) {
- card.job_number = data.job_number || '';
- card.job_name = data.job_name || '';
- card.customer_name = data.customer_name || '';
- card.delivery_date = data.delivery_date || null;
- card.quantity = data.quantity || '';
- card.notes = data.notes || '';
- card.full_note = data.full_note !== undefined ? data.full_note : (card.full_note || '');
- }
- var el = document.querySelector('.kanban-card[data-id="' + id + '"]');
- if (el && card) {
- el.innerHTML = cardBodyHtml(card);
- el.dataset.searchText = buildCardSearchText(card);
- }
- applyCardFilter();
- },
- onCardDeleted: function (id) {
- var card = KANBAN.cards.find(function (c) { return String(c.id) === String(id); });
- var colId = card ? card.column_id : null;
- var laneId = card ? card.swim_lane_id : null;
- KANBAN.cards = KANBAN.cards.filter(function (c) { return String(c.id) !== String(id); });
- var el = document.querySelector('.kanban-card[data-id="' + id + '"]');
- if (el) el.remove();
- applyCardFilter();
- if (colId !== null) refreshColumnCount(colId);
- if (laneId !== null) refreshLaneCount(laneId);
- },
- addColumn: function (col) {
- var grid = document.getElementById('kanban-grid');
-
- var headers = grid.querySelectorAll('.kanban-col-header');
- var refNode = headers.length ? headers[headers.length - 1].nextSibling : null;
-
- var hdr = document.createElement('div');
- hdr.className = 'kanban-col-header';
- hdr.dataset.colId = col.id;
- hdr.innerHTML = '<span class="col-label">' + esc(col.name) + '</span>';
- grid.insertBefore(hdr, refNode);
-
- columnShowCount[String(col.id)] = false;
- columnShowExport[String(col.id)] = false;
- columnAgeSettings[String(col.id)] = { showCardAge: false, cardAgeWarningDays: 0 };
-
- var laneHeaders = grid.querySelectorAll('.kanban-lane-header');
- laneHeaders.forEach(function (lh) {
- var laneId = lh.dataset.laneId;
- var cell = document.createElement('div');
- cell.className = 'kanban-cell';
- cell.dataset.colId = col.id;
- cell.dataset.laneId = laneId;
- var row = lh.parentNode;
- row.appendChild(cell);
- createCellSortable(cell);
- });
-
- applyGridTemplate();
- },
- removeColumn: function (colId) {
- document.querySelector('.kanban-col-header[data-col-id="' + colId + '"]').remove();
- document.querySelectorAll('.kanban-cell[data-col-id="' + colId + '"]').forEach(function (el) { el.remove(); });
- KANBAN.cards = KANBAN.cards.filter(function (c) { return String(c.column_id) !== String(colId); });
- delete columnShowCount[String(colId)];
- delete columnShowExport[String(colId)];
- delete columnAgeSettings[String(colId)];
- applyGridTemplate();
- },
- addLane: function (lane) {
- var grid = document.getElementById('kanban-grid');
- var colHeaders = grid.querySelectorAll('.kanban-col-header');
-
- var lh = document.createElement('div');
- lh.className = 'kanban-lane-header';
- lh.dataset.laneId = lane.id;
- lh.innerHTML =
- '<button type="button" class="lane-toggle" title="Collapse swim lane" aria-label="Collapse swim lane" aria-expanded="true">' +
- '<i class="bi bi-chevron-down" aria-hidden="true"></i>' +
- '</button>' +
- '<span class="lane-label">' + esc(lane.name) + '</span>';
- grid.appendChild(lh);
- bindLaneHeaderToggle(lh);
-
- laneShowCount[String(lane.id)] = false;
- laneShowExport[String(lane.id)] = false;
- laneAgeSettings[String(lane.id)] = { showCardAge: false, cardAgeWarningDays: 0 };
-
- colHeaders.forEach(function (ch) {
- var cell = document.createElement('div');
- cell.className = 'kanban-cell';
- cell.dataset.colId = ch.dataset.colId;
- cell.dataset.laneId = lane.id;
- grid.appendChild(cell);
- createCellSortable(cell);
- });
-
- if (collapsedLaneIds[String(lane.id)]) {
- setLaneCollapsed(lane.id, true);
- }
-
- applyGridTemplate();
- },
- removeLane: function (laneId) {
- document.querySelector('.kanban-lane-header[data-lane-id="' + laneId + '"]').remove();
- document.querySelectorAll('.kanban-cell[data-lane-id="' + laneId + '"]').forEach(function (el) { el.remove(); });
- KANBAN.cards = KANBAN.cards.filter(function (c) { return String(c.swim_lane_id) !== String(laneId); });
- delete laneShowCount[String(laneId)];
- delete laneShowExport[String(laneId)];
- delete laneAgeSettings[String(laneId)];
- if (collapsedLaneIds[String(laneId)]) {
- delete collapsedLaneIds[String(laneId)];
- saveCollapsedLaneIds();
- }
- },
- renameColumn: function (colId, name) {
- var hdr = document.querySelector('.kanban-col-header[data-col-id="' + colId + '"] .col-label');
- if (hdr) hdr.textContent = name;
- },
- renameLane: function (laneId, name) {
- var hdr = document.querySelector('.kanban-lane-header[data-lane-id="' + laneId + '"] .lane-label');
- if (hdr) hdr.textContent = name;
- },
- setColumnShowCount: function (colId, show) {
- columnShowCount[String(colId)] = show;
- refreshColumnCount(colId);
- },
- setLaneShowCount: function (laneId, show) {
- laneShowCount[String(laneId)] = show;
- refreshLaneCount(laneId);
- },
- setColumnShowExport: function (colId, show) {
- columnShowExport[String(colId)] = !!show;
- refreshColumnExport(colId);
- },
- setLaneShowExport: function (laneId, show) {
- laneShowExport[String(laneId)] = !!show;
- refreshLaneExport(laneId);
- },
- setColumnCardAge: function (colId, showCardAge, cardAgeWarningDays) {
- columnAgeSettings[String(colId)] = {
- showCardAge: !!showCardAge,
- cardAgeWarningDays: parseInt(cardAgeWarningDays, 10) || 0
- };
- refreshAllCardAges();
- },
- setLaneCardAge: function (laneId, showCardAge, cardAgeWarningDays) {
- laneAgeSettings[String(laneId)] = {
- showCardAge: !!showCardAge,
- cardAgeWarningDays: parseInt(cardAgeWarningDays, 10) || 0
- };
- refreshAllCardAges();
- }
- };
-
- applyGridTemplate();
- initCardAgeSettings();
- initExportButtons();
- renderCards();
- initSortables();
- initJobSearch();
- initLaneHeaderToggles();
- initColumnCounts();
- initLaneCounts();
- })();
|