@cccarv82/freya 2.15.0 → 2.17.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 -306
- package/cli/web.js +445 -550
- package/package.json +2 -3
- package/scripts/build-vector-index.js +87 -35
- package/templates/base/scripts/build-vector-index.js +87 -35
- package/scripts/generate-weekly-report.js +0 -128
package/cli/web-ui.js
CHANGED
|
@@ -1283,83 +1283,6 @@
|
|
|
1283
1283
|
}
|
|
1284
1284
|
}
|
|
1285
1285
|
|
|
1286
|
-
async function refreshIncidents() {
|
|
1287
|
-
try {
|
|
1288
|
-
const r = await api('/api/incidents', { dir: dirOrDefault() });
|
|
1289
|
-
const el = $('incidentsBox');
|
|
1290
|
-
if (el) {
|
|
1291
|
-
const md = r.markdown || '';
|
|
1292
|
-
if (!md) { el.innerHTML = '<div class="help">Nenhum incidente registrado.</div>'; return; }
|
|
1293
|
-
const lines = md.split(/\n/);
|
|
1294
|
-
const cards = [];
|
|
1295
|
-
let current = null;
|
|
1296
|
-
for (const line of lines) {
|
|
1297
|
-
if (line.startsWith('- **')) {
|
|
1298
|
-
if (current) cards.push(current);
|
|
1299
|
-
current = { title: line.replace('- **', '').replace('**', '').trim(), body: [] };
|
|
1300
|
-
} else if (current && line.trim().startsWith('- ')) {
|
|
1301
|
-
current.body.push(line.trim().replace(/^- /, ''));
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
if (current) cards.push(current);
|
|
1305
|
-
el.innerHTML = '';
|
|
1306
|
-
if (!cards.length) { el.innerHTML = renderMarkdown(md); return; }
|
|
1307
|
-
for (let idx = 0; idx < cards.length; idx++) {
|
|
1308
|
-
const c = cards[idx];
|
|
1309
|
-
const card = document.createElement('div');
|
|
1310
|
-
card.className = 'reportCard';
|
|
1311
|
-
const dateLine = c.body.find((b) => b.toLowerCase().includes('data'));
|
|
1312
|
-
const impactLine = c.body.find((b) => b.toLowerCase().includes('descricao') || b.toLowerCase().includes('impacto'));
|
|
1313
|
-
const statusLine = c.body.find((b) => /^status\s*:/i.test(b));
|
|
1314
|
-
const statusRaw = statusLine ? statusLine.split(':').slice(1).join(':').trim().toLowerCase() : '';
|
|
1315
|
-
let statusKey = '';
|
|
1316
|
-
if (['open', 'aberto', 'aberta'].includes(statusRaw)) statusKey = 'open';
|
|
1317
|
-
else if (['mitigating', 'mitigando', 'mitigacao', 'mitigação'].includes(statusRaw)) statusKey = 'mitigating';
|
|
1318
|
-
else if (['resolved', 'resolvido', 'resolvida', 'closed', 'fechado', 'fechada'].includes(statusRaw)) statusKey = 'resolved';
|
|
1319
|
-
|
|
1320
|
-
card.innerHTML = '<div class="reportTitle">' + escapeHtml(c.title) + '</div>'
|
|
1321
|
-
+ (dateLine ? ('<div class="reportMeta">' + escapeHtml(dateLine) + '</div>') : '')
|
|
1322
|
-
+ (impactLine ? ('<div class="help" style="margin-top:4px">' + escapeHtml(impactLine) + '</div>') : '')
|
|
1323
|
-
+ c.body.filter((b) => b !== dateLine && b !== impactLine && b !== statusLine).map((b) => '<div class="help" style="margin-top:4px">' + escapeHtml(b) + '</div>').join('');
|
|
1324
|
-
|
|
1325
|
-
if (statusKey) {
|
|
1326
|
-
const actions = document.createElement('div');
|
|
1327
|
-
actions.className = 'reportActions';
|
|
1328
|
-
actions.style.display = 'flex';
|
|
1329
|
-
actions.style.gap = '8px';
|
|
1330
|
-
actions.style.marginTop = '8px';
|
|
1331
|
-
actions.style.flexWrap = 'wrap';
|
|
1332
|
-
|
|
1333
|
-
const label = statusKey === 'open' ? 'aberto' : (statusKey === 'mitigating' ? 'mitigando' : 'resolvido');
|
|
1334
|
-
const pillClass = statusKey === 'resolved' ? 'ok' : (statusKey === 'mitigating' ? 'info' : 'warn');
|
|
1335
|
-
const pill = document.createElement('span');
|
|
1336
|
-
pill.className = 'pill ' + pillClass;
|
|
1337
|
-
pill.textContent = label;
|
|
1338
|
-
actions.appendChild(pill);
|
|
1339
|
-
|
|
1340
|
-
if (statusKey !== 'resolved') {
|
|
1341
|
-
const btn = document.createElement('button');
|
|
1342
|
-
btn.className = 'btn small';
|
|
1343
|
-
btn.type = 'button';
|
|
1344
|
-
btn.textContent = 'Marcar resolvido';
|
|
1345
|
-
btn.onclick = async () => {
|
|
1346
|
-
await api('/api/incidents/resolve', { dir: dirOrDefault(), title: c.title, index: idx });
|
|
1347
|
-
await refreshIncidents();
|
|
1348
|
-
};
|
|
1349
|
-
actions.appendChild(btn);
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
card.appendChild(actions);
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
el.appendChild(card);
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
} catch {
|
|
1359
|
-
const el = $('incidentsBox');
|
|
1360
|
-
if (el) el.textContent = 'Falha ao carregar incidentes.';
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
1286
|
|
|
1364
1287
|
function setHeatmapSort(sort) {
|
|
1365
1288
|
state.heatmapSort = sort;
|
|
@@ -1545,229 +1468,9 @@
|
|
|
1545
1468
|
return '#94a3b8';
|
|
1546
1469
|
}
|
|
1547
1470
|
|
|
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
|
-
|
|
1471
|
+
// refreshToday now delegates to loadKanban (dashboard uses embedded kanban board)
|
|
1759
1472
|
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
|
-
}
|
|
1473
|
+
await loadKanban();
|
|
1771
1474
|
}
|
|
1772
1475
|
|
|
1773
1476
|
function renderBlockersInsights(payload) {
|
|
@@ -2906,7 +2609,6 @@
|
|
|
2906
2609
|
window.refreshProjects = refreshProjects;
|
|
2907
2610
|
window.refreshTimeline = refreshTimeline;
|
|
2908
2611
|
window.refreshGraph = refreshGraph;
|
|
2909
|
-
window.refreshIncidents = refreshIncidents;
|
|
2910
2612
|
window.refreshHeatmap = refreshHeatmap;
|
|
2911
2613
|
window.setHeatmapSort = setHeatmapSort;
|
|
2912
2614
|
window.setTimelineKind = setTimelineKind;
|
|
@@ -2935,16 +2637,45 @@
|
|
|
2935
2637
|
window.askFreyaFromInput = askFreyaFromInput;
|
|
2936
2638
|
|
|
2937
2639
|
/* ── Quick-Add Modal ── */
|
|
2640
|
+
|
|
2641
|
+
// Wire hidden date picker → text input (once)
|
|
2642
|
+
var _qaDateWired = false;
|
|
2643
|
+
function wireQaDatePicker() {
|
|
2644
|
+
if (_qaDateWired) return;
|
|
2645
|
+
var picker = $('qaDuePicker');
|
|
2646
|
+
var text = $('qaDue');
|
|
2647
|
+
if (!picker || !text) return;
|
|
2648
|
+
_qaDateWired = true;
|
|
2649
|
+
picker.addEventListener('change', function() {
|
|
2650
|
+
if (picker.value) {
|
|
2651
|
+
text.value = fmtDateBR(picker.value);
|
|
2652
|
+
}
|
|
2653
|
+
});
|
|
2654
|
+
// Auto-format on blur: accept dd/mm/aaaa typed manually
|
|
2655
|
+
text.addEventListener('keyup', function(e) {
|
|
2656
|
+
var v = text.value.replace(/[^\d]/g, '');
|
|
2657
|
+
if (v.length >= 3 && text.value.indexOf('/') < 0) {
|
|
2658
|
+
// Auto-insert slashes: dd/mm/aaaa
|
|
2659
|
+
var formatted = v.slice(0, 2);
|
|
2660
|
+
if (v.length >= 3) formatted += '/' + v.slice(2, 4);
|
|
2661
|
+
if (v.length >= 5) formatted += '/' + v.slice(4, 8);
|
|
2662
|
+
text.value = formatted;
|
|
2663
|
+
}
|
|
2664
|
+
});
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2938
2667
|
function openQuickAdd() {
|
|
2939
2668
|
const overlay = $('quickAddOverlay');
|
|
2940
2669
|
if (!overlay) return;
|
|
2941
2670
|
overlay.style.display = 'flex';
|
|
2671
|
+
wireQaDatePicker();
|
|
2942
2672
|
const desc = $('qaDesc');
|
|
2943
2673
|
if (desc) { desc.value = ''; desc.focus(); }
|
|
2944
2674
|
var cat = $('qaCat'); if (cat) cat.value = 'DO_NOW';
|
|
2945
2675
|
var pri = $('qaPriority'); if (pri) pri.value = '';
|
|
2946
2676
|
var slug = $('qaSlug'); if (slug) slug.value = '';
|
|
2947
2677
|
var due = $('qaDue'); if (due) due.value = '';
|
|
2678
|
+
var picker = $('qaDuePicker'); if (picker) picker.value = '';
|
|
2948
2679
|
}
|
|
2949
2680
|
|
|
2950
2681
|
function closeQuickAdd() {
|
|
@@ -2952,6 +2683,19 @@
|
|
|
2952
2683
|
if (overlay) overlay.style.display = 'none';
|
|
2953
2684
|
}
|
|
2954
2685
|
|
|
2686
|
+
// Parse dd/mm/aaaa → YYYY-MM-DD
|
|
2687
|
+
function parseDateBR(str) {
|
|
2688
|
+
if (!str) return '';
|
|
2689
|
+
var s = str.trim();
|
|
2690
|
+
if (s.indexOf('/') > -1) {
|
|
2691
|
+
var p = s.split('/');
|
|
2692
|
+
if (p.length === 3 && p[0].length === 2 && p[1].length === 2 && p[2].length === 4) {
|
|
2693
|
+
return p[2] + '-' + p[1] + '-' + p[0];
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
return s; // fallback: pass as-is (may be YYYY-MM-DD already)
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2955
2699
|
async function submitQuickAdd() {
|
|
2956
2700
|
var desc = $('qaDesc');
|
|
2957
2701
|
var text = desc ? desc.value.trim() : '';
|
|
@@ -2960,7 +2704,7 @@
|
|
|
2960
2704
|
var cat = $('qaCat'); var catVal = cat ? cat.value : 'DO_NOW';
|
|
2961
2705
|
var pri = $('qaPriority'); var priVal = pri ? pri.value : '';
|
|
2962
2706
|
var slug = $('qaSlug'); var slugVal = slug ? slug.value.trim() : '';
|
|
2963
|
-
var due = $('qaDue'); var dueVal = due ? due.value : '';
|
|
2707
|
+
var due = $('qaDue'); var dueVal = due ? parseDateBR(due.value) : '';
|
|
2964
2708
|
|
|
2965
2709
|
var body = { dir: dirOrDefault(), description: text, category: catVal };
|
|
2966
2710
|
if (priVal) body.priority = priVal;
|
|
@@ -2971,8 +2715,7 @@
|
|
|
2971
2715
|
await api('/api/tasks/create', body);
|
|
2972
2716
|
closeQuickAdd();
|
|
2973
2717
|
showToast('ok', 'Task criada');
|
|
2974
|
-
|
|
2975
|
-
else await refreshToday();
|
|
2718
|
+
await loadKanban();
|
|
2976
2719
|
} catch (e) {
|
|
2977
2720
|
showToast('err', 'Erro ao criar task');
|
|
2978
2721
|
}
|
|
@@ -3017,6 +2760,8 @@
|
|
|
3017
2760
|
renderKanban();
|
|
3018
2761
|
renderKanbanBlockers();
|
|
3019
2762
|
loadDelta();
|
|
2763
|
+
// On dashboard, also refresh blockers insights panel
|
|
2764
|
+
if ($('blockersInsightsWrap')) refreshBlockersInsights();
|
|
3020
2765
|
} catch (e) {
|
|
3021
2766
|
showToast('err', 'Erro ao carregar kanban');
|
|
3022
2767
|
}
|
|
@@ -3047,6 +2792,127 @@
|
|
|
3047
2792
|
return _kanbanData.tasks.filter(function(t) { return t.projectSlug === filter; });
|
|
3048
2793
|
}
|
|
3049
2794
|
|
|
2795
|
+
// Format ISO date (YYYY-MM-DD or full ISO) to dd/mm/aaaa
|
|
2796
|
+
function fmtDateBR(isoStr) {
|
|
2797
|
+
if (!isoStr) return '';
|
|
2798
|
+
var d = isoStr.slice(0, 10); // YYYY-MM-DD
|
|
2799
|
+
var parts = d.split('-');
|
|
2800
|
+
if (parts.length !== 3) return d;
|
|
2801
|
+
return parts[2] + '/' + parts[1] + '/' + parts[0];
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
// Format ISO datetime to dd/mm/aaaa HH:mm
|
|
2805
|
+
function fmtDateTimeBR(isoStr) {
|
|
2806
|
+
if (!isoStr) return '';
|
|
2807
|
+
var dt = new Date(isoStr);
|
|
2808
|
+
if (isNaN(dt.getTime())) return isoStr;
|
|
2809
|
+
var dd = String(dt.getDate()).padStart(2, '0');
|
|
2810
|
+
var mm = String(dt.getMonth() + 1).padStart(2, '0');
|
|
2811
|
+
var yyyy = dt.getFullYear();
|
|
2812
|
+
var hh = String(dt.getHours()).padStart(2, '0');
|
|
2813
|
+
var min = String(dt.getMinutes()).padStart(2, '0');
|
|
2814
|
+
return dd + '/' + mm + '/' + yyyy + ' ' + hh + ':' + min;
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
// Show detail panel for a task or blocker
|
|
2818
|
+
function showDetailPanel(item, type) {
|
|
2819
|
+
// Remove existing panel
|
|
2820
|
+
var old = document.querySelector('.detail-panel-overlay');
|
|
2821
|
+
if (old) old.remove();
|
|
2822
|
+
|
|
2823
|
+
var overlay = document.createElement('div');
|
|
2824
|
+
overlay.className = 'detail-panel-overlay';
|
|
2825
|
+
overlay.onclick = function(e) { if (e.target === overlay) overlay.remove(); };
|
|
2826
|
+
|
|
2827
|
+
var panel = document.createElement('div');
|
|
2828
|
+
panel.className = 'detail-panel';
|
|
2829
|
+
|
|
2830
|
+
var isTask = type === 'task';
|
|
2831
|
+
var pc = isTask ? priColor(item.priority) : null;
|
|
2832
|
+
|
|
2833
|
+
var html = '<div class="detail-panel-header">';
|
|
2834
|
+
html += '<div style="display:flex; align-items:center; gap:10px; flex:1; min-width:0;">';
|
|
2835
|
+
if (isTask && pc) {
|
|
2836
|
+
html += '<span class="kanban-pri-dot" style="background:' + pc.dot + '; width:12px; height:12px;"></span>';
|
|
2837
|
+
} else {
|
|
2838
|
+
var sColor = sevColor(item.severity);
|
|
2839
|
+
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>';
|
|
2840
|
+
}
|
|
2841
|
+
html += '<span class="detail-panel-title">' + escapeHtml(isTask ? item.description : item.title) + '</span>';
|
|
2842
|
+
html += '</div>';
|
|
2843
|
+
html += '<button class="detail-panel-close" onclick="this.closest(\'.detail-panel-overlay\').remove()">×</button>';
|
|
2844
|
+
html += '</div>';
|
|
2845
|
+
|
|
2846
|
+
// Info grid
|
|
2847
|
+
html += '<div class="detail-panel-grid">';
|
|
2848
|
+
if (isTask) {
|
|
2849
|
+
html += detailRow('ID', item.id);
|
|
2850
|
+
html += detailRow('Status', item.status);
|
|
2851
|
+
html += detailRow('Categoria', item.category);
|
|
2852
|
+
html += detailRow('Prioridade', pc ? pc.label : 'Nenhuma');
|
|
2853
|
+
html += detailRow('Projeto', item.projectSlug || '—');
|
|
2854
|
+
html += detailRow('Stream', item.streamSlug || '—');
|
|
2855
|
+
html += detailRow('Due Date', item.dueDate ? fmtDateBR(item.dueDate) : '—');
|
|
2856
|
+
html += detailRow('Criada em', fmtDateTimeBR(item.createdAt));
|
|
2857
|
+
if (item.completedAt) html += detailRow('Concluida em', fmtDateTimeBR(item.completedAt));
|
|
2858
|
+
if (item.source) html += detailRow('Fonte', item.source);
|
|
2859
|
+
} else {
|
|
2860
|
+
html += detailRow('ID', item.id);
|
|
2861
|
+
html += detailRow('Severidade', item.severity);
|
|
2862
|
+
html += detailRow('Status', item.status);
|
|
2863
|
+
html += detailRow('Projeto', item.projectSlug || '—');
|
|
2864
|
+
html += detailRow('Responsavel', item.owner || '—');
|
|
2865
|
+
html += detailRow('Proxima Acao', item.nextAction || '—');
|
|
2866
|
+
html += detailRow('Criado em', fmtDateTimeBR(item.createdAt));
|
|
2867
|
+
if (item.resolvedAt) html += detailRow('Resolvido em', fmtDateTimeBR(item.resolvedAt));
|
|
2868
|
+
if (item.source) html += detailRow('Fonte', item.source);
|
|
2869
|
+
}
|
|
2870
|
+
html += '</div>';
|
|
2871
|
+
|
|
2872
|
+
// Comments section (tasks only)
|
|
2873
|
+
if (isTask && item.comments && item.comments.length > 0) {
|
|
2874
|
+
html += '<div class="detail-panel-section">';
|
|
2875
|
+
html += '<div class="detail-panel-section-title">Comentarios</div>';
|
|
2876
|
+
item.comments.forEach(function(c) {
|
|
2877
|
+
var cObj = typeof c === 'string' ? { text: c } : c;
|
|
2878
|
+
html += '<div class="detail-panel-comment">';
|
|
2879
|
+
if (cObj.at) html += '<span class="detail-panel-comment-date">' + fmtDateTimeBR(cObj.at) + '</span>';
|
|
2880
|
+
html += '<span>' + escapeHtml(cObj.text || cObj.comment || String(c)) + '</span>';
|
|
2881
|
+
html += '</div>';
|
|
2882
|
+
});
|
|
2883
|
+
html += '</div>';
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
// Metadata section (raw, if has extra fields)
|
|
2887
|
+
var metaKeys = item.metadata ? Object.keys(item.metadata).filter(function(k) {
|
|
2888
|
+
return ['priority', 'streamSlug', 'comments', 'source'].indexOf(k) < 0 && item.metadata[k];
|
|
2889
|
+
}) : [];
|
|
2890
|
+
if (metaKeys.length > 0) {
|
|
2891
|
+
html += '<div class="detail-panel-section">';
|
|
2892
|
+
html += '<div class="detail-panel-section-title">Metadados Adicionais</div>';
|
|
2893
|
+
html += '<div class="detail-panel-grid">';
|
|
2894
|
+
metaKeys.forEach(function(k) {
|
|
2895
|
+
html += detailRow(k, typeof item.metadata[k] === 'object' ? JSON.stringify(item.metadata[k]) : String(item.metadata[k]));
|
|
2896
|
+
});
|
|
2897
|
+
html += '</div></div>';
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
panel.innerHTML = html;
|
|
2901
|
+
overlay.appendChild(panel);
|
|
2902
|
+
document.body.appendChild(overlay);
|
|
2903
|
+
|
|
2904
|
+
// Close on Escape
|
|
2905
|
+
var escHandler = function(e) {
|
|
2906
|
+
if (e.key === 'Escape') { overlay.remove(); document.removeEventListener('keydown', escHandler); }
|
|
2907
|
+
};
|
|
2908
|
+
document.addEventListener('keydown', escHandler);
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
function detailRow(label, value) {
|
|
2912
|
+
return '<div class="detail-row-label">' + escapeHtml(label) + '</div>'
|
|
2913
|
+
+ '<div class="detail-row-value">' + escapeHtml(String(value || '')) + '</div>';
|
|
2914
|
+
}
|
|
2915
|
+
|
|
3050
2916
|
function renderKanban() {
|
|
3051
2917
|
var tasks = getFilteredTasks();
|
|
3052
2918
|
var today = new Date().toISOString().slice(0, 10);
|
|
@@ -3093,7 +2959,7 @@
|
|
|
3093
2959
|
if (t.projectSlug) meta.push('<span class="kanban-tag">' + escapeHtml(t.projectSlug) + '</span>');
|
|
3094
2960
|
if (t.dueDate) {
|
|
3095
2961
|
var dueCls = isOverdue ? 'kanban-due overdue' : 'kanban-due';
|
|
3096
|
-
meta.push('<span class="' + dueCls + '">' + escapeHtml(t.dueDate) + '</span>');
|
|
2962
|
+
meta.push('<span class="' + dueCls + '">' + escapeHtml(fmtDateBR(t.dueDate)) + '</span>');
|
|
3097
2963
|
}
|
|
3098
2964
|
if (meta.length) html += '<div class="kanban-card-meta">' + meta.join('') + '</div>';
|
|
3099
2965
|
|
|
@@ -3106,6 +2972,17 @@
|
|
|
3106
2972
|
|
|
3107
2973
|
card.innerHTML = html;
|
|
3108
2974
|
|
|
2975
|
+
// Click to show detail panel (only if not dragging)
|
|
2976
|
+
var _dragged = false;
|
|
2977
|
+
card.addEventListener('mousedown', function() { _dragged = false; });
|
|
2978
|
+
card.addEventListener('mousemove', function() { _dragged = true; });
|
|
2979
|
+
card.addEventListener('click', function(e) {
|
|
2980
|
+
if (_dragged) return;
|
|
2981
|
+
// Don't trigger on action buttons
|
|
2982
|
+
if (e.target.closest('.kanban-card-actions')) return;
|
|
2983
|
+
showDetailPanel(t, 'task');
|
|
2984
|
+
});
|
|
2985
|
+
|
|
3109
2986
|
// Drag events
|
|
3110
2987
|
if (cat !== 'COMPLETED') {
|
|
3111
2988
|
card.addEventListener('dragstart', function(e) {
|
|
@@ -3137,8 +3014,17 @@
|
|
|
3137
3014
|
if (!newCat) return;
|
|
3138
3015
|
var newSlug = prompt('Projeto (slug):', t.projectSlug || '');
|
|
3139
3016
|
if (newSlug === null) return;
|
|
3140
|
-
var
|
|
3141
|
-
|
|
3017
|
+
var currentDueBR = t.dueDate ? fmtDateBR(t.dueDate) : '';
|
|
3018
|
+
var newDueBR = prompt('Due date (dd/mm/aaaa):', currentDueBR);
|
|
3019
|
+
if (newDueBR === null) return;
|
|
3020
|
+
// Convert dd/mm/aaaa back to YYYY-MM-DD for API
|
|
3021
|
+
var newDue = '';
|
|
3022
|
+
if (newDueBR && newDueBR.indexOf('/') > -1) {
|
|
3023
|
+
var dp = newDueBR.split('/');
|
|
3024
|
+
if (dp.length === 3) newDue = dp[2] + '-' + dp[1] + '-' + dp[0];
|
|
3025
|
+
} else {
|
|
3026
|
+
newDue = newDueBR; // fallback: accept YYYY-MM-DD too
|
|
3027
|
+
}
|
|
3142
3028
|
try {
|
|
3143
3029
|
await api('/api/tasks/update', {
|
|
3144
3030
|
dir: dirOrDefault(), id: t.id,
|
|
@@ -3204,6 +3090,8 @@
|
|
|
3204
3090
|
+ '<span class="kanban-blocker-title">' + escapeHtml(b.title || '') + '</span>'
|
|
3205
3091
|
+ (b.projectSlug ? '<span class="kanban-tag">' + escapeHtml(b.projectSlug) + '</span>' : '')
|
|
3206
3092
|
+ (b.owner ? '<span class="kanban-owner">' + escapeHtml(b.owner) + '</span>' : '');
|
|
3093
|
+
card.style.cursor = 'pointer';
|
|
3094
|
+
card.addEventListener('click', function() { showDetailPanel(b, 'blocker'); });
|
|
3207
3095
|
list.appendChild(card);
|
|
3208
3096
|
});
|
|
3209
3097
|
}
|