@cccarv82/freya 2.15.0 → 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 +154 -1
- package/cli/web-ui.js +194 -228
- package/cli/web.js +96 -54
- package/package.json +1 -1
package/cli/web-ui.css
CHANGED
|
@@ -1682,6 +1682,11 @@ textarea:focus {
|
|
|
1682
1682
|
background: var(--chip);
|
|
1683
1683
|
}
|
|
1684
1684
|
|
|
1685
|
+
/* Compact kanban on dashboard (not standalone /kanban page) */
|
|
1686
|
+
body:not([data-page="kanban"]) .kanban-col-body {
|
|
1687
|
+
max-height: 400px;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1685
1690
|
.kanban-card {
|
|
1686
1691
|
background: var(--paper);
|
|
1687
1692
|
border: 1px solid var(--line2);
|
|
@@ -1901,6 +1906,48 @@ textarea:focus {
|
|
|
1901
1906
|
align-items: center;
|
|
1902
1907
|
}
|
|
1903
1908
|
|
|
1909
|
+
.qa-date-wrap {
|
|
1910
|
+
position: relative;
|
|
1911
|
+
width: 160px;
|
|
1912
|
+
display: flex;
|
|
1913
|
+
align-items: center;
|
|
1914
|
+
}
|
|
1915
|
+
.qa-date-text {
|
|
1916
|
+
padding-right: 32px !important;
|
|
1917
|
+
}
|
|
1918
|
+
.qa-date-hidden {
|
|
1919
|
+
position: absolute;
|
|
1920
|
+
top: 0;
|
|
1921
|
+
left: 0;
|
|
1922
|
+
width: 1px;
|
|
1923
|
+
height: 1px;
|
|
1924
|
+
opacity: 0;
|
|
1925
|
+
pointer-events: none;
|
|
1926
|
+
overflow: hidden;
|
|
1927
|
+
clip: rect(0,0,0,0);
|
|
1928
|
+
border: 0;
|
|
1929
|
+
padding: 0;
|
|
1930
|
+
margin: -1px;
|
|
1931
|
+
}
|
|
1932
|
+
.qa-date-btn {
|
|
1933
|
+
position: absolute;
|
|
1934
|
+
right: 4px;
|
|
1935
|
+
top: 50%;
|
|
1936
|
+
transform: translateY(-50%);
|
|
1937
|
+
background: none;
|
|
1938
|
+
border: none;
|
|
1939
|
+
cursor: pointer;
|
|
1940
|
+
font-size: 16px;
|
|
1941
|
+
padding: 2px 4px;
|
|
1942
|
+
line-height: 1;
|
|
1943
|
+
color: var(--muted);
|
|
1944
|
+
opacity: 0.8;
|
|
1945
|
+
transition: opacity 0.15s;
|
|
1946
|
+
}
|
|
1947
|
+
.qa-date-btn:hover {
|
|
1948
|
+
opacity: 1;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1904
1951
|
/* ── Delta Banner ── */
|
|
1905
1952
|
.delta-banner {
|
|
1906
1953
|
display: flex;
|
|
@@ -1913,4 +1960,110 @@ textarea:focus {
|
|
|
1913
1960
|
font-size: 12px;
|
|
1914
1961
|
color: var(--text);
|
|
1915
1962
|
border-left: 3px solid var(--primary);
|
|
1916
|
-
}
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
/* ── Detail Panel (click-to-view on kanban cards) ── */
|
|
1966
|
+
.detail-panel-overlay {
|
|
1967
|
+
position: fixed;
|
|
1968
|
+
inset: 0;
|
|
1969
|
+
background: rgba(0,0,0,0.55);
|
|
1970
|
+
display: flex;
|
|
1971
|
+
align-items: center;
|
|
1972
|
+
justify-content: center;
|
|
1973
|
+
z-index: 10000;
|
|
1974
|
+
animation: fadeIn 0.15s ease;
|
|
1975
|
+
}
|
|
1976
|
+
.detail-panel {
|
|
1977
|
+
background: var(--paper);
|
|
1978
|
+
border: 1px solid var(--line2);
|
|
1979
|
+
border-radius: 12px;
|
|
1980
|
+
width: 520px;
|
|
1981
|
+
max-width: 95vw;
|
|
1982
|
+
max-height: 85vh;
|
|
1983
|
+
overflow-y: auto;
|
|
1984
|
+
padding: 20px 24px;
|
|
1985
|
+
box-shadow: 0 12px 40px rgba(0,0,0,0.4);
|
|
1986
|
+
}
|
|
1987
|
+
.detail-panel-header {
|
|
1988
|
+
display: flex;
|
|
1989
|
+
align-items: flex-start;
|
|
1990
|
+
justify-content: space-between;
|
|
1991
|
+
gap: 12px;
|
|
1992
|
+
margin-bottom: 16px;
|
|
1993
|
+
padding-bottom: 12px;
|
|
1994
|
+
border-bottom: 1px solid var(--border);
|
|
1995
|
+
}
|
|
1996
|
+
.detail-panel-title {
|
|
1997
|
+
font-size: 16px;
|
|
1998
|
+
font-weight: 700;
|
|
1999
|
+
color: var(--text);
|
|
2000
|
+
word-break: break-word;
|
|
2001
|
+
}
|
|
2002
|
+
.detail-panel-close {
|
|
2003
|
+
background: none;
|
|
2004
|
+
border: none;
|
|
2005
|
+
color: var(--muted);
|
|
2006
|
+
font-size: 22px;
|
|
2007
|
+
cursor: pointer;
|
|
2008
|
+
padding: 0 4px;
|
|
2009
|
+
line-height: 1;
|
|
2010
|
+
flex-shrink: 0;
|
|
2011
|
+
}
|
|
2012
|
+
.detail-panel-close:hover {
|
|
2013
|
+
color: var(--text);
|
|
2014
|
+
}
|
|
2015
|
+
.detail-panel-grid {
|
|
2016
|
+
display: grid;
|
|
2017
|
+
grid-template-columns: 120px 1fr;
|
|
2018
|
+
gap: 6px 12px;
|
|
2019
|
+
margin-bottom: 12px;
|
|
2020
|
+
}
|
|
2021
|
+
.detail-row-label {
|
|
2022
|
+
font-size: 11px;
|
|
2023
|
+
font-weight: 700;
|
|
2024
|
+
text-transform: uppercase;
|
|
2025
|
+
letter-spacing: 0.5px;
|
|
2026
|
+
color: var(--muted);
|
|
2027
|
+
padding: 3px 0;
|
|
2028
|
+
}
|
|
2029
|
+
.detail-row-value {
|
|
2030
|
+
font-size: 13px;
|
|
2031
|
+
color: var(--text);
|
|
2032
|
+
padding: 3px 0;
|
|
2033
|
+
word-break: break-word;
|
|
2034
|
+
font-family: var(--mono);
|
|
2035
|
+
}
|
|
2036
|
+
.detail-panel-section {
|
|
2037
|
+
margin-top: 12px;
|
|
2038
|
+
padding-top: 12px;
|
|
2039
|
+
border-top: 1px solid var(--border);
|
|
2040
|
+
}
|
|
2041
|
+
.detail-panel-section-title {
|
|
2042
|
+
font-size: 11px;
|
|
2043
|
+
font-weight: 700;
|
|
2044
|
+
text-transform: uppercase;
|
|
2045
|
+
letter-spacing: 0.5px;
|
|
2046
|
+
color: var(--muted);
|
|
2047
|
+
margin-bottom: 8px;
|
|
2048
|
+
}
|
|
2049
|
+
.detail-panel-comment {
|
|
2050
|
+
font-size: 12px;
|
|
2051
|
+
color: var(--text);
|
|
2052
|
+
padding: 6px 10px;
|
|
2053
|
+
background: var(--bg2);
|
|
2054
|
+
border-radius: 6px;
|
|
2055
|
+
margin-bottom: 4px;
|
|
2056
|
+
display: flex;
|
|
2057
|
+
flex-direction: column;
|
|
2058
|
+
gap: 2px;
|
|
2059
|
+
}
|
|
2060
|
+
.detail-panel-comment-date {
|
|
2061
|
+
font-size: 10px;
|
|
2062
|
+
color: var(--muted);
|
|
2063
|
+
font-family: var(--mono);
|
|
2064
|
+
}
|
|
2065
|
+
@keyframes fadeIn {
|
|
2066
|
+
from { opacity: 0; }
|
|
2067
|
+
to { opacity: 1; }
|
|
2068
|
+
}
|
|
2069
|
+
|
package/cli/web-ui.js
CHANGED
|
@@ -1545,229 +1545,9 @@
|
|
|
1545
1545
|
return '#94a3b8';
|
|
1546
1546
|
}
|
|
1547
1547
|
|
|
1548
|
-
|
|
1549
|
-
const el = $('swimlaneContainer');
|
|
1550
|
-
if (!el) return;
|
|
1551
|
-
el.innerHTML = '';
|
|
1552
|
-
|
|
1553
|
-
// Update top-level summary chips
|
|
1554
|
-
const chips = $('focusSummaryChips');
|
|
1555
|
-
if (chips) {
|
|
1556
|
-
chips.innerHTML = '';
|
|
1557
|
-
if (blockers.length > 0) {
|
|
1558
|
-
const bc = document.createElement('span');
|
|
1559
|
-
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);';
|
|
1560
|
-
bc.textContent = blockers.length + ' blocker' + (blockers.length > 1 ? 's' : '');
|
|
1561
|
-
chips.appendChild(bc);
|
|
1562
|
-
}
|
|
1563
|
-
if (tasks.length > 0) {
|
|
1564
|
-
const tc = document.createElement('span');
|
|
1565
|
-
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);';
|
|
1566
|
-
tc.textContent = tasks.length + ' task' + (tasks.length > 1 ? 's' : '');
|
|
1567
|
-
chips.appendChild(tc);
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
// Update progress bar (tasks completed today vs total — we show pending count)
|
|
1572
|
-
const progWrap = $('focusProgressWrap');
|
|
1573
|
-
const progBar = $('focusProgressBar');
|
|
1574
|
-
const progLabel = $('focusProgressLabel');
|
|
1575
|
-
if (progWrap) {
|
|
1576
|
-
if (tasks.length > 0 || blockers.length > 0) {
|
|
1577
|
-
progWrap.style.display = 'flex';
|
|
1578
|
-
// We only have pending tasks here; show blockers impact
|
|
1579
|
-
const blocker_pct = blockers.length === 0 ? 100 : Math.max(10, Math.round((1 - blockers.length / Math.max(tasks.length + blockers.length, 1)) * 100));
|
|
1580
|
-
if (progBar) progBar.style.width = blocker_pct + '%';
|
|
1581
|
-
if (progBar) progBar.style.background = blockers.length > 0 ? '#f97316' : 'var(--accent)';
|
|
1582
|
-
if (progLabel) progLabel.textContent = blockers.length > 0 ? blockers.length + ' blocking' : 'tudo livre';
|
|
1583
|
-
} else {
|
|
1584
|
-
progWrap.style.display = 'none';
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
const groups = {};
|
|
1589
|
-
const addGroup = (slug) => {
|
|
1590
|
-
const key = slug || 'Global / Sem Projeto';
|
|
1591
|
-
if (!groups[key]) groups[key] = { tasks: [], blockers: [] };
|
|
1592
|
-
return key;
|
|
1593
|
-
};
|
|
1594
|
-
|
|
1595
|
-
for (const t of tasks) groups[addGroup(t.projectSlug)].tasks.push(t);
|
|
1596
|
-
for (const b of blockers) groups[addGroup(b.projectSlug)].blockers.push(b);
|
|
1597
|
-
|
|
1598
|
-
const sortedKeys = Object.keys(groups).sort((a, b) => {
|
|
1599
|
-
if (a === 'Global / Sem Projeto') return 1;
|
|
1600
|
-
if (b === 'Global / Sem Projeto') return -1;
|
|
1601
|
-
return a.localeCompare(b);
|
|
1602
|
-
});
|
|
1603
|
-
sortedKeys.sort((a, b) => groups[b].blockers.length - groups[a].blockers.length);
|
|
1604
|
-
|
|
1605
|
-
if (sortedKeys.length === 0) {
|
|
1606
|
-
const empty = document.createElement('div');
|
|
1607
|
-
empty.style.cssText = 'display:flex; flex-direction:column; align-items:center; justify-content:center; padding:48px 24px; gap:8px; flex:1;';
|
|
1608
|
-
empty.innerHTML = '<div style="font-size:28px; line-height:1;">✅</div>'
|
|
1609
|
-
+ '<div style="font-size:14px; font-weight:600; color:var(--text);">Dia limpo!</div>'
|
|
1610
|
-
+ '<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>';
|
|
1611
|
-
el.appendChild(empty);
|
|
1612
|
-
return;
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
const createTaskRow = (t) => {
|
|
1616
|
-
const row = document.createElement('div');
|
|
1617
|
-
const pc = priColor(t.priority);
|
|
1618
|
-
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;';
|
|
1619
|
-
row.onmouseover = () => row.style.background = 'var(--paper)';
|
|
1620
|
-
row.onmouseout = () => row.style.background = 'var(--bg)';
|
|
1621
|
-
row.innerHTML =
|
|
1622
|
-
'<div style="display:flex; align-items:center; gap:10px; min-width:0; flex:1;">'
|
|
1623
|
-
+ '<div style="width:8px; height:8px; border-radius:50%; background:' + pc.dot + '; flex-shrink:0;" title="Prioridade: ' + escapeHtml(pc.label) + '"></div>'
|
|
1624
|
-
+ '<div style="min-width:0;">'
|
|
1625
|
-
+ '<div style="font-weight:600; font-size:13px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">' + escapeHtml(t.description || '') + '</div>'
|
|
1626
|
-
+ '<div style="font-size:11px; color:var(--muted); margin-top:2px;">'
|
|
1627
|
-
+ escapeHtml(pc.label)
|
|
1628
|
-
+ (t.category ? ' · <span style="font-family:var(--mono);">' + escapeHtml(t.category) + '</span>' : '')
|
|
1629
|
-
+ '</div>'
|
|
1630
|
-
+ '</div></div>'
|
|
1631
|
-
+ '<div class="task-actions" style="display:flex; gap:6px; flex-shrink:0; align-items:center;">'
|
|
1632
|
-
+ '<button class="btn small complete-btn" type="button" style="padding:3px 10px; font-size:11px;" title="Marcar como concluída">✓ Concluir</button>'
|
|
1633
|
-
+ '<button class="btn small edit-btn" type="button" style="padding:3px 8px; font-size:11px;">✎</button>'
|
|
1634
|
-
+ '</div>';
|
|
1635
|
-
row.querySelector('.edit-btn').onclick = () => editTask(t);
|
|
1636
|
-
|
|
1637
|
-
var attachCompleteHandler = function() {
|
|
1638
|
-
var cBtn = row.querySelector('.complete-btn');
|
|
1639
|
-
if (!cBtn) return;
|
|
1640
|
-
cBtn.onclick = function() {
|
|
1641
|
-
var actionsDiv = row.querySelector('.task-actions');
|
|
1642
|
-
actionsDiv.innerHTML =
|
|
1643
|
-
'<input type="text" class="comment-input" placeholder="Comentario (opcional)" '
|
|
1644
|
-
+ 'style="font-size:11px; padding:3px 8px; background:var(--bg); border:1px solid var(--border); '
|
|
1645
|
-
+ 'color:var(--text); width:180px; outline:none; font-family:var(--mono);" />'
|
|
1646
|
-
+ '<button class="btn small confirm-btn" type="button" style="padding:3px 10px; font-size:11px; '
|
|
1647
|
-
+ 'background:var(--accent); color:#000; font-weight:700;">Confirmar</button>'
|
|
1648
|
-
+ '<button class="btn small cancel-btn" type="button" style="padding:3px 8px; font-size:11px; '
|
|
1649
|
-
+ 'color:var(--muted);">\u2715</button>';
|
|
1650
|
-
|
|
1651
|
-
var input = actionsDiv.querySelector('.comment-input');
|
|
1652
|
-
input.focus();
|
|
1653
|
-
|
|
1654
|
-
var doComplete = async function() {
|
|
1655
|
-
var comment = input.value.trim();
|
|
1656
|
-
actionsDiv.querySelector('.confirm-btn').disabled = true;
|
|
1657
|
-
actionsDiv.querySelector('.confirm-btn').textContent = '\u2026';
|
|
1658
|
-
try {
|
|
1659
|
-
setPill('run', 'concluindo\u2026');
|
|
1660
|
-
var body = { dir: dirOrDefault(), id: t.id };
|
|
1661
|
-
if (comment) body.comment = comment;
|
|
1662
|
-
await api('/api/tasks/complete', body);
|
|
1663
|
-
row.style.opacity = '0.4';
|
|
1664
|
-
row.style.pointerEvents = 'none';
|
|
1665
|
-
await refreshToday();
|
|
1666
|
-
setPill('ok', 'conclu\u00edda');
|
|
1667
|
-
setTimeout(function() { setPill('ok', 'pronto'); }, 800);
|
|
1668
|
-
} catch (e) {
|
|
1669
|
-
actionsDiv.querySelector('.confirm-btn').disabled = false;
|
|
1670
|
-
actionsDiv.querySelector('.confirm-btn').textContent = 'Confirmar';
|
|
1671
|
-
setPill('err', 'falhou');
|
|
1672
|
-
}
|
|
1673
|
-
};
|
|
1674
|
-
|
|
1675
|
-
actionsDiv.querySelector('.confirm-btn').onclick = doComplete;
|
|
1676
|
-
input.onkeydown = function(e) { if (e.key === 'Enter') doComplete(); };
|
|
1677
|
-
actionsDiv.querySelector('.cancel-btn').onclick = function() {
|
|
1678
|
-
actionsDiv.innerHTML =
|
|
1679
|
-
'<button class="btn small complete-btn" type="button" style="padding:3px 10px; font-size:11px;" '
|
|
1680
|
-
+ 'title="Marcar como conclu\u00edda">\u2713 Concluir</button>'
|
|
1681
|
-
+ '<button class="btn small edit-btn" type="button" style="padding:3px 8px; font-size:11px;">\u270E</button>';
|
|
1682
|
-
actionsDiv.querySelector('.edit-btn').onclick = function() { editTask(t); };
|
|
1683
|
-
attachCompleteHandler();
|
|
1684
|
-
};
|
|
1685
|
-
};
|
|
1686
|
-
};
|
|
1687
|
-
attachCompleteHandler();
|
|
1688
|
-
return row;
|
|
1689
|
-
};
|
|
1690
|
-
|
|
1691
|
-
const createBlockerRow = (b) => {
|
|
1692
|
-
const row = document.createElement('div');
|
|
1693
|
-
const sev = String(b.severity || '').toUpperCase();
|
|
1694
|
-
const color = sevColor(sev);
|
|
1695
|
-
const when = fmtWhen(new Date(b.createdAt || Date.now()).getTime());
|
|
1696
|
-
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;';
|
|
1697
|
-
row.onmouseover = () => row.style.background = 'rgba(239,68,68,0.08)';
|
|
1698
|
-
row.onmouseout = () => row.style.background = 'rgba(239,68,68,0.04)';
|
|
1699
|
-
row.innerHTML =
|
|
1700
|
-
'<div style="display:flex; align-items:flex-start; gap:10px; min-width:0; flex:1;">'
|
|
1701
|
-
+ '<div style="flex-shrink:0; margin-top:1px;">'
|
|
1702
|
-
+ '<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>'
|
|
1703
|
-
+ '</div>'
|
|
1704
|
-
+ '<div style="min-width:0; flex:1;">'
|
|
1705
|
-
+ '<div style="font-weight:600; font-size:13px; color:var(--text);">' + escapeHtml(b.title || '') + '</div>'
|
|
1706
|
-
+ '<div style="font-size:11px; color:var(--muted); margin-top:3px;">🕐 ' + escapeHtml(when) + '</div>'
|
|
1707
|
-
+ '</div></div>'
|
|
1708
|
-
+ '<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>';
|
|
1709
|
-
row.querySelector('.edit-btn').onclick = () => editBlocker(b);
|
|
1710
|
-
return row;
|
|
1711
|
-
};
|
|
1712
|
-
|
|
1713
|
-
for (const key of sortedKeys) {
|
|
1714
|
-
const g = groups[key];
|
|
1715
|
-
const bCount = g.blockers.length;
|
|
1716
|
-
const tCount = g.tasks.length;
|
|
1717
|
-
const hasBlockers = bCount > 0;
|
|
1718
|
-
|
|
1719
|
-
const swimlane = document.createElement('div');
|
|
1720
|
-
swimlane.style.cssText = 'border-bottom:1px solid var(--border); overflow:hidden;';
|
|
1721
|
-
|
|
1722
|
-
const head = document.createElement('div');
|
|
1723
|
-
head.style.cssText = 'display:flex; justify-content:space-between; align-items:center; cursor:pointer; padding:9px 16px; background:var(--bg2); transition:background 0.15s;'
|
|
1724
|
-
+ (hasBlockers ? 'border-left:3px solid #f97316;' : 'border-left:3px solid transparent;');
|
|
1725
|
-
head.onmouseover = () => head.style.background = 'var(--paper2)';
|
|
1726
|
-
head.onmouseout = () => head.style.background = 'var(--bg2)';
|
|
1727
|
-
|
|
1728
|
-
head.innerHTML = `
|
|
1729
|
-
<div style="display:flex; align-items:center; gap:8px; min-width:0;">
|
|
1730
|
-
<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>
|
|
1731
|
-
<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>
|
|
1732
|
-
</div>
|
|
1733
|
-
<div style="display:flex; gap:5px; align-items:center; flex-shrink:0;">
|
|
1734
|
-
${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>` : ''}
|
|
1735
|
-
${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>` : ''}
|
|
1736
|
-
</div>
|
|
1737
|
-
`;
|
|
1738
|
-
|
|
1739
|
-
const body = document.createElement('div');
|
|
1740
|
-
body.style.cssText = 'display:flex; flex-direction:column; background:var(--bg);';
|
|
1741
|
-
|
|
1742
|
-
for (const b of g.blockers) body.appendChild(createBlockerRow(b));
|
|
1743
|
-
for (const t of g.tasks) body.appendChild(createTaskRow(t));
|
|
1744
|
-
|
|
1745
|
-
let isOpen = true;
|
|
1746
|
-
head.onclick = () => {
|
|
1747
|
-
isOpen = !isOpen;
|
|
1748
|
-
body.style.display = isOpen ? 'flex' : 'none';
|
|
1749
|
-
const svg = head.querySelector('svg');
|
|
1750
|
-
if (svg) svg.style.transform = isOpen ? 'rotate(0deg)' : 'rotate(-90deg)';
|
|
1751
|
-
};
|
|
1752
|
-
|
|
1753
|
-
swimlane.appendChild(head);
|
|
1754
|
-
swimlane.appendChild(body);
|
|
1755
|
-
el.appendChild(swimlane);
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1548
|
+
// refreshToday now delegates to loadKanban (dashboard uses embedded kanban board)
|
|
1759
1549
|
async function refreshToday() {
|
|
1760
|
-
|
|
1761
|
-
try {
|
|
1762
|
-
const [t, b] = await Promise.all([
|
|
1763
|
-
api('/api/tasks/list', { dir: dirOrDefault(), category: 'DO_NOW', status: 'PENDING', limit: 50 }),
|
|
1764
|
-
api('/api/blockers/list', { dir: dirOrDefault(), status: 'OPEN', limit: 50 })
|
|
1765
|
-
]);
|
|
1766
|
-
renderSwimlanes((t && t.tasks) || [], (b && b.blockers) || []);
|
|
1767
|
-
refreshBlockersInsights();
|
|
1768
|
-
} catch (e) {
|
|
1769
|
-
// keep silent in background refresh
|
|
1770
|
-
}
|
|
1550
|
+
await loadKanban();
|
|
1771
1551
|
}
|
|
1772
1552
|
|
|
1773
1553
|
function renderBlockersInsights(payload) {
|
|
@@ -2935,16 +2715,45 @@
|
|
|
2935
2715
|
window.askFreyaFromInput = askFreyaFromInput;
|
|
2936
2716
|
|
|
2937
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
|
+
|
|
2938
2745
|
function openQuickAdd() {
|
|
2939
2746
|
const overlay = $('quickAddOverlay');
|
|
2940
2747
|
if (!overlay) return;
|
|
2941
2748
|
overlay.style.display = 'flex';
|
|
2749
|
+
wireQaDatePicker();
|
|
2942
2750
|
const desc = $('qaDesc');
|
|
2943
2751
|
if (desc) { desc.value = ''; desc.focus(); }
|
|
2944
2752
|
var cat = $('qaCat'); if (cat) cat.value = 'DO_NOW';
|
|
2945
2753
|
var pri = $('qaPriority'); if (pri) pri.value = '';
|
|
2946
2754
|
var slug = $('qaSlug'); if (slug) slug.value = '';
|
|
2947
2755
|
var due = $('qaDue'); if (due) due.value = '';
|
|
2756
|
+
var picker = $('qaDuePicker'); if (picker) picker.value = '';
|
|
2948
2757
|
}
|
|
2949
2758
|
|
|
2950
2759
|
function closeQuickAdd() {
|
|
@@ -2952,6 +2761,19 @@
|
|
|
2952
2761
|
if (overlay) overlay.style.display = 'none';
|
|
2953
2762
|
}
|
|
2954
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
|
+
|
|
2955
2777
|
async function submitQuickAdd() {
|
|
2956
2778
|
var desc = $('qaDesc');
|
|
2957
2779
|
var text = desc ? desc.value.trim() : '';
|
|
@@ -2960,7 +2782,7 @@
|
|
|
2960
2782
|
var cat = $('qaCat'); var catVal = cat ? cat.value : 'DO_NOW';
|
|
2961
2783
|
var pri = $('qaPriority'); var priVal = pri ? pri.value : '';
|
|
2962
2784
|
var slug = $('qaSlug'); var slugVal = slug ? slug.value.trim() : '';
|
|
2963
|
-
var due = $('qaDue'); var dueVal = due ? due.value : '';
|
|
2785
|
+
var due = $('qaDue'); var dueVal = due ? parseDateBR(due.value) : '';
|
|
2964
2786
|
|
|
2965
2787
|
var body = { dir: dirOrDefault(), description: text, category: catVal };
|
|
2966
2788
|
if (priVal) body.priority = priVal;
|
|
@@ -2971,8 +2793,7 @@
|
|
|
2971
2793
|
await api('/api/tasks/create', body);
|
|
2972
2794
|
closeQuickAdd();
|
|
2973
2795
|
showToast('ok', 'Task criada');
|
|
2974
|
-
|
|
2975
|
-
else await refreshToday();
|
|
2796
|
+
await loadKanban();
|
|
2976
2797
|
} catch (e) {
|
|
2977
2798
|
showToast('err', 'Erro ao criar task');
|
|
2978
2799
|
}
|
|
@@ -3017,6 +2838,8 @@
|
|
|
3017
2838
|
renderKanban();
|
|
3018
2839
|
renderKanbanBlockers();
|
|
3019
2840
|
loadDelta();
|
|
2841
|
+
// On dashboard, also refresh blockers insights panel
|
|
2842
|
+
if ($('blockersInsightsWrap')) refreshBlockersInsights();
|
|
3020
2843
|
} catch (e) {
|
|
3021
2844
|
showToast('err', 'Erro ao carregar kanban');
|
|
3022
2845
|
}
|
|
@@ -3047,6 +2870,127 @@
|
|
|
3047
2870
|
return _kanbanData.tasks.filter(function(t) { return t.projectSlug === filter; });
|
|
3048
2871
|
}
|
|
3049
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
|
+
|
|
3050
2994
|
function renderKanban() {
|
|
3051
2995
|
var tasks = getFilteredTasks();
|
|
3052
2996
|
var today = new Date().toISOString().slice(0, 10);
|
|
@@ -3093,7 +3037,7 @@
|
|
|
3093
3037
|
if (t.projectSlug) meta.push('<span class="kanban-tag">' + escapeHtml(t.projectSlug) + '</span>');
|
|
3094
3038
|
if (t.dueDate) {
|
|
3095
3039
|
var dueCls = isOverdue ? 'kanban-due overdue' : 'kanban-due';
|
|
3096
|
-
meta.push('<span class="' + dueCls + '">' + escapeHtml(t.dueDate) + '</span>');
|
|
3040
|
+
meta.push('<span class="' + dueCls + '">' + escapeHtml(fmtDateBR(t.dueDate)) + '</span>');
|
|
3097
3041
|
}
|
|
3098
3042
|
if (meta.length) html += '<div class="kanban-card-meta">' + meta.join('') + '</div>';
|
|
3099
3043
|
|
|
@@ -3106,6 +3050,17 @@
|
|
|
3106
3050
|
|
|
3107
3051
|
card.innerHTML = html;
|
|
3108
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
|
+
|
|
3109
3064
|
// Drag events
|
|
3110
3065
|
if (cat !== 'COMPLETED') {
|
|
3111
3066
|
card.addEventListener('dragstart', function(e) {
|
|
@@ -3137,8 +3092,17 @@
|
|
|
3137
3092
|
if (!newCat) return;
|
|
3138
3093
|
var newSlug = prompt('Projeto (slug):', t.projectSlug || '');
|
|
3139
3094
|
if (newSlug === null) return;
|
|
3140
|
-
var
|
|
3141
|
-
|
|
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
|
+
}
|
|
3142
3106
|
try {
|
|
3143
3107
|
await api('/api/tasks/update', {
|
|
3144
3108
|
dir: dirOrDefault(), id: t.id,
|
|
@@ -3204,6 +3168,8 @@
|
|
|
3204
3168
|
+ '<span class="kanban-blocker-title">' + escapeHtml(b.title || '') + '</span>'
|
|
3205
3169
|
+ (b.projectSlug ? '<span class="kanban-tag">' + escapeHtml(b.projectSlug) + '</span>' : '')
|
|
3206
3170
|
+ (b.owner ? '<span class="kanban-owner">' + escapeHtml(b.owner) + '</span>' : '');
|
|
3171
|
+
card.style.cursor = 'pointer';
|
|
3172
|
+
card.addEventListener('click', function() { showDetailPanel(b, 'blocker'); });
|
|
3207
3173
|
list.appendChild(card);
|
|
3208
3174
|
});
|
|
3209
3175
|
}
|
package/cli/web.js
CHANGED
|
@@ -1272,55 +1272,78 @@ function buildHtml(safeDefault, appVersion) {
|
|
|
1272
1272
|
</div>
|
|
1273
1273
|
</div>
|
|
1274
1274
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
<
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
<
|
|
1275
|
+
<!-- Kanban toolbar -->
|
|
1276
|
+
<div class="kanban-toolbar" style="display:flex; justify-content:space-between; align-items:center; padding:0 0 16px; gap:12px; flex-wrap:wrap;">
|
|
1277
|
+
<div style="display:flex; gap:8px; align-items:center;">
|
|
1278
|
+
<select id="kanbanFilterProject" class="kanban-filter" onchange="window.filterKanban()">
|
|
1279
|
+
<option value="">Todos os projetos</option>
|
|
1280
|
+
</select>
|
|
1281
|
+
<button class="btn small" type="button" onclick="window.loadKanban()">
|
|
1282
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:-2px;margin-right:4px"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>
|
|
1283
|
+
Atualizar
|
|
1284
|
+
</button>
|
|
1285
|
+
<div class="statusLine" style="margin:0;">
|
|
1286
|
+
<span class="small" id="last"></span>
|
|
1287
|
+
</div>
|
|
1282
1288
|
</div>
|
|
1289
|
+
<button class="btn primary small" type="button" onclick="window.openQuickAdd()">+ Nova Task</button>
|
|
1283
1290
|
</div>
|
|
1284
1291
|
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
<div class="panelHead" style="background: linear-gradient(90deg, var(--paper2), var(--paper)); border-left: 4px solid var(--accent); flex-shrink:0;">
|
|
1288
|
-
<div style="display:flex; align-items:center; gap:10px;">
|
|
1289
|
-
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--accent); flex-shrink:0;"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
|
|
1290
|
-
<b style="color: var(--text); font-size: 14px;">Foco de Hoje</b>
|
|
1291
|
-
<div id="focusSummaryChips" style="display:flex; gap:5px; margin-left:4px;"></div>
|
|
1292
|
-
</div>
|
|
1293
|
-
<div class="stack">
|
|
1294
|
-
<div id="focusProgressWrap" style="display:none; align-items:center; gap:6px; font-size:11px; color:var(--muted);">
|
|
1295
|
-
<div style="width:80px; height:5px; background:var(--border); border-radius:3px; overflow:hidden;">
|
|
1296
|
-
<div id="focusProgressBar" style="height:100%; background:var(--accent); border-radius:3px; transition:width 0.4s; width:0%"></div>
|
|
1297
|
-
</div>
|
|
1298
|
-
<span id="focusProgressLabel"></span>
|
|
1299
|
-
</div>
|
|
1300
|
-
<button class="btn small" type="button" onclick="refreshToday()">
|
|
1301
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:-2px;margin-right:4px"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>
|
|
1302
|
-
Atualizar
|
|
1303
|
-
</button>
|
|
1304
|
-
</div>
|
|
1305
|
-
</div>
|
|
1292
|
+
<!-- Delta banner -->
|
|
1293
|
+
<div id="kanbanDelta" style="display:none;"></div>
|
|
1306
1294
|
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1295
|
+
<!-- Kanban columns -->
|
|
1296
|
+
<div id="kanbanBoard" class="kanban-board">
|
|
1297
|
+
<div class="kanban-col" data-category="DO_NOW">
|
|
1298
|
+
<div class="kanban-col-head do-now">
|
|
1299
|
+
<span class="kanban-col-title">DO NOW</span>
|
|
1300
|
+
<span class="kanban-col-count" id="countDoNow">0</span>
|
|
1301
|
+
</div>
|
|
1302
|
+
<div class="kanban-col-body" id="colDoNow"></div>
|
|
1303
|
+
</div>
|
|
1304
|
+
<div class="kanban-col" data-category="SCHEDULE">
|
|
1305
|
+
<div class="kanban-col-head schedule">
|
|
1306
|
+
<span class="kanban-col-title">SCHEDULE</span>
|
|
1307
|
+
<span class="kanban-col-count" id="countSchedule">0</span>
|
|
1308
|
+
</div>
|
|
1309
|
+
<div class="kanban-col-body" id="colSchedule"></div>
|
|
1310
|
+
</div>
|
|
1311
|
+
<div class="kanban-col" data-category="DELEGATE">
|
|
1312
|
+
<div class="kanban-col-head delegate">
|
|
1313
|
+
<span class="kanban-col-title">DELEGATE</span>
|
|
1314
|
+
<span class="kanban-col-count" id="countDelegate">0</span>
|
|
1315
|
+
</div>
|
|
1316
|
+
<div class="kanban-col-body" id="colDelegate"></div>
|
|
1317
|
+
</div>
|
|
1318
|
+
<div class="kanban-col" data-category="COMPLETED">
|
|
1319
|
+
<div class="kanban-col-head done">
|
|
1320
|
+
<span class="kanban-col-title">DONE (7d)</span>
|
|
1321
|
+
<span class="kanban-col-count" id="countDone">0</span>
|
|
1310
1322
|
</div>
|
|
1323
|
+
<div class="kanban-col-body" id="colDone"></div>
|
|
1324
|
+
</div>
|
|
1325
|
+
</div>
|
|
1311
1326
|
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1327
|
+
<!-- Blockers strip below kanban -->
|
|
1328
|
+
<div id="kanbanBlockers" style="margin-top:20px; display:none;">
|
|
1329
|
+
<div style="font-size:13px; font-weight:700; color:var(--text); margin-bottom:8px; display:flex; align-items:center; gap:6px;">
|
|
1330
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
|
1331
|
+
Blockers Ativos
|
|
1332
|
+
</div>
|
|
1333
|
+
<div id="kanbanBlockersList" class="kanban-blockers-list"></div>
|
|
1334
|
+
</div>
|
|
1335
|
+
|
|
1336
|
+
<!-- Insights de bloqueios -->
|
|
1337
|
+
<div id="blockersInsightsWrap" style="margin-top:16px; border-top: 1px solid var(--border); display:none;">
|
|
1338
|
+
<div style="display:flex; justify-content:space-between; align-items:center; padding: 8px 0 4px;">
|
|
1339
|
+
<div style="font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:0.5px; color:var(--muted);">
|
|
1340
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:-1px;margin-right:4px"><path d="M12 2a10 10 0 1 0 0 20A10 10 0 0 0 12 2z"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
|
|
1341
|
+
Insights de Bloqueios
|
|
1322
1342
|
</div>
|
|
1323
|
-
|
|
1343
|
+
<button class="btn small" type="button" onclick="refreshBlockersInsights()" style="font-size:10px; padding:2px 6px;">↻</button>
|
|
1344
|
+
</div>
|
|
1345
|
+
<div id="blockersInsights" style="padding: 4px 0 12px; font-size:12px; color:var(--muted);"></div>
|
|
1346
|
+
</div>
|
|
1324
1347
|
|
|
1325
1348
|
</div>
|
|
1326
1349
|
</main>
|
|
@@ -1352,7 +1375,11 @@ function buildHtml(safeDefault, appVersion) {
|
|
|
1352
1375
|
</div>
|
|
1353
1376
|
<div class="qa-row">
|
|
1354
1377
|
<input id="qaSlug" class="qa-input" placeholder="Projeto (slug)" style="flex:1;" />
|
|
1355
|
-
<
|
|
1378
|
+
<div class="qa-date-wrap">
|
|
1379
|
+
<input id="qaDue" type="text" class="qa-input qa-date-text" placeholder="dd/mm/aaaa" maxlength="10" />
|
|
1380
|
+
<input id="qaDuePicker" type="date" class="qa-date-hidden" tabindex="-1" />
|
|
1381
|
+
<button type="button" class="qa-date-btn" onclick="var p=this.parentElement.querySelector('#qaDuePicker'); p.showPicker ? p.showPicker() : p.click();" title="Abrir calendario">📅</button>
|
|
1382
|
+
</div>
|
|
1356
1383
|
</div>
|
|
1357
1384
|
<div class="qa-row" style="justify-content:flex-end;">
|
|
1358
1385
|
<button class="btn small" type="button" onclick="window.closeQuickAdd()">Cancelar</button>
|
|
@@ -4256,7 +4283,10 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
4256
4283
|
dueDate: t.due_date || null,
|
|
4257
4284
|
projectSlug: t.project_slug,
|
|
4258
4285
|
priority: meta.priority,
|
|
4259
|
-
streamSlug: meta.streamSlug
|
|
4286
|
+
streamSlug: meta.streamSlug,
|
|
4287
|
+
comments: meta.comments || [],
|
|
4288
|
+
source: meta.source || null,
|
|
4289
|
+
metadata: meta
|
|
4260
4290
|
};
|
|
4261
4291
|
});
|
|
4262
4292
|
|
|
@@ -4265,15 +4295,23 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
4265
4295
|
ORDER BY
|
|
4266
4296
|
CASE severity WHEN 'CRITICAL' THEN 0 WHEN 'HIGH' THEN 1 WHEN 'MEDIUM' THEN 2 WHEN 'LOW' THEN 3 ELSE 9 END ASC,
|
|
4267
4297
|
created_at ASC
|
|
4268
|
-
`).all().map(b =>
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4298
|
+
`).all().map(b => {
|
|
4299
|
+
let meta = {};
|
|
4300
|
+
try { meta = b.metadata ? JSON.parse(b.metadata) : {}; } catch { meta = {}; }
|
|
4301
|
+
return {
|
|
4302
|
+
id: b.id,
|
|
4303
|
+
title: b.title,
|
|
4304
|
+
severity: b.severity,
|
|
4305
|
+
status: b.status,
|
|
4306
|
+
projectSlug: b.project_slug,
|
|
4307
|
+
owner: b.owner,
|
|
4308
|
+
nextAction: b.next_action,
|
|
4309
|
+
createdAt: b.created_at,
|
|
4310
|
+
resolvedAt: b.resolved_at,
|
|
4311
|
+
source: meta.source || null,
|
|
4312
|
+
metadata: meta
|
|
4313
|
+
};
|
|
4314
|
+
});
|
|
4277
4315
|
|
|
4278
4316
|
return safeJson(res, 200, { ok: true, tasks, blockers: openBlockers });
|
|
4279
4317
|
}
|
|
@@ -4675,7 +4713,11 @@ function buildKanbanHtml(safeDefault, appVersion) {
|
|
|
4675
4713
|
</div>
|
|
4676
4714
|
<div class="qa-row">
|
|
4677
4715
|
<input id="qaSlug" class="qa-input" placeholder="Projeto (slug)" style="flex:1;" />
|
|
4678
|
-
<
|
|
4716
|
+
<div class="qa-date-wrap">
|
|
4717
|
+
<input id="qaDue" type="text" class="qa-input qa-date-text" placeholder="dd/mm/aaaa" maxlength="10" />
|
|
4718
|
+
<input id="qaDuePicker" type="date" class="qa-date-hidden" tabindex="-1" />
|
|
4719
|
+
<button type="button" class="qa-date-btn" onclick="var p=this.parentElement.querySelector('#qaDuePicker'); p.showPicker ? p.showPicker() : p.click();" title="Abrir calendario">📅</button>
|
|
4720
|
+
</div>
|
|
4679
4721
|
</div>
|
|
4680
4722
|
<div class="qa-row" style="justify-content:flex-end;">
|
|
4681
4723
|
<button class="btn small" type="button" onclick="window.closeQuickAdd()">Cancelar</button>
|
package/package.json
CHANGED