@cccarv82/freya 2.14.1 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/web-ui.css +457 -3
- package/cli/web-ui.js +494 -229
- package/cli/web.js +460 -52
- package/package.json +2 -2
- package/scripts/lib/DataLayer.js +10 -1
- package/scripts/lib/DataManager.js +1 -0
- package/scripts/lib/Embedder.js +5 -1
- package/templates/base/scripts/lib/DataLayer.js +11 -2
- package/templates/base/scripts/lib/DataManager.js +15 -14
- package/templates/base/scripts/lib/Embedder.js +5 -1
package/cli/web-ui.js
CHANGED
|
@@ -341,6 +341,11 @@
|
|
|
341
341
|
persistChatItem({ ts: Date.now(), role, markdown: !!opts.markdown, text: raw });
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
+
// show the thread now that it has content
|
|
345
|
+
thread.style.display = 'flex';
|
|
346
|
+
thread.style.padding = '12px';
|
|
347
|
+
thread.style.borderTop = '1px solid var(--border)';
|
|
348
|
+
|
|
344
349
|
// keep newest in view
|
|
345
350
|
try {
|
|
346
351
|
thread.scrollTop = thread.scrollHeight;
|
|
@@ -477,8 +482,8 @@
|
|
|
477
482
|
const thread = $('chatThread');
|
|
478
483
|
if (!thread) return;
|
|
479
484
|
const hasContent = thread.children.length > 0;
|
|
485
|
+
thread.style.display = hasContent ? 'flex' : 'none';
|
|
480
486
|
thread.style.padding = hasContent ? '12px' : '0';
|
|
481
|
-
thread.style.maxHeight = hasContent ? '280px' : '0';
|
|
482
487
|
thread.style.borderTop = hasContent ? '1px solid var(--border)' : 'none';
|
|
483
488
|
}
|
|
484
489
|
|
|
@@ -1431,6 +1436,7 @@
|
|
|
1431
1436
|
const health = $('railCompanion');
|
|
1432
1437
|
const graph = $('railGraph');
|
|
1433
1438
|
const docs = $('railDocs');
|
|
1439
|
+
const kanban = $('railKanban');
|
|
1434
1440
|
|
|
1435
1441
|
const curPage = (document.body && document.body.dataset) ? document.body.dataset.page : null;
|
|
1436
1442
|
const isDashboard = !curPage || curPage === 'dashboard';
|
|
@@ -1460,6 +1466,11 @@
|
|
|
1460
1466
|
if (curPage !== 'companion') window.location.href = '/companion';
|
|
1461
1467
|
};
|
|
1462
1468
|
}
|
|
1469
|
+
if (kanban) {
|
|
1470
|
+
kanban.onclick = () => {
|
|
1471
|
+
if (curPage !== 'kanban') window.location.href = '/kanban';
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1463
1474
|
if (tl) {
|
|
1464
1475
|
tl.onclick = () => {
|
|
1465
1476
|
if (curPage !== 'timeline') window.location.href = '/timeline';
|
|
@@ -1534,229 +1545,9 @@
|
|
|
1534
1545
|
return '#94a3b8';
|
|
1535
1546
|
}
|
|
1536
1547
|
|
|
1537
|
-
|
|
1538
|
-
const el = $('swimlaneContainer');
|
|
1539
|
-
if (!el) return;
|
|
1540
|
-
el.innerHTML = '';
|
|
1541
|
-
|
|
1542
|
-
// Update top-level summary chips
|
|
1543
|
-
const chips = $('focusSummaryChips');
|
|
1544
|
-
if (chips) {
|
|
1545
|
-
chips.innerHTML = '';
|
|
1546
|
-
if (blockers.length > 0) {
|
|
1547
|
-
const bc = document.createElement('span');
|
|
1548
|
-
bc.style.cssText = 'font-size:11px; font-weight:700; padding:2px 8px; border-radius:10px; background:rgba(239,68,68,0.12); color:#f87171; border:1px solid rgba(239,68,68,0.3);';
|
|
1549
|
-
bc.textContent = blockers.length + ' blocker' + (blockers.length > 1 ? 's' : '');
|
|
1550
|
-
chips.appendChild(bc);
|
|
1551
|
-
}
|
|
1552
|
-
if (tasks.length > 0) {
|
|
1553
|
-
const tc = document.createElement('span');
|
|
1554
|
-
tc.style.cssText = 'font-size:11px; font-weight:700; padding:2px 8px; border-radius:10px; background:rgba(34,197,94,0.10); color:#4ade80; border:1px solid rgba(34,197,94,0.25);';
|
|
1555
|
-
tc.textContent = tasks.length + ' task' + (tasks.length > 1 ? 's' : '');
|
|
1556
|
-
chips.appendChild(tc);
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
// Update progress bar (tasks completed today vs total — we show pending count)
|
|
1561
|
-
const progWrap = $('focusProgressWrap');
|
|
1562
|
-
const progBar = $('focusProgressBar');
|
|
1563
|
-
const progLabel = $('focusProgressLabel');
|
|
1564
|
-
if (progWrap) {
|
|
1565
|
-
if (tasks.length > 0 || blockers.length > 0) {
|
|
1566
|
-
progWrap.style.display = 'flex';
|
|
1567
|
-
// We only have pending tasks here; show blockers impact
|
|
1568
|
-
const blocker_pct = blockers.length === 0 ? 100 : Math.max(10, Math.round((1 - blockers.length / Math.max(tasks.length + blockers.length, 1)) * 100));
|
|
1569
|
-
if (progBar) progBar.style.width = blocker_pct + '%';
|
|
1570
|
-
if (progBar) progBar.style.background = blockers.length > 0 ? '#f97316' : 'var(--accent)';
|
|
1571
|
-
if (progLabel) progLabel.textContent = blockers.length > 0 ? blockers.length + ' blocking' : 'tudo livre';
|
|
1572
|
-
} else {
|
|
1573
|
-
progWrap.style.display = 'none';
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
const groups = {};
|
|
1578
|
-
const addGroup = (slug) => {
|
|
1579
|
-
const key = slug || 'Global / Sem Projeto';
|
|
1580
|
-
if (!groups[key]) groups[key] = { tasks: [], blockers: [] };
|
|
1581
|
-
return key;
|
|
1582
|
-
};
|
|
1583
|
-
|
|
1584
|
-
for (const t of tasks) groups[addGroup(t.projectSlug)].tasks.push(t);
|
|
1585
|
-
for (const b of blockers) groups[addGroup(b.projectSlug)].blockers.push(b);
|
|
1586
|
-
|
|
1587
|
-
const sortedKeys = Object.keys(groups).sort((a, b) => {
|
|
1588
|
-
if (a === 'Global / Sem Projeto') return 1;
|
|
1589
|
-
if (b === 'Global / Sem Projeto') return -1;
|
|
1590
|
-
return a.localeCompare(b);
|
|
1591
|
-
});
|
|
1592
|
-
sortedKeys.sort((a, b) => groups[b].blockers.length - groups[a].blockers.length);
|
|
1593
|
-
|
|
1594
|
-
if (sortedKeys.length === 0) {
|
|
1595
|
-
const empty = document.createElement('div');
|
|
1596
|
-
empty.style.cssText = 'display:flex; flex-direction:column; align-items:center; justify-content:center; padding:48px 24px; gap:8px; flex:1;';
|
|
1597
|
-
empty.innerHTML = '<div style="font-size:28px; line-height:1;">✅</div>'
|
|
1598
|
-
+ '<div style="font-size:14px; font-weight:600; color:var(--text);">Dia limpo!</div>'
|
|
1599
|
-
+ '<div style="font-size:12px; color:var(--muted); text-align:center;">Nenhuma tarefa pendente (DO_NOW) nem bloqueios em aberto. Use o campo acima para registrar o que estiver fazendo.</div>';
|
|
1600
|
-
el.appendChild(empty);
|
|
1601
|
-
return;
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
const createTaskRow = (t) => {
|
|
1605
|
-
const row = document.createElement('div');
|
|
1606
|
-
const pc = priColor(t.priority);
|
|
1607
|
-
row.style.cssText = 'display:flex; justify-content:space-between; gap:10px; align-items:center; padding:10px 16px; background:var(--bg); border-bottom:1px solid var(--border); transition:background 0.15s;';
|
|
1608
|
-
row.onmouseover = () => row.style.background = 'var(--paper)';
|
|
1609
|
-
row.onmouseout = () => row.style.background = 'var(--bg)';
|
|
1610
|
-
row.innerHTML =
|
|
1611
|
-
'<div style="display:flex; align-items:center; gap:10px; min-width:0; flex:1;">'
|
|
1612
|
-
+ '<div style="width:8px; height:8px; border-radius:50%; background:' + pc.dot + '; flex-shrink:0;" title="Prioridade: ' + escapeHtml(pc.label) + '"></div>'
|
|
1613
|
-
+ '<div style="min-width:0;">'
|
|
1614
|
-
+ '<div style="font-weight:600; font-size:13px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">' + escapeHtml(t.description || '') + '</div>'
|
|
1615
|
-
+ '<div style="font-size:11px; color:var(--muted); margin-top:2px;">'
|
|
1616
|
-
+ escapeHtml(pc.label)
|
|
1617
|
-
+ (t.category ? ' · <span style="font-family:var(--mono);">' + escapeHtml(t.category) + '</span>' : '')
|
|
1618
|
-
+ '</div>'
|
|
1619
|
-
+ '</div></div>'
|
|
1620
|
-
+ '<div class="task-actions" style="display:flex; gap:6px; flex-shrink:0; align-items:center;">'
|
|
1621
|
-
+ '<button class="btn small complete-btn" type="button" style="padding:3px 10px; font-size:11px;" title="Marcar como concluída">✓ Concluir</button>'
|
|
1622
|
-
+ '<button class="btn small edit-btn" type="button" style="padding:3px 8px; font-size:11px;">✎</button>'
|
|
1623
|
-
+ '</div>';
|
|
1624
|
-
row.querySelector('.edit-btn').onclick = () => editTask(t);
|
|
1625
|
-
|
|
1626
|
-
var attachCompleteHandler = function() {
|
|
1627
|
-
var cBtn = row.querySelector('.complete-btn');
|
|
1628
|
-
if (!cBtn) return;
|
|
1629
|
-
cBtn.onclick = function() {
|
|
1630
|
-
var actionsDiv = row.querySelector('.task-actions');
|
|
1631
|
-
actionsDiv.innerHTML =
|
|
1632
|
-
'<input type="text" class="comment-input" placeholder="Comentario (opcional)" '
|
|
1633
|
-
+ 'style="font-size:11px; padding:3px 8px; background:var(--bg); border:1px solid var(--border); '
|
|
1634
|
-
+ 'color:var(--text); width:180px; outline:none; font-family:var(--mono);" />'
|
|
1635
|
-
+ '<button class="btn small confirm-btn" type="button" style="padding:3px 10px; font-size:11px; '
|
|
1636
|
-
+ 'background:var(--accent); color:#000; font-weight:700;">Confirmar</button>'
|
|
1637
|
-
+ '<button class="btn small cancel-btn" type="button" style="padding:3px 8px; font-size:11px; '
|
|
1638
|
-
+ 'color:var(--muted);">\u2715</button>';
|
|
1639
|
-
|
|
1640
|
-
var input = actionsDiv.querySelector('.comment-input');
|
|
1641
|
-
input.focus();
|
|
1642
|
-
|
|
1643
|
-
var doComplete = async function() {
|
|
1644
|
-
var comment = input.value.trim();
|
|
1645
|
-
actionsDiv.querySelector('.confirm-btn').disabled = true;
|
|
1646
|
-
actionsDiv.querySelector('.confirm-btn').textContent = '\u2026';
|
|
1647
|
-
try {
|
|
1648
|
-
setPill('run', 'concluindo\u2026');
|
|
1649
|
-
var body = { dir: dirOrDefault(), id: t.id };
|
|
1650
|
-
if (comment) body.comment = comment;
|
|
1651
|
-
await api('/api/tasks/complete', body);
|
|
1652
|
-
row.style.opacity = '0.4';
|
|
1653
|
-
row.style.pointerEvents = 'none';
|
|
1654
|
-
await refreshToday();
|
|
1655
|
-
setPill('ok', 'conclu\u00edda');
|
|
1656
|
-
setTimeout(function() { setPill('ok', 'pronto'); }, 800);
|
|
1657
|
-
} catch (e) {
|
|
1658
|
-
actionsDiv.querySelector('.confirm-btn').disabled = false;
|
|
1659
|
-
actionsDiv.querySelector('.confirm-btn').textContent = 'Confirmar';
|
|
1660
|
-
setPill('err', 'falhou');
|
|
1661
|
-
}
|
|
1662
|
-
};
|
|
1663
|
-
|
|
1664
|
-
actionsDiv.querySelector('.confirm-btn').onclick = doComplete;
|
|
1665
|
-
input.onkeydown = function(e) { if (e.key === 'Enter') doComplete(); };
|
|
1666
|
-
actionsDiv.querySelector('.cancel-btn').onclick = function() {
|
|
1667
|
-
actionsDiv.innerHTML =
|
|
1668
|
-
'<button class="btn small complete-btn" type="button" style="padding:3px 10px; font-size:11px;" '
|
|
1669
|
-
+ 'title="Marcar como conclu\u00edda">\u2713 Concluir</button>'
|
|
1670
|
-
+ '<button class="btn small edit-btn" type="button" style="padding:3px 8px; font-size:11px;">\u270E</button>';
|
|
1671
|
-
actionsDiv.querySelector('.edit-btn').onclick = function() { editTask(t); };
|
|
1672
|
-
attachCompleteHandler();
|
|
1673
|
-
};
|
|
1674
|
-
};
|
|
1675
|
-
};
|
|
1676
|
-
attachCompleteHandler();
|
|
1677
|
-
return row;
|
|
1678
|
-
};
|
|
1679
|
-
|
|
1680
|
-
const createBlockerRow = (b) => {
|
|
1681
|
-
const row = document.createElement('div');
|
|
1682
|
-
const sev = String(b.severity || '').toUpperCase();
|
|
1683
|
-
const color = sevColor(sev);
|
|
1684
|
-
const when = fmtWhen(new Date(b.createdAt || Date.now()).getTime());
|
|
1685
|
-
row.style.cssText = 'display:flex; justify-content:space-between; gap:10px; align-items:center; padding:10px 16px; border-bottom:1px solid rgba(239,68,68,0.15); border-left:3px solid ' + color + '; background:rgba(239,68,68,0.04); transition:background 0.15s;';
|
|
1686
|
-
row.onmouseover = () => row.style.background = 'rgba(239,68,68,0.08)';
|
|
1687
|
-
row.onmouseout = () => row.style.background = 'rgba(239,68,68,0.04)';
|
|
1688
|
-
row.innerHTML =
|
|
1689
|
-
'<div style="display:flex; align-items:flex-start; gap:10px; min-width:0; flex:1;">'
|
|
1690
|
-
+ '<div style="flex-shrink:0; margin-top:1px;">'
|
|
1691
|
-
+ '<span style="font-size:10px; font-weight:800; letter-spacing:0.5px; padding:2px 6px; border-radius:4px; background:' + color + '22; color:' + color + '; border:1px solid ' + color + '44;">' + escapeHtml(sev || 'BLOCKER') + '</span>'
|
|
1692
|
-
+ '</div>'
|
|
1693
|
-
+ '<div style="min-width:0; flex:1;">'
|
|
1694
|
-
+ '<div style="font-weight:600; font-size:13px; color:var(--text);">' + escapeHtml(b.title || '') + '</div>'
|
|
1695
|
-
+ '<div style="font-size:11px; color:var(--muted); margin-top:3px;">🕐 ' + escapeHtml(when) + '</div>'
|
|
1696
|
-
+ '</div></div>'
|
|
1697
|
-
+ '<button class="btn small edit-btn" type="button" style="padding:3px 8px; font-size:11px; flex-shrink:0; border-color:' + color + '66; color:' + color + ';">✎</button>';
|
|
1698
|
-
row.querySelector('.edit-btn').onclick = () => editBlocker(b);
|
|
1699
|
-
return row;
|
|
1700
|
-
};
|
|
1701
|
-
|
|
1702
|
-
for (const key of sortedKeys) {
|
|
1703
|
-
const g = groups[key];
|
|
1704
|
-
const bCount = g.blockers.length;
|
|
1705
|
-
const tCount = g.tasks.length;
|
|
1706
|
-
const hasBlockers = bCount > 0;
|
|
1707
|
-
|
|
1708
|
-
const swimlane = document.createElement('div');
|
|
1709
|
-
swimlane.style.cssText = 'border-bottom:1px solid var(--border); overflow:hidden;';
|
|
1710
|
-
|
|
1711
|
-
const head = document.createElement('div');
|
|
1712
|
-
head.style.cssText = 'display:flex; justify-content:space-between; align-items:center; cursor:pointer; padding:9px 16px; background:var(--bg2); transition:background 0.15s;'
|
|
1713
|
-
+ (hasBlockers ? 'border-left:3px solid #f97316;' : 'border-left:3px solid transparent;');
|
|
1714
|
-
head.onmouseover = () => head.style.background = 'var(--paper2)';
|
|
1715
|
-
head.onmouseout = () => head.style.background = 'var(--bg2)';
|
|
1716
|
-
|
|
1717
|
-
head.innerHTML = `
|
|
1718
|
-
<div style="display:flex; align-items:center; gap:8px; min-width:0;">
|
|
1719
|
-
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0; opacity:0.5; transition:transform 0.2s;"><polyline points="6 9 12 15 18 9"></polyline></svg>
|
|
1720
|
-
<span style="font-family:var(--mono); font-size:12px; font-weight:700; color:var(--accent); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${escapeHtml(key)}</span>
|
|
1721
|
-
</div>
|
|
1722
|
-
<div style="display:flex; gap:5px; align-items:center; flex-shrink:0;">
|
|
1723
|
-
${bCount > 0 ? `<span style="font-size:11px; font-weight:700; padding:2px 7px; border-radius:10px; background:rgba(239,68,68,0.12); color:#f87171; border:1px solid rgba(239,68,68,0.3);">⛔ ${bCount}</span>` : ''}
|
|
1724
|
-
${tCount > 0 ? `<span style="font-size:11px; font-weight:700; padding:2px 7px; border-radius:10px; background:rgba(34,197,94,0.10); color:#4ade80; border:1px solid rgba(34,197,94,0.25);">✓ ${tCount}</span>` : ''}
|
|
1725
|
-
</div>
|
|
1726
|
-
`;
|
|
1727
|
-
|
|
1728
|
-
const body = document.createElement('div');
|
|
1729
|
-
body.style.cssText = 'display:flex; flex-direction:column; background:var(--bg);';
|
|
1730
|
-
|
|
1731
|
-
for (const b of g.blockers) body.appendChild(createBlockerRow(b));
|
|
1732
|
-
for (const t of g.tasks) body.appendChild(createTaskRow(t));
|
|
1733
|
-
|
|
1734
|
-
let isOpen = true;
|
|
1735
|
-
head.onclick = () => {
|
|
1736
|
-
isOpen = !isOpen;
|
|
1737
|
-
body.style.display = isOpen ? 'flex' : 'none';
|
|
1738
|
-
const svg = head.querySelector('svg');
|
|
1739
|
-
if (svg) svg.style.transform = isOpen ? 'rotate(0deg)' : 'rotate(-90deg)';
|
|
1740
|
-
};
|
|
1741
|
-
|
|
1742
|
-
swimlane.appendChild(head);
|
|
1743
|
-
swimlane.appendChild(body);
|
|
1744
|
-
el.appendChild(swimlane);
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1548
|
+
// refreshToday now delegates to loadKanban (dashboard uses embedded kanban board)
|
|
1748
1549
|
async function refreshToday() {
|
|
1749
|
-
|
|
1750
|
-
try {
|
|
1751
|
-
const [t, b] = await Promise.all([
|
|
1752
|
-
api('/api/tasks/list', { dir: dirOrDefault(), category: 'DO_NOW', status: 'PENDING', limit: 50 }),
|
|
1753
|
-
api('/api/blockers/list', { dir: dirOrDefault(), status: 'OPEN', limit: 50 })
|
|
1754
|
-
]);
|
|
1755
|
-
renderSwimlanes((t && t.tasks) || [], (b && b.blockers) || []);
|
|
1756
|
-
refreshBlockersInsights();
|
|
1757
|
-
} catch (e) {
|
|
1758
|
-
// keep silent in background refresh
|
|
1759
|
-
}
|
|
1550
|
+
await loadKanban();
|
|
1760
1551
|
}
|
|
1761
1552
|
|
|
1762
1553
|
function renderBlockersInsights(payload) {
|
|
@@ -2769,6 +2560,7 @@
|
|
|
2769
2560
|
const isTimelinePage = document.body && document.body.dataset && document.body.dataset.page === 'timeline';
|
|
2770
2561
|
const isCompanionPage = document.body && document.body.dataset && document.body.dataset.page === 'companion';
|
|
2771
2562
|
const isGraphPage = document.body && document.body.dataset && document.body.dataset.page === 'graph';
|
|
2563
|
+
const isKanbanPage = document.body && document.body.dataset && document.body.dataset.page === 'kanban';
|
|
2772
2564
|
|
|
2773
2565
|
// Load persisted settings from the workspace + bootstrap (auto-init + auto-health)
|
|
2774
2566
|
(async () => {
|
|
@@ -2819,6 +2611,11 @@
|
|
|
2819
2611
|
return;
|
|
2820
2612
|
}
|
|
2821
2613
|
|
|
2614
|
+
if (isKanbanPage) {
|
|
2615
|
+
await loadKanban();
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2822
2619
|
// If workspace isn't initialized yet, auto-init (reduces clicks)
|
|
2823
2620
|
try {
|
|
2824
2621
|
if (defaults && defaults.workspaceOk === false) {
|
|
@@ -2847,16 +2644,20 @@
|
|
|
2847
2644
|
if (typeof saveAndPlan === 'function') saveAndPlan();
|
|
2848
2645
|
return;
|
|
2849
2646
|
}
|
|
2850
|
-
// Ctrl/Cmd+K:
|
|
2647
|
+
// Ctrl/Cmd+K: Open quick-add modal
|
|
2851
2648
|
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
2852
2649
|
e.preventDefault();
|
|
2853
|
-
|
|
2854
|
-
if (target) target.focus();
|
|
2650
|
+
openQuickAdd();
|
|
2855
2651
|
return;
|
|
2856
2652
|
}
|
|
2857
|
-
// Escape:
|
|
2858
|
-
if (e.key === 'Escape'
|
|
2859
|
-
|
|
2653
|
+
// Escape: Close quick-add modal or blur active element
|
|
2654
|
+
if (e.key === 'Escape') {
|
|
2655
|
+
const overlay = $('quickAddOverlay');
|
|
2656
|
+
if (overlay && overlay.style.display !== 'none') {
|
|
2657
|
+
closeQuickAdd();
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
if (document.activeElement) document.activeElement.blur();
|
|
2860
2661
|
}
|
|
2861
2662
|
});
|
|
2862
2663
|
|
|
@@ -2912,4 +2713,468 @@
|
|
|
2912
2713
|
window.askFreya = askFreya;
|
|
2913
2714
|
window.askFreyaInline = askFreyaInline;
|
|
2914
2715
|
window.askFreyaFromInput = askFreyaFromInput;
|
|
2716
|
+
|
|
2717
|
+
/* ── Quick-Add Modal ── */
|
|
2718
|
+
|
|
2719
|
+
// Wire hidden date picker → text input (once)
|
|
2720
|
+
var _qaDateWired = false;
|
|
2721
|
+
function wireQaDatePicker() {
|
|
2722
|
+
if (_qaDateWired) return;
|
|
2723
|
+
var picker = $('qaDuePicker');
|
|
2724
|
+
var text = $('qaDue');
|
|
2725
|
+
if (!picker || !text) return;
|
|
2726
|
+
_qaDateWired = true;
|
|
2727
|
+
picker.addEventListener('change', function() {
|
|
2728
|
+
if (picker.value) {
|
|
2729
|
+
text.value = fmtDateBR(picker.value);
|
|
2730
|
+
}
|
|
2731
|
+
});
|
|
2732
|
+
// Auto-format on blur: accept dd/mm/aaaa typed manually
|
|
2733
|
+
text.addEventListener('keyup', function(e) {
|
|
2734
|
+
var v = text.value.replace(/[^\d]/g, '');
|
|
2735
|
+
if (v.length >= 3 && text.value.indexOf('/') < 0) {
|
|
2736
|
+
// Auto-insert slashes: dd/mm/aaaa
|
|
2737
|
+
var formatted = v.slice(0, 2);
|
|
2738
|
+
if (v.length >= 3) formatted += '/' + v.slice(2, 4);
|
|
2739
|
+
if (v.length >= 5) formatted += '/' + v.slice(4, 8);
|
|
2740
|
+
text.value = formatted;
|
|
2741
|
+
}
|
|
2742
|
+
});
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
function openQuickAdd() {
|
|
2746
|
+
const overlay = $('quickAddOverlay');
|
|
2747
|
+
if (!overlay) return;
|
|
2748
|
+
overlay.style.display = 'flex';
|
|
2749
|
+
wireQaDatePicker();
|
|
2750
|
+
const desc = $('qaDesc');
|
|
2751
|
+
if (desc) { desc.value = ''; desc.focus(); }
|
|
2752
|
+
var cat = $('qaCat'); if (cat) cat.value = 'DO_NOW';
|
|
2753
|
+
var pri = $('qaPriority'); if (pri) pri.value = '';
|
|
2754
|
+
var slug = $('qaSlug'); if (slug) slug.value = '';
|
|
2755
|
+
var due = $('qaDue'); if (due) due.value = '';
|
|
2756
|
+
var picker = $('qaDuePicker'); if (picker) picker.value = '';
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
function closeQuickAdd() {
|
|
2760
|
+
var overlay = $('quickAddOverlay');
|
|
2761
|
+
if (overlay) overlay.style.display = 'none';
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
// Parse dd/mm/aaaa → YYYY-MM-DD
|
|
2765
|
+
function parseDateBR(str) {
|
|
2766
|
+
if (!str) return '';
|
|
2767
|
+
var s = str.trim();
|
|
2768
|
+
if (s.indexOf('/') > -1) {
|
|
2769
|
+
var p = s.split('/');
|
|
2770
|
+
if (p.length === 3 && p[0].length === 2 && p[1].length === 2 && p[2].length === 4) {
|
|
2771
|
+
return p[2] + '-' + p[1] + '-' + p[0];
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
return s; // fallback: pass as-is (may be YYYY-MM-DD already)
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
async function submitQuickAdd() {
|
|
2778
|
+
var desc = $('qaDesc');
|
|
2779
|
+
var text = desc ? desc.value.trim() : '';
|
|
2780
|
+
if (!text) { showToast('err', 'Descricao obrigatoria'); return; }
|
|
2781
|
+
|
|
2782
|
+
var cat = $('qaCat'); var catVal = cat ? cat.value : 'DO_NOW';
|
|
2783
|
+
var pri = $('qaPriority'); var priVal = pri ? pri.value : '';
|
|
2784
|
+
var slug = $('qaSlug'); var slugVal = slug ? slug.value.trim() : '';
|
|
2785
|
+
var due = $('qaDue'); var dueVal = due ? parseDateBR(due.value) : '';
|
|
2786
|
+
|
|
2787
|
+
var body = { dir: dirOrDefault(), description: text, category: catVal };
|
|
2788
|
+
if (priVal) body.priority = priVal;
|
|
2789
|
+
if (slugVal) body.projectSlug = slugVal;
|
|
2790
|
+
if (dueVal) body.dueDate = dueVal;
|
|
2791
|
+
|
|
2792
|
+
try {
|
|
2793
|
+
await api('/api/tasks/create', body);
|
|
2794
|
+
closeQuickAdd();
|
|
2795
|
+
showToast('ok', 'Task criada');
|
|
2796
|
+
await loadKanban();
|
|
2797
|
+
} catch (e) {
|
|
2798
|
+
showToast('err', 'Erro ao criar task');
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
window.openQuickAdd = openQuickAdd;
|
|
2803
|
+
window.closeQuickAdd = closeQuickAdd;
|
|
2804
|
+
window.submitQuickAdd = submitQuickAdd;
|
|
2805
|
+
|
|
2806
|
+
/* ── Delta Banner ── */
|
|
2807
|
+
async function loadDelta() {
|
|
2808
|
+
var el = $('kanbanDelta') || $('deltaBanner');
|
|
2809
|
+
if (!el) return;
|
|
2810
|
+
try {
|
|
2811
|
+
var res = await api('/api/summary/delta', { dir: dirOrDefault() });
|
|
2812
|
+
if (!res || !res.delta) { el.style.display = 'none'; return; }
|
|
2813
|
+
var d = res.delta;
|
|
2814
|
+
var parts = [];
|
|
2815
|
+
if (d.completedTasks > 0) parts.push(d.completedTasks + ' concluida(s)');
|
|
2816
|
+
if (d.resolvedBlockers > 0) parts.push(d.resolvedBlockers + ' blocker(s) resolvido(s)');
|
|
2817
|
+
if (d.newTasks > 0) parts.push(d.newTasks + ' nova(s)');
|
|
2818
|
+
if (d.newBlockers > 0) parts.push(d.newBlockers + ' novo(s) blocker(s)');
|
|
2819
|
+
if (d.overdueTasks > 0) parts.push(d.overdueTasks + ' atrasada(s)');
|
|
2820
|
+
if (parts.length === 0) { el.style.display = 'none'; return; }
|
|
2821
|
+
el.style.display = 'flex';
|
|
2822
|
+
el.className = 'delta-banner';
|
|
2823
|
+
el.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/></svg>'
|
|
2824
|
+
+ '<span>Ultimas 24h: ' + escapeHtml(parts.join(' \u00b7 ')) + '</span>';
|
|
2825
|
+
} catch { el.style.display = 'none'; }
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
/* ── Kanban Board ── */
|
|
2829
|
+
var _kanbanData = { tasks: [], blockers: [] };
|
|
2830
|
+
|
|
2831
|
+
async function loadKanban() {
|
|
2832
|
+
try {
|
|
2833
|
+
var res = await api('/api/tasks/kanban', { dir: dirOrDefault() });
|
|
2834
|
+
if (!res || !res.ok) return;
|
|
2835
|
+
_kanbanData.tasks = res.tasks || [];
|
|
2836
|
+
_kanbanData.blockers = res.blockers || [];
|
|
2837
|
+
populateKanbanProjects();
|
|
2838
|
+
renderKanban();
|
|
2839
|
+
renderKanbanBlockers();
|
|
2840
|
+
loadDelta();
|
|
2841
|
+
// On dashboard, also refresh blockers insights panel
|
|
2842
|
+
if ($('blockersInsightsWrap')) refreshBlockersInsights();
|
|
2843
|
+
} catch (e) {
|
|
2844
|
+
showToast('err', 'Erro ao carregar kanban');
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
function populateKanbanProjects() {
|
|
2849
|
+
var sel = $('kanbanFilterProject');
|
|
2850
|
+
if (!sel) return;
|
|
2851
|
+
var slugs = new Set();
|
|
2852
|
+
_kanbanData.tasks.forEach(function(t) { if (t.projectSlug) slugs.add(t.projectSlug); });
|
|
2853
|
+
_kanbanData.blockers.forEach(function(b) { if (b.projectSlug) slugs.add(b.projectSlug); });
|
|
2854
|
+
var current = sel.value;
|
|
2855
|
+
sel.innerHTML = '<option value="">Todos os projetos</option>';
|
|
2856
|
+
Array.from(slugs).sort().forEach(function(s) {
|
|
2857
|
+
var opt = document.createElement('option');
|
|
2858
|
+
opt.value = s; opt.textContent = s;
|
|
2859
|
+
sel.appendChild(opt);
|
|
2860
|
+
});
|
|
2861
|
+
sel.value = current;
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
function filterKanban() { renderKanban(); renderKanbanBlockers(); }
|
|
2865
|
+
|
|
2866
|
+
function getFilteredTasks() {
|
|
2867
|
+
var sel = $('kanbanFilterProject');
|
|
2868
|
+
var filter = sel ? sel.value : '';
|
|
2869
|
+
if (!filter) return _kanbanData.tasks;
|
|
2870
|
+
return _kanbanData.tasks.filter(function(t) { return t.projectSlug === filter; });
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
// Format ISO date (YYYY-MM-DD or full ISO) to dd/mm/aaaa
|
|
2874
|
+
function fmtDateBR(isoStr) {
|
|
2875
|
+
if (!isoStr) return '';
|
|
2876
|
+
var d = isoStr.slice(0, 10); // YYYY-MM-DD
|
|
2877
|
+
var parts = d.split('-');
|
|
2878
|
+
if (parts.length !== 3) return d;
|
|
2879
|
+
return parts[2] + '/' + parts[1] + '/' + parts[0];
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
// Format ISO datetime to dd/mm/aaaa HH:mm
|
|
2883
|
+
function fmtDateTimeBR(isoStr) {
|
|
2884
|
+
if (!isoStr) return '';
|
|
2885
|
+
var dt = new Date(isoStr);
|
|
2886
|
+
if (isNaN(dt.getTime())) return isoStr;
|
|
2887
|
+
var dd = String(dt.getDate()).padStart(2, '0');
|
|
2888
|
+
var mm = String(dt.getMonth() + 1).padStart(2, '0');
|
|
2889
|
+
var yyyy = dt.getFullYear();
|
|
2890
|
+
var hh = String(dt.getHours()).padStart(2, '0');
|
|
2891
|
+
var min = String(dt.getMinutes()).padStart(2, '0');
|
|
2892
|
+
return dd + '/' + mm + '/' + yyyy + ' ' + hh + ':' + min;
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
// Show detail panel for a task or blocker
|
|
2896
|
+
function showDetailPanel(item, type) {
|
|
2897
|
+
// Remove existing panel
|
|
2898
|
+
var old = document.querySelector('.detail-panel-overlay');
|
|
2899
|
+
if (old) old.remove();
|
|
2900
|
+
|
|
2901
|
+
var overlay = document.createElement('div');
|
|
2902
|
+
overlay.className = 'detail-panel-overlay';
|
|
2903
|
+
overlay.onclick = function(e) { if (e.target === overlay) overlay.remove(); };
|
|
2904
|
+
|
|
2905
|
+
var panel = document.createElement('div');
|
|
2906
|
+
panel.className = 'detail-panel';
|
|
2907
|
+
|
|
2908
|
+
var isTask = type === 'task';
|
|
2909
|
+
var pc = isTask ? priColor(item.priority) : null;
|
|
2910
|
+
|
|
2911
|
+
var html = '<div class="detail-panel-header">';
|
|
2912
|
+
html += '<div style="display:flex; align-items:center; gap:10px; flex:1; min-width:0;">';
|
|
2913
|
+
if (isTask && pc) {
|
|
2914
|
+
html += '<span class="kanban-pri-dot" style="background:' + pc.dot + '; width:12px; height:12px;"></span>';
|
|
2915
|
+
} else {
|
|
2916
|
+
var sColor = sevColor(item.severity);
|
|
2917
|
+
html += '<span style="font-size:11px; font-weight:800; padding:3px 8px; border-radius:4px; background:' + sColor + '22; color:' + sColor + '; border:1px solid ' + sColor + '44;">' + escapeHtml(item.severity || '') + '</span>';
|
|
2918
|
+
}
|
|
2919
|
+
html += '<span class="detail-panel-title">' + escapeHtml(isTask ? item.description : item.title) + '</span>';
|
|
2920
|
+
html += '</div>';
|
|
2921
|
+
html += '<button class="detail-panel-close" onclick="this.closest(\'.detail-panel-overlay\').remove()">×</button>';
|
|
2922
|
+
html += '</div>';
|
|
2923
|
+
|
|
2924
|
+
// Info grid
|
|
2925
|
+
html += '<div class="detail-panel-grid">';
|
|
2926
|
+
if (isTask) {
|
|
2927
|
+
html += detailRow('ID', item.id);
|
|
2928
|
+
html += detailRow('Status', item.status);
|
|
2929
|
+
html += detailRow('Categoria', item.category);
|
|
2930
|
+
html += detailRow('Prioridade', pc ? pc.label : 'Nenhuma');
|
|
2931
|
+
html += detailRow('Projeto', item.projectSlug || '—');
|
|
2932
|
+
html += detailRow('Stream', item.streamSlug || '—');
|
|
2933
|
+
html += detailRow('Due Date', item.dueDate ? fmtDateBR(item.dueDate) : '—');
|
|
2934
|
+
html += detailRow('Criada em', fmtDateTimeBR(item.createdAt));
|
|
2935
|
+
if (item.completedAt) html += detailRow('Concluida em', fmtDateTimeBR(item.completedAt));
|
|
2936
|
+
if (item.source) html += detailRow('Fonte', item.source);
|
|
2937
|
+
} else {
|
|
2938
|
+
html += detailRow('ID', item.id);
|
|
2939
|
+
html += detailRow('Severidade', item.severity);
|
|
2940
|
+
html += detailRow('Status', item.status);
|
|
2941
|
+
html += detailRow('Projeto', item.projectSlug || '—');
|
|
2942
|
+
html += detailRow('Responsavel', item.owner || '—');
|
|
2943
|
+
html += detailRow('Proxima Acao', item.nextAction || '—');
|
|
2944
|
+
html += detailRow('Criado em', fmtDateTimeBR(item.createdAt));
|
|
2945
|
+
if (item.resolvedAt) html += detailRow('Resolvido em', fmtDateTimeBR(item.resolvedAt));
|
|
2946
|
+
if (item.source) html += detailRow('Fonte', item.source);
|
|
2947
|
+
}
|
|
2948
|
+
html += '</div>';
|
|
2949
|
+
|
|
2950
|
+
// Comments section (tasks only)
|
|
2951
|
+
if (isTask && item.comments && item.comments.length > 0) {
|
|
2952
|
+
html += '<div class="detail-panel-section">';
|
|
2953
|
+
html += '<div class="detail-panel-section-title">Comentarios</div>';
|
|
2954
|
+
item.comments.forEach(function(c) {
|
|
2955
|
+
var cObj = typeof c === 'string' ? { text: c } : c;
|
|
2956
|
+
html += '<div class="detail-panel-comment">';
|
|
2957
|
+
if (cObj.at) html += '<span class="detail-panel-comment-date">' + fmtDateTimeBR(cObj.at) + '</span>';
|
|
2958
|
+
html += '<span>' + escapeHtml(cObj.text || cObj.comment || String(c)) + '</span>';
|
|
2959
|
+
html += '</div>';
|
|
2960
|
+
});
|
|
2961
|
+
html += '</div>';
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
// Metadata section (raw, if has extra fields)
|
|
2965
|
+
var metaKeys = item.metadata ? Object.keys(item.metadata).filter(function(k) {
|
|
2966
|
+
return ['priority', 'streamSlug', 'comments', 'source'].indexOf(k) < 0 && item.metadata[k];
|
|
2967
|
+
}) : [];
|
|
2968
|
+
if (metaKeys.length > 0) {
|
|
2969
|
+
html += '<div class="detail-panel-section">';
|
|
2970
|
+
html += '<div class="detail-panel-section-title">Metadados Adicionais</div>';
|
|
2971
|
+
html += '<div class="detail-panel-grid">';
|
|
2972
|
+
metaKeys.forEach(function(k) {
|
|
2973
|
+
html += detailRow(k, typeof item.metadata[k] === 'object' ? JSON.stringify(item.metadata[k]) : String(item.metadata[k]));
|
|
2974
|
+
});
|
|
2975
|
+
html += '</div></div>';
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
panel.innerHTML = html;
|
|
2979
|
+
overlay.appendChild(panel);
|
|
2980
|
+
document.body.appendChild(overlay);
|
|
2981
|
+
|
|
2982
|
+
// Close on Escape
|
|
2983
|
+
var escHandler = function(e) {
|
|
2984
|
+
if (e.key === 'Escape') { overlay.remove(); document.removeEventListener('keydown', escHandler); }
|
|
2985
|
+
};
|
|
2986
|
+
document.addEventListener('keydown', escHandler);
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
function detailRow(label, value) {
|
|
2990
|
+
return '<div class="detail-row-label">' + escapeHtml(label) + '</div>'
|
|
2991
|
+
+ '<div class="detail-row-value">' + escapeHtml(String(value || '')) + '</div>';
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
function renderKanban() {
|
|
2995
|
+
var tasks = getFilteredTasks();
|
|
2996
|
+
var today = new Date().toISOString().slice(0, 10);
|
|
2997
|
+
var sevenAgo = new Date(Date.now() - 7 * 86400000).toISOString();
|
|
2998
|
+
|
|
2999
|
+
var cols = {
|
|
3000
|
+
DO_NOW: [], SCHEDULE: [], DELEGATE: [], COMPLETED: []
|
|
3001
|
+
};
|
|
3002
|
+
|
|
3003
|
+
tasks.forEach(function(t) {
|
|
3004
|
+
if (t.status === 'COMPLETED') {
|
|
3005
|
+
if (t.completedAt && t.completedAt >= sevenAgo) cols.COMPLETED.push(t);
|
|
3006
|
+
} else if (t.status === 'PENDING' && cols[t.category]) {
|
|
3007
|
+
cols[t.category].push(t);
|
|
3008
|
+
}
|
|
3009
|
+
});
|
|
3010
|
+
|
|
3011
|
+
var idMap = { DO_NOW: 'colDoNow', SCHEDULE: 'colSchedule', DELEGATE: 'colDelegate', COMPLETED: 'colDone' };
|
|
3012
|
+
var countMap = { DO_NOW: 'countDoNow', SCHEDULE: 'countSchedule', DELEGATE: 'countDelegate', COMPLETED: 'countDone' };
|
|
3013
|
+
|
|
3014
|
+
Object.keys(cols).forEach(function(cat) {
|
|
3015
|
+
var el = $(idMap[cat]);
|
|
3016
|
+
var countEl = $(countMap[cat]);
|
|
3017
|
+
if (countEl) countEl.textContent = cols[cat].length;
|
|
3018
|
+
if (!el) return;
|
|
3019
|
+
el.innerHTML = '';
|
|
3020
|
+
|
|
3021
|
+
cols[cat].forEach(function(t) {
|
|
3022
|
+
var card = document.createElement('div');
|
|
3023
|
+
card.className = 'kanban-card';
|
|
3024
|
+
card.draggable = (cat !== 'COMPLETED');
|
|
3025
|
+
card.dataset.taskId = t.id;
|
|
3026
|
+
card.dataset.category = cat;
|
|
3027
|
+
|
|
3028
|
+
var pc = priColor(t.priority);
|
|
3029
|
+
var isOverdue = t.dueDate && t.dueDate < today && t.status === 'PENDING';
|
|
3030
|
+
|
|
3031
|
+
var html = '<div class="kanban-card-header">'
|
|
3032
|
+
+ '<span class="kanban-pri-dot" style="background:' + pc.dot + ';" title="' + escapeHtml(pc.label) + '"></span>'
|
|
3033
|
+
+ '<span class="kanban-card-desc">' + escapeHtml(t.description || '') + '</span>'
|
|
3034
|
+
+ '</div>';
|
|
3035
|
+
|
|
3036
|
+
var meta = [];
|
|
3037
|
+
if (t.projectSlug) meta.push('<span class="kanban-tag">' + escapeHtml(t.projectSlug) + '</span>');
|
|
3038
|
+
if (t.dueDate) {
|
|
3039
|
+
var dueCls = isOverdue ? 'kanban-due overdue' : 'kanban-due';
|
|
3040
|
+
meta.push('<span class="' + dueCls + '">' + escapeHtml(fmtDateBR(t.dueDate)) + '</span>');
|
|
3041
|
+
}
|
|
3042
|
+
if (meta.length) html += '<div class="kanban-card-meta">' + meta.join('') + '</div>';
|
|
3043
|
+
|
|
3044
|
+
if (cat !== 'COMPLETED') {
|
|
3045
|
+
html += '<div class="kanban-card-actions">'
|
|
3046
|
+
+ '<button class="kanban-action-btn complete-btn" title="Concluir">\u2713</button>'
|
|
3047
|
+
+ '<button class="kanban-action-btn edit-btn" title="Editar">\u270E</button>'
|
|
3048
|
+
+ '</div>';
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
card.innerHTML = html;
|
|
3052
|
+
|
|
3053
|
+
// Click to show detail panel (only if not dragging)
|
|
3054
|
+
var _dragged = false;
|
|
3055
|
+
card.addEventListener('mousedown', function() { _dragged = false; });
|
|
3056
|
+
card.addEventListener('mousemove', function() { _dragged = true; });
|
|
3057
|
+
card.addEventListener('click', function(e) {
|
|
3058
|
+
if (_dragged) return;
|
|
3059
|
+
// Don't trigger on action buttons
|
|
3060
|
+
if (e.target.closest('.kanban-card-actions')) return;
|
|
3061
|
+
showDetailPanel(t, 'task');
|
|
3062
|
+
});
|
|
3063
|
+
|
|
3064
|
+
// Drag events
|
|
3065
|
+
if (cat !== 'COMPLETED') {
|
|
3066
|
+
card.addEventListener('dragstart', function(e) {
|
|
3067
|
+
e.dataTransfer.setData('text/plain', t.id);
|
|
3068
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
3069
|
+
card.classList.add('dragging');
|
|
3070
|
+
});
|
|
3071
|
+
card.addEventListener('dragend', function() {
|
|
3072
|
+
card.classList.remove('dragging');
|
|
3073
|
+
});
|
|
3074
|
+
|
|
3075
|
+
// Complete button
|
|
3076
|
+
var completeBtn = card.querySelector('.complete-btn');
|
|
3077
|
+
if (completeBtn) {
|
|
3078
|
+
completeBtn.onclick = async function() {
|
|
3079
|
+
try {
|
|
3080
|
+
await api('/api/tasks/complete', { dir: dirOrDefault(), id: t.id });
|
|
3081
|
+
showToast('ok', 'Concluida');
|
|
3082
|
+
await loadKanban();
|
|
3083
|
+
} catch { showToast('err', 'Falhou'); }
|
|
3084
|
+
};
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
// Edit button - inline edit via prompt
|
|
3088
|
+
var editBtn = card.querySelector('.edit-btn');
|
|
3089
|
+
if (editBtn) {
|
|
3090
|
+
editBtn.onclick = async function() {
|
|
3091
|
+
var newCat = prompt('Categoria (DO_NOW|SCHEDULE|DELEGATE):', cat);
|
|
3092
|
+
if (!newCat) return;
|
|
3093
|
+
var newSlug = prompt('Projeto (slug):', t.projectSlug || '');
|
|
3094
|
+
if (newSlug === null) return;
|
|
3095
|
+
var currentDueBR = t.dueDate ? fmtDateBR(t.dueDate) : '';
|
|
3096
|
+
var newDueBR = prompt('Due date (dd/mm/aaaa):', currentDueBR);
|
|
3097
|
+
if (newDueBR === null) return;
|
|
3098
|
+
// Convert dd/mm/aaaa back to YYYY-MM-DD for API
|
|
3099
|
+
var newDue = '';
|
|
3100
|
+
if (newDueBR && newDueBR.indexOf('/') > -1) {
|
|
3101
|
+
var dp = newDueBR.split('/');
|
|
3102
|
+
if (dp.length === 3) newDue = dp[2] + '-' + dp[1] + '-' + dp[0];
|
|
3103
|
+
} else {
|
|
3104
|
+
newDue = newDueBR; // fallback: accept YYYY-MM-DD too
|
|
3105
|
+
}
|
|
3106
|
+
try {
|
|
3107
|
+
await api('/api/tasks/update', {
|
|
3108
|
+
dir: dirOrDefault(), id: t.id,
|
|
3109
|
+
patch: { category: newCat, projectSlug: newSlug, dueDate: newDue || null }
|
|
3110
|
+
});
|
|
3111
|
+
showToast('ok', 'Atualizada');
|
|
3112
|
+
await loadKanban();
|
|
3113
|
+
} catch { showToast('err', 'Falhou'); }
|
|
3114
|
+
};
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
el.appendChild(card);
|
|
3119
|
+
});
|
|
3120
|
+
|
|
3121
|
+
// Drop zone
|
|
3122
|
+
if (cat !== 'COMPLETED') {
|
|
3123
|
+
el.addEventListener('dragover', function(e) {
|
|
3124
|
+
e.preventDefault();
|
|
3125
|
+
e.dataTransfer.dropEffect = 'move';
|
|
3126
|
+
el.classList.add('drag-over');
|
|
3127
|
+
});
|
|
3128
|
+
el.addEventListener('dragleave', function() {
|
|
3129
|
+
el.classList.remove('drag-over');
|
|
3130
|
+
});
|
|
3131
|
+
el.addEventListener('drop', async function(e) {
|
|
3132
|
+
e.preventDefault();
|
|
3133
|
+
el.classList.remove('drag-over');
|
|
3134
|
+
var taskId = e.dataTransfer.getData('text/plain');
|
|
3135
|
+
if (!taskId) return;
|
|
3136
|
+
try {
|
|
3137
|
+
await api('/api/tasks/update', {
|
|
3138
|
+
dir: dirOrDefault(), id: taskId,
|
|
3139
|
+
patch: { category: cat }
|
|
3140
|
+
});
|
|
3141
|
+
showToast('ok', 'Movida para ' + cat);
|
|
3142
|
+
await loadKanban();
|
|
3143
|
+
} catch { showToast('err', 'Falhou'); }
|
|
3144
|
+
});
|
|
3145
|
+
}
|
|
3146
|
+
});
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
function renderKanbanBlockers() {
|
|
3150
|
+
var wrap = $('kanbanBlockers');
|
|
3151
|
+
var list = $('kanbanBlockersList');
|
|
3152
|
+
if (!wrap || !list) return;
|
|
3153
|
+
|
|
3154
|
+
var sel = $('kanbanFilterProject');
|
|
3155
|
+
var filter = sel ? sel.value : '';
|
|
3156
|
+
var blockers = _kanbanData.blockers;
|
|
3157
|
+
if (filter) blockers = blockers.filter(function(b) { return b.projectSlug === filter; });
|
|
3158
|
+
|
|
3159
|
+
if (blockers.length === 0) { wrap.style.display = 'none'; return; }
|
|
3160
|
+
wrap.style.display = 'block';
|
|
3161
|
+
list.innerHTML = '';
|
|
3162
|
+
|
|
3163
|
+
blockers.forEach(function(b) {
|
|
3164
|
+
var card = document.createElement('div');
|
|
3165
|
+
card.className = 'kanban-blocker-card';
|
|
3166
|
+
var color = sevColor(b.severity);
|
|
3167
|
+
card.innerHTML = '<span class="kanban-blocker-sev" style="background:' + color + '22; color:' + color + '; border-color:' + color + '44;">' + escapeHtml(b.severity || 'BLOCKER') + '</span>'
|
|
3168
|
+
+ '<span class="kanban-blocker-title">' + escapeHtml(b.title || '') + '</span>'
|
|
3169
|
+
+ (b.projectSlug ? '<span class="kanban-tag">' + escapeHtml(b.projectSlug) + '</span>' : '')
|
|
3170
|
+
+ (b.owner ? '<span class="kanban-owner">' + escapeHtml(b.owner) + '</span>' : '');
|
|
3171
|
+
card.style.cursor = 'pointer';
|
|
3172
|
+
card.addEventListener('click', function() { showDetailPanel(b, 'blocker'); });
|
|
3173
|
+
list.appendChild(card);
|
|
3174
|
+
});
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
window.loadKanban = loadKanban;
|
|
3178
|
+
window.filterKanban = filterKanban;
|
|
3179
|
+
window.loadDelta = loadDelta;
|
|
2915
3180
|
})();
|