KAOS/code/web/templates/layout.html
djuka aabdfa9e50 T10: Drag & Drop — premesti task prevlačenjem
- Sortable.js na Kanban board sa drag & drop
- Server-side validacija: allowedMoves mapa, isMoveAllowed()
- Zabranjeni potezi vraćaju 403 (ready→active, active→review)
- Toast notifikacije (zeleni uspeh, crveni greška)
- Ghost/chosen/drag CSS animacije
- Board auto-refresh posle svakog poteza
- 7 novih testova, 90 ukupno — svi prolaze
- T09 premešten u done/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:18:50 +00:00

90 lines
2.4 KiB
HTML

<!DOCTYPE html>
<html lang="sr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>KAOS Dashboard</title>
<link rel="stylesheet" href="/static/style.css">
<script src="/static/htmx.min.js"></script>
<script src="/static/sortable.min.js"></script>
</head>
<body>
<div class="header">
<h1>🔧 KAOS Dashboard</h1>
<span class="version">v0.2</span>
</div>
{{block "content" .}}{{end}}
<div id="task-detail"></div>
<div id="toast" class="toast"></div>
<script>
document.body.addEventListener('htmx:afterSwap', function(e) {
if (e.detail.target.id === 'task-detail') {
e.detail.target.classList.add('active');
}
});
function closeDetail() {
var el = document.getElementById('task-detail');
el.classList.remove('active');
el.innerHTML = '';
}
function showToast(msg, type) {
var toast = document.getElementById('toast');
toast.textContent = msg;
toast.className = 'toast toast-' + type + ' toast-show';
setTimeout(function() {
toast.className = 'toast';
}, 2000);
}
document.addEventListener('DOMContentLoaded', function() {
initSortable();
});
document.body.addEventListener('htmx:afterSwap', function(e) {
if (e.detail.target.id === 'board') {
initSortable();
}
});
function initSortable() {
document.querySelectorAll('.column-tasks').forEach(function(col) {
new Sortable(col, {
group: 'tasks',
animation: 150,
ghostClass: 'task-ghost',
chosenClass: 'task-chosen',
dragClass: 'task-drag',
filter: '.column-header',
onEnd: function(evt) {
var taskId = evt.item.dataset.id;
var toFolder = evt.to.dataset.folder;
var fromFolder = evt.from.dataset.folder;
if (fromFolder === toFolder) return;
fetch('/api/task/' + taskId + '/move?to=' + toFolder, {
method: 'POST'
})
.then(function(resp) {
if (!resp.ok) {
return resp.json().then(function(data) {
throw new Error(data.error || 'Greška');
});
}
showToast(taskId + ' → ' + toFolder, 'success');
htmx.ajax('GET', '/', {target: '#board', swap: 'outerHTML', select: '#board'});
})
.catch(function(err) {
showToast(err.message, 'error');
htmx.ajax('GET', '/', {target: '#board', swap: 'outerHTML', select: '#board'});
});
}
});
});
}
</script>
</body>
</html>