@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.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
- function renderSwimlanes(tasks, blockers) {
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
- showSkeleton('swimlaneContainer');
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
- if (isKanbanPage) await loadKanban();
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()">&times;</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 newDue = prompt('Due date (YYYY-MM-DD):', t.dueDate || '');
3141
- if (newDue === null) return;
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
  }