/* kanban-settings.js — settings panel: add/rename/delete/reorder columns and lanes */
(function () {
'use strict';
var boardId = KANBAN.boardId;
var panel = document.getElementById('settings-panel');
var overlay = document.getElementById('settings-overlay');
/* ── Panel open/close ────────────────────────────────────── */
document.getElementById('btn-settings').addEventListener('click', openPanel);
document.getElementById('btn-close-settings').addEventListener('click', closePanel);
overlay.addEventListener('click', closePanel);
function openPanel() {
panel.classList.add('open');
overlay.classList.remove('d-none');
}
function closePanel() {
panel.classList.remove('open');
overlay.classList.add('d-none');
}
/* ── Helpers ─────────────────────────────────────────────── */
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 postJson(url, payload, cb) {
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}).then(function (r) { return r.json(); }).then(cb)
.catch(function (e) { console.error(url, e); });
}
function collectOrder(listId) {
return Array.from(document.querySelectorAll('#' + listId + ' li')).map(function (li, idx) {
return { id: parseInt(li.dataset.id), position: idx };
});
}
function buildListItem(id, name, editClass, deleteClass, labelClass, countToggle, agePrefix) {
var li = document.createElement('li');
li.className = 'list-group-item py-2';
li.dataset.id = id;
li.innerHTML =
'
' +
'
' +
'
' + esc(name) + '' +
(countToggle ?
'
' +
'' +
'
' : '') +
'
' +
'
' +
'
' +
'
' +
'' +
'
' +
'' +
'' +
'
' +
'
' +
'' +
'days' +
'
' +
'
Overdue after this many days. 0 = no overdue marking.
' +
'
';
return li;
}
/* ── Card age settings (per column / per swim lane) ────────── */
function bindAgeSettings(li, prefix, urlBase, setter) {
var toggleBtn = li.querySelector('.btn-toggle-' + prefix + '-age');
var settingsDiv = li.querySelector('.' + prefix + '-age-settings');
if (toggleBtn && settingsDiv) {
toggleBtn.addEventListener('click', function () {
settingsDiv.classList.toggle('d-none');
});
}
var ageToggle = li.querySelector('.' + prefix + '-age-toggle');
var ageDays = li.querySelector('.' + prefix + '-age-days');
if (!ageToggle || !ageDays) return;
function save() {
var show = ageToggle.checked;
var days = parseInt(ageDays.value, 10);
if (isNaN(days) || days < 0) days = 0;
ageDays.value = days;
post(urlBase + li.dataset.id + '/card-age-settings', {
show_card_age: show ? '1' : '0',
card_age_warning_days: days
}, function (res) {
if (!res.ok) {
alert(res.error || 'Update failed');
return;
}
setter(li.dataset.id, res.show_card_age, res.card_age_warning_days);
});
}
ageToggle.addEventListener('change', save);
ageDays.addEventListener('change', save);
}
function esc(s) {
return String(s)
.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
}
/* ── Sortable reorder ─────────────────────────────────────── */
function initSortable(listId, reorderUrl) {
var el = document.getElementById(listId);
Sortable.create(el, {
handle: '.drag-handle',
animation: 150,
onEnd: function () {
postJson(reorderUrl, collectOrder(listId), function (res) {
if (!res.ok) console.error('Reorder failed', res);
});
}
});
}
initSortable('col-list', '/columns/reorder');
initSortable('lane-list', '/swimlanes/reorder');
/* ═══════════════════════════════════════════════════════════
COLUMNS
═══════════════════════════════════════════════════════════ */
/* ── Add column ───────────────────────────────────────────── */
document.getElementById('btn-add-column').addEventListener('click', function () {
document.getElementById('col-add-form').classList.remove('d-none');
document.getElementById('col-add-input').focus();
});
document.getElementById('btn-col-add-cancel').addEventListener('click', function () {
document.getElementById('col-add-form').classList.add('d-none');
document.getElementById('col-add-input').value = '';
});
document.getElementById('btn-col-add-save').addEventListener('click', function () {
var name = document.getElementById('col-add-input').value.trim();
if (!name) return;
post('/columns', { board_id: boardId, name: name }, function (res) {
if (!res.ok) { alert(res.error || 'Failed'); return; }
document.getElementById('col-add-form').classList.add('d-none');
document.getElementById('col-add-input').value = '';
var li = buildListItem(res.id, res.name, 'btn-edit-col', 'btn-delete-col', 'col-label-text', true, 'col');
document.getElementById('col-list').appendChild(li);
bindColItem(li);
window.KanbanBoard.addColumn(res);
});
});
/* ── Bind edit/delete on existing column items ─────────────── */
function bindColItem(li) {
li.querySelector('.btn-edit-col').addEventListener('click', function () {
startRename(li, '.col-label-text', function (newName, done) {
post('/columns/' + li.dataset.id, { name: newName }, function (res) {
if (res.ok) {
done(true);
window.KanbanBoard.renameColumn(li.dataset.id, newName);
} else {
done(false);
alert(res.error || 'Rename failed');
}
});
});
});
li.querySelector('.btn-delete-col').addEventListener('click', function () {
if (!confirm('Delete this column and all its cards?')) return;
post('/columns/' + li.dataset.id + '/delete', {}, function (res) {
if (res.ok) {
window.KanbanBoard.removeColumn(li.dataset.id);
li.remove();
} else {
alert(res.error || 'Delete failed');
}
});
});
var countToggle = li.querySelector('.col-count-toggle');
if (countToggle) {
countToggle.addEventListener('change', function () {
var show = countToggle.checked;
post('/columns/' + li.dataset.id + '/toggle-count', { show_card_count: show ? '1' : '0' }, function (res) {
if (res.ok) {
window.KanbanBoard.setColumnShowCount(li.dataset.id, show);
} else {
countToggle.checked = !show;
alert(res.error || 'Update failed');
}
});
});
}
bindAgeSettings(li, 'col', '/columns/', window.KanbanBoard.setColumnCardAge);
}
document.querySelectorAll('#col-list li').forEach(bindColItem);
/* ═══════════════════════════════════════════════════════════
SWIM LANES
═══════════════════════════════════════════════════════════ */
/* ── Add lane ─────────────────────────────────────────────── */
document.getElementById('btn-add-lane').addEventListener('click', function () {
document.getElementById('lane-add-form').classList.remove('d-none');
document.getElementById('lane-add-input').focus();
});
document.getElementById('btn-lane-add-cancel').addEventListener('click', function () {
document.getElementById('lane-add-form').classList.add('d-none');
document.getElementById('lane-add-input').value = '';
});
document.getElementById('btn-lane-add-save').addEventListener('click', function () {
var name = document.getElementById('lane-add-input').value.trim();
if (!name) return;
post('/swimlanes', { board_id: boardId, name: name }, function (res) {
if (!res.ok) { alert(res.error || 'Failed'); return; }
document.getElementById('lane-add-form').classList.add('d-none');
document.getElementById('lane-add-input').value = '';
var li = buildListItem(res.id, res.name, 'btn-edit-lane', 'btn-delete-lane', 'lane-label-text', false, 'lane');
document.getElementById('lane-list').appendChild(li);
bindLaneItem(li);
window.KanbanBoard.addLane(res);
});
});
/* ── Bind edit/delete on existing lane items ──────────────── */
function bindLaneItem(li) {
li.querySelector('.btn-edit-lane').addEventListener('click', function () {
startRename(li, '.lane-label-text', function (newName, done) {
post('/swimlanes/' + li.dataset.id, { name: newName }, function (res) {
if (res.ok) {
done(true);
window.KanbanBoard.renameLane(li.dataset.id, newName);
} else {
done(false);
alert(res.error || 'Rename failed');
}
});
});
});
li.querySelector('.btn-delete-lane').addEventListener('click', function () {
if (!confirm('Delete this swim lane and all its cards?')) return;
post('/swimlanes/' + li.dataset.id + '/delete', {}, function (res) {
if (res.ok) {
window.KanbanBoard.removeLane(li.dataset.id);
li.remove();
} else {
alert(res.error || 'Delete failed');
}
});
});
bindAgeSettings(li, 'lane', '/swimlanes/', window.KanbanBoard.setLaneCardAge);
}
document.querySelectorAll('#lane-list li').forEach(bindLaneItem);
/* ── Inline rename helper ─────────────────────────────────── */
function startRename(li, labelSel, saveCb) {
var span = li.querySelector(labelSel);
var oldName = span.textContent.trim();
var input = document.createElement('input');
input.type = 'text';
input.className = 'form-control form-control-sm inline-rename flex-grow-1';
input.value = oldName;
span.replaceWith(input);
input.focus();
input.select();
function commit() {
var newName = input.value.trim();
if (!newName || newName === oldName) {
abort();
return;
}
saveCb(newName, function (ok) {
var replacement = document.createElement('span');
replacement.className = labelSel.replace('.', '') + ' flex-grow-1';
replacement.textContent = ok ? newName : oldName;
input.replaceWith(replacement);
});
}
function abort() {
var replacement = document.createElement('span');
replacement.className = labelSel.replace('.', '') + ' flex-grow-1';
replacement.textContent = oldName;
input.replaceWith(replacement);
}
input.addEventListener('blur', commit);
input.addEventListener('keydown', function (e) {
if (e.key === 'Enter') { e.preventDefault(); commit(); }
if (e.key === 'Escape') { e.preventDefault(); abort(); }
});
}
})();