Files
clawd/kanban/index.html
Echo fdabea7271 Dashboard v2: Activity + Issues panels
- Replace Kanban with new Dashboard layout
- Status bar (ANAF, Git, Cron) - collapsible
- Activity panel - Clawdbot activity log
- Issues panel - Eisenhower priority, owner filter, programs
- All sections collapsible with localStorage persistence
- issues.json for ROA work tracking
- First issue: D101 RD49→RD50 fix
- Fix notes.html index.json format handling
2026-01-30 19:48:01 +00:00

1291 lines
42 KiB
HTML

<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Echo · Dashboard</title>
<link rel="stylesheet" href="common.css">
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
<script src="swipe-nav.js"></script>
<style>
.main {
max-width: 1400px;
margin: 0 auto;
padding: var(--space-5);
}
.page-header {
margin-bottom: var(--space-4);
}
.page-title {
font-size: var(--text-xl);
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--space-1);
}
.page-subtitle {
font-size: var(--text-sm);
color: var(--text-muted);
}
/* Status bar */
.status-bar {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
margin-bottom: var(--space-4);
overflow: hidden;
}
.status-header {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
background: linear-gradient(135deg, rgba(34, 197, 94, 0.15), rgba(22, 163, 74, 0.1));
border-bottom: 1px solid var(--border);
cursor: pointer;
user-select: none;
}
.status-header:hover {
filter: brightness(1.05);
}
.status-title {
display: flex;
align-items: center;
gap: var(--space-2);
font-weight: 600;
font-size: var(--text-sm);
color: var(--text-primary);
}
.status-title svg {
width: 16px;
height: 16px;
color: #22c55e;
}
.status-summary {
flex: 1;
font-size: var(--text-xs);
color: var(--text-muted);
text-align: right;
}
.status-toggle {
width: 16px;
height: 16px;
color: var(--text-muted);
transition: transform var(--transition-fast);
}
.status-bar.collapsed .status-toggle {
transform: rotate(-90deg);
}
.status-bar.collapsed .status-content {
display: none;
}
.status-content {
padding: var(--space-3) var(--space-4);
}
.status-row {
display: flex;
flex-wrap: wrap;
gap: var(--space-4);
margin-bottom: var(--space-2);
}
.status-item {
display: flex;
align-items: center;
gap: var(--space-1);
font-size: var(--text-sm);
}
.status-label {
color: var(--text-muted);
}
.status-value {
font-weight: 600;
color: var(--text-primary);
}
.status-value.ok { color: #22c55e; }
.status-value.warning { color: #f59e0b; }
.status-value.error { color: #ef4444; }
.status-time {
font-size: var(--text-xs);
color: var(--text-muted);
margin-left: var(--space-1);
}
.cron-row {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: var(--text-sm);
padding-top: var(--space-2);
border-top: 1px solid var(--border);
}
.cron-label {
color: var(--text-muted);
}
.cron-list {
color: var(--text-secondary);
}
.cron-done {
color: var(--text-muted);
text-decoration: line-through;
}
/* Two-column dashboard */
.dashboard-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-4);
}
@media (max-width: 900px) {
.dashboard-grid {
grid-template-columns: 1fr;
}
}
/* Panel styling */
.panel {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--border);
cursor: pointer;
user-select: none;
}
.panel-header:hover {
filter: brightness(1.05);
}
.panel-header-left {
display: flex;
align-items: center;
gap: var(--space-2);
}
.panel-title {
display: flex;
align-items: center;
gap: var(--space-2);
font-weight: 600;
font-size: var(--text-sm);
color: var(--text-primary);
}
.panel-title svg {
width: 18px;
height: 18px;
}
.panel-toggle {
width: 16px;
height: 16px;
color: var(--text-muted);
transition: transform var(--transition-fast);
}
.panel.collapsed .panel-toggle {
transform: rotate(-90deg);
}
.panel.collapsed .panel-body {
display: none;
}
.panel-actions {
display: flex;
gap: var(--space-2);
}
.panel-count {
font-size: var(--text-xs);
color: var(--text-muted);
background: var(--bg-elevated);
padding: 2px 8px;
border-radius: var(--radius-sm);
}
.panel-body {
padding: var(--space-3);
max-height: 500px;
overflow-y: auto;
}
/* Activity panel */
.activity-panel .panel-header {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.15), rgba(139, 92, 246, 0.1));
}
.activity-panel .panel-title svg {
color: #8b5cf6;
}
.activity-section {
margin-bottom: var(--space-4);
}
.activity-section-title {
font-size: var(--text-xs);
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: var(--space-2);
display: flex;
align-items: center;
gap: var(--space-2);
}
.activity-item {
display: flex;
align-items: flex-start;
gap: var(--space-3);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-md);
margin-bottom: var(--space-1);
transition: background var(--transition-fast);
}
.activity-item:hover {
background: var(--bg-elevated);
}
.activity-icon {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
flex-shrink: 0;
}
.activity-icon.done {
background: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.activity-icon.running {
background: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
.activity-icon svg {
width: 14px;
height: 14px;
}
.activity-content {
flex: 1;
min-width: 0;
}
.activity-text {
font-size: var(--text-sm);
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.activity-meta {
font-size: var(--text-xs);
color: var(--text-muted);
display: flex;
gap: var(--space-2);
}
.activity-agent {
color: var(--accent);
}
.progress-bar {
height: 4px;
background: var(--bg-elevated);
border-radius: 2px;
margin-top: var(--space-1);
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--accent);
border-radius: 2px;
transition: width 0.3s ease;
}
/* Issues panel */
.issues-panel .panel-header {
background: linear-gradient(135deg, rgba(249, 115, 22, 0.15), rgba(234, 88, 12, 0.1));
}
.issues-panel .panel-title svg {
color: #f97316;
}
.issues-filters {
display: flex;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
border-bottom: 1px solid var(--border);
flex-wrap: wrap;
}
.filter-btn {
padding: var(--space-1) var(--space-2);
font-size: var(--text-xs);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: transparent;
color: var(--text-muted);
cursor: pointer;
transition: all var(--transition-fast);
}
.filter-btn:hover, .filter-btn.active {
border-color: var(--accent);
color: var(--accent);
background: var(--accent-subtle);
}
.priority-group {
margin-bottom: var(--space-3);
}
.priority-header {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: var(--text-xs);
font-weight: 600;
color: var(--text-muted);
margin-bottom: var(--space-2);
cursor: pointer;
padding: var(--space-1) 0;
}
.priority-header:hover {
color: var(--text-secondary);
}
.priority-header svg {
width: 14px;
height: 14px;
transition: transform var(--transition-fast);
}
.priority-header.collapsed svg {
transform: rotate(-90deg);
}
.priority-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.priority-dot.urgent-important { background: #ef4444; }
.priority-dot.important { background: #f59e0b; }
.priority-dot.urgent { background: #eab308; }
.priority-dot.backlog { background: #6b7280; }
.priority-content {
display: block;
}
.priority-content.hidden {
display: none;
}
.issue-item {
display: flex;
align-items: flex-start;
gap: var(--space-3);
padding: var(--space-3);
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: var(--radius-md);
margin-bottom: var(--space-2);
cursor: pointer;
transition: all var(--transition-fast);
}
.issue-item:hover {
border-color: var(--border-focus);
transform: translateX(2px);
}
.issue-item.done {
opacity: 0.6;
}
.issue-item.done .issue-title {
text-decoration: line-through;
}
.issue-checkbox {
width: 18px;
height: 18px;
border: 2px solid var(--border);
border-radius: var(--radius-sm);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all var(--transition-fast);
}
.issue-checkbox:hover {
border-color: var(--accent);
}
.issue-checkbox.checked {
background: var(--accent);
border-color: var(--accent);
}
.issue-checkbox svg {
width: 12px;
height: 12px;
color: white;
display: none;
}
.issue-checkbox.checked svg {
display: block;
}
.issue-content {
flex: 1;
min-width: 0;
}
.issue-title {
font-size: var(--text-sm);
font-weight: 500;
color: var(--text-primary);
margin-bottom: var(--space-1);
}
.issue-meta {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
font-size: var(--text-xs);
}
.issue-tag {
padding: 2px 6px;
border-radius: var(--radius-sm);
background: var(--bg-surface);
color: var(--text-muted);
}
.issue-tag.program {
background: rgba(99, 102, 241, 0.15);
color: #818cf8;
}
.issue-owner {
display: flex;
align-items: center;
gap: 4px;
}
.issue-owner.marius { color: #22c55e; }
.issue-owner.clawdbot { color: #8b5cf6; }
.issue-date {
color: var(--text-muted);
}
/* Add issue */
.add-issue-btn {
width: 100%;
padding: var(--space-3);
background: transparent;
border: 1px dashed var(--border);
border-radius: var(--radius-md);
color: var(--text-muted);
font-size: var(--text-sm);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
transition: all var(--transition-fast);
margin-top: var(--space-3);
}
.add-issue-btn:hover {
border-color: var(--accent);
color: var(--accent);
background: var(--accent-subtle);
}
.add-issue-btn svg {
width: 16px;
height: 16px;
}
/* Add form modal */
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal-overlay.active {
display: flex;
}
.modal {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: var(--space-5);
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
}
.modal-title {
font-size: var(--text-lg);
font-weight: 600;
margin-bottom: var(--space-4);
}
.form-group {
margin-bottom: var(--space-4);
}
.form-label {
display: block;
font-size: var(--text-sm);
font-weight: 500;
margin-bottom: var(--space-1);
color: var(--text-secondary);
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-3);
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: var(--space-2);
margin-top: var(--space-5);
}
/* Toast */
.toast {
position: fixed;
bottom: var(--space-5);
left: 50%;
transform: translateX(-50%) translateY(100px);
background: var(--bg-elevated);
border: 1px solid var(--border);
padding: var(--space-3) var(--space-5);
border-radius: var(--radius-md);
font-size: var(--text-sm);
opacity: 0;
transition: all var(--transition-base);
z-index: 1001;
}
.toast.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
/* Empty state */
.empty-state {
text-align: center;
padding: var(--space-6);
color: var(--text-muted);
}
.empty-state svg {
width: 48px;
height: 48px;
margin-bottom: var(--space-3);
opacity: 0.5;
}
</style>
</head>
<body>
<header class="header">
<a href="index.html" class="logo">
<i data-lucide="circle-dot"></i>
Echo
</a>
<nav class="nav">
<a href="index.html" class="nav-item active">
<i data-lucide="layout-dashboard"></i>
<span>Dashboard</span>
</a>
<a href="notes.html" class="nav-item">
<i data-lucide="file-text"></i>
<span>Notes</span>
</a>
<a href="files.html" class="nav-item">
<i data-lucide="folder"></i>
<span>Files</span>
</a>
<a href="grup-sprijin.html" class="nav-item">
<i data-lucide="heart-handshake"></i>
<span>Grup</span>
</a>
<button class="theme-toggle" onclick="toggleTheme()" title="Schimbă tema">
<i data-lucide="sun" id="themeIcon"></i>
</button>
</nav>
</header>
<main class="main">
<div class="page-header">
<h1 class="page-title">Dashboard</h1>
<p class="page-subtitle">Echo Work · Productivitate și proiecte</p>
</div>
<!-- Status Bar -->
<div class="status-bar" id="statusBar">
<div class="status-header" onclick="toggleSection('statusBar')">
<div class="status-title">
<i data-lucide="activity"></i>
<span>Status</span>
</div>
<div class="status-summary" id="statusSummary">Se încarcă...</div>
<i data-lucide="chevron-down" class="status-toggle"></i>
</div>
<div class="status-content">
<div class="status-row">
<div class="status-item">
<span class="status-label">ANAF:</span>
<span class="status-value" id="anafStatus">-</span>
</div>
<div class="status-item">
<span class="status-label">Git:</span>
<span class="status-value" id="gitStatus">-</span>
</div>
<div class="status-item">
<span class="status-label">Raport:</span>
<span class="status-value" id="lastReport">-</span>
<span class="status-time" id="reportTime"></span>
</div>
</div>
<div class="cron-row">
<span class="cron-label">Cron azi:</span>
<span class="cron-list" id="cronList">-</span>
</div>
</div>
</div>
<div class="dashboard-grid">
<!-- Activity Panel -->
<div class="panel activity-panel" id="activityPanel">
<div class="panel-header" onclick="toggleSection('activityPanel')">
<div class="panel-header-left">
<i data-lucide="chevron-down" class="panel-toggle"></i>
<div class="panel-title">
<i data-lucide="bot"></i>
<span>Activity</span>
</div>
<span class="panel-count" id="activityCount">0</span>
</div>
<div class="panel-actions" onclick="event.stopPropagation()">
<button class="btn btn-secondary btn-sm" onclick="refreshActivity()" title="Refresh">
<i data-lucide="refresh-cw"></i>
</button>
</div>
</div>
<div class="panel-body" id="activityBody">
<div class="empty-state">
<i data-lucide="loader"></i>
<p>Se încarcă...</p>
</div>
</div>
</div>
<!-- Issues Panel -->
<div class="panel issues-panel" id="issuesPanel">
<div class="panel-header" onclick="toggleSection('issuesPanel')">
<div class="panel-header-left">
<i data-lucide="chevron-down" class="panel-toggle"></i>
<div class="panel-title">
<i data-lucide="check-square"></i>
<span>Issues</span>
</div>
<span class="panel-count" id="issuesCount">0</span>
</div>
<div class="panel-actions" onclick="event.stopPropagation()">
<button class="btn btn-primary btn-sm" onclick="showAddModal()">
<i data-lucide="plus"></i>
<span>Nou</span>
</button>
</div>
</div>
<div class="issues-filters" id="issuesFilters">
<button class="filter-btn active" data-filter="all">Toate</button>
<button class="filter-btn" data-filter="marius">👤 Marius</button>
<button class="filter-btn" data-filter="clawdbot">🤖 Clawdbot</button>
</div>
<div class="panel-body" id="issuesBody">
<div class="empty-state">
<i data-lucide="loader"></i>
<p>Se încarcă...</p>
</div>
</div>
</div>
</div>
</main>
<!-- Add Issue Modal -->
<div class="modal-overlay" id="addModal">
<div class="modal">
<h2 class="modal-title">Issue nou</h2>
<div class="form-group">
<label class="form-label">Titlu *</label>
<input type="text" class="input" id="issueTitle" placeholder="Ce trebuie făcut?">
</div>
<div class="form-group">
<label class="form-label">Descriere</label>
<textarea class="input" id="issueDesc" rows="3" placeholder="Detalii..."></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Program</label>
<select class="input" id="issueProgram">
<option value="">— Selectează —</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Owner</label>
<select class="input" id="issueOwner">
<option value="marius">👤 Marius</option>
<option value="clawdbot">🤖 Clawdbot</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Prioritate</label>
<select class="input" id="issuePriority">
<option value="urgent-important">🔴 Urgent + Important</option>
<option value="important">🟠 Important</option>
<option value="urgent">🟡 Urgent</option>
<option value="backlog">⚪ Backlog</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Deadline</label>
<input type="date" class="input" id="issueDeadline">
</div>
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="hideAddModal()">Anulează</button>
<button class="btn btn-primary" onclick="addIssue()">Adaugă</button>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
// Theme
function initTheme() {
const saved = localStorage.getItem('theme') || 'dark';
document.documentElement.setAttribute('data-theme', saved);
updateThemeIcon(saved);
}
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme') || 'dark';
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
updateThemeIcon(next);
}
function updateThemeIcon(theme) {
const icon = document.getElementById('themeIcon');
if (icon) {
icon.setAttribute('data-lucide', theme === 'dark' ? 'sun' : 'moon');
lucide.createIcons();
}
}
initTheme();
lucide.createIcons();
// Collapsible sections
function getCollapsedSections() {
try {
return JSON.parse(localStorage.getItem('collapsedSections') || '["statusBar"]');
} catch { return ['statusBar']; }
}
function setCollapsedSections(sections) {
localStorage.setItem('collapsedSections', JSON.stringify(sections));
}
function initCollapsedSections() {
const collapsed = getCollapsedSections();
collapsed.forEach(id => {
const el = document.getElementById(id);
if (el) el.classList.add('collapsed');
});
}
function toggleSection(id) {
const el = document.getElementById(id);
if (!el) return;
el.classList.toggle('collapsed');
const collapsed = getCollapsedSections();
const idx = collapsed.indexOf(id);
if (el.classList.contains('collapsed')) {
if (idx === -1) collapsed.push(id);
} else {
if (idx > -1) collapsed.splice(idx, 1);
}
setCollapsedSections(collapsed);
}
initCollapsedSections();
// Data
let issuesData = null;
let activityData = [];
let currentFilter = 'all';
let collapsedPriorities = new Set(['backlog']);
// Priority labels
const priorityLabels = {
'urgent-important': '🔴 Urgent + Important',
'important': '🟠 Important',
'urgent': '🟡 Urgent',
'backlog': '⚪ Backlog'
};
const priorityOrder = ['urgent-important', 'important', 'urgent', 'backlog'];
// Status
async function loadStatus() {
try {
const response = await fetch('status.json?' + Date.now());
if (response.ok) {
const status = await response.json();
updateStatus(status);
}
} catch (e) {
console.log('No status.json');
}
updateCronList();
}
function updateStatus(status) {
const anafEl = document.getElementById('anafStatus');
if (status.anaf) {
anafEl.textContent = status.anaf.status;
anafEl.className = 'status-value ' + (status.anaf.ok ? 'ok' : 'warning');
}
const gitEl = document.getElementById('gitStatus');
if (status.git) {
gitEl.textContent = status.git.status;
gitEl.className = 'status-value ' + (status.git.clean ? 'ok' : 'warning');
}
const reportEl = document.getElementById('lastReport');
const timeEl = document.getElementById('reportTime');
if (status.lastReport) {
reportEl.textContent = status.lastReport.summary || 'OK';
timeEl.textContent = status.lastReport.time ? '(' + status.lastReport.time + ')' : '';
}
const summaryEl = document.getElementById('statusSummary');
let summary = [];
if (status.anaf) summary.push('ANAF: ' + status.anaf.status);
if (status.git) summary.push('Git: ' + status.git.status);
summaryEl.textContent = summary.join(' · ') || 'OK';
}
function updateCronList() {
const now = new Date();
const hour = now.getHours();
const jobs = [
{ time: '07:30', name: 'coaching', done: hour >= 8 },
{ time: '08:30', name: 'raport', done: hour >= 9 },
{ time: '20:00', name: 'raport', done: hour >= 20 },
{ time: '21:00', name: 'coaching', done: hour >= 21 }
];
const listEl = document.getElementById('cronList');
listEl.innerHTML = jobs.map(job =>
`<span class="${job.done ? 'cron-done' : ''}">${job.time} ${job.name}</span>`
).join(' · ');
}
// Load data
async function loadIssues() {
try {
const response = await fetch('issues.json?' + Date.now());
issuesData = await response.json();
populateProgramSelect();
renderIssues();
updateIssuesCount();
} catch (error) {
console.error('Error loading issues:', error);
document.getElementById('issuesBody').innerHTML = `
<div class="empty-state">
<i data-lucide="alert-circle"></i>
<p>Eroare la încărcare</p>
</div>
`;
lucide.createIcons();
}
}
function updateIssuesCount() {
if (!issuesData) return;
const todoCount = issuesData.issues.filter(i => i.status !== 'done').length;
document.getElementById('issuesCount').textContent = todoCount;
}
async function loadActivity() {
// For now, show static data. TODO: fetch from API
activityData = [
{ type: 'done', text: 'Răspuns întrebare D101', agent: 'Echo Work', time: '15:10' },
{ type: 'done', text: 'Propunere dashboard v2', agent: 'Echo Work', time: '15:23' },
{ type: 'done', text: 'Fix notes.html loading', agent: 'Echo Work', time: '17:39' }
];
renderActivity();
document.getElementById('activityCount').textContent = activityData.length;
}
function refreshActivity() {
loadActivity();
showToast('Activitate reîmprospătată');
}
function renderActivity() {
const body = document.getElementById('activityBody');
if (activityData.length === 0) {
body.innerHTML = `
<div class="empty-state">
<i data-lucide="inbox"></i>
<p>Nicio activitate recentă</p>
</div>
`;
lucide.createIcons();
return;
}
const today = new Date().toLocaleDateString('ro-RO', { day: 'numeric', month: 'short' });
body.innerHTML = `
<div class="activity-section">
<div class="activity-section-title">
<i data-lucide="calendar"></i>
Azi (${today})
</div>
${activityData.map(item => `
<div class="activity-item">
<div class="activity-icon ${item.type}">
<i data-lucide="${item.type === 'running' ? 'loader' : 'check'}"></i>
</div>
<div class="activity-content">
<div class="activity-text">${item.text}</div>
<div class="activity-meta">
<span class="activity-agent">${item.agent}</span>
<span>${item.time}</span>
</div>
${item.progress ? `
<div class="progress-bar">
<div class="progress-fill" style="width: ${item.progress}%"></div>
</div>
` : ''}
</div>
</div>
`).join('')}
</div>
`;
lucide.createIcons();
}
function renderIssues() {
const body = document.getElementById('issuesBody');
if (!issuesData || issuesData.issues.length === 0) {
body.innerHTML = `
<div class="empty-state">
<i data-lucide="check-circle"></i>
<p>Niciun issue. Adaugă primul!</p>
</div>
<button class="add-issue-btn" onclick="showAddModal()">
<i data-lucide="plus"></i>
Adaugă issue
</button>
`;
lucide.createIcons();
return;
}
// Filter issues
let filtered = issuesData.issues;
if (currentFilter !== 'all') {
filtered = filtered.filter(i => i.owner === currentFilter);
}
// Group by priority
const grouped = {};
priorityOrder.forEach(p => grouped[p] = []);
filtered.forEach(issue => {
const p = issue.priority || 'backlog';
if (grouped[p]) grouped[p].push(issue);
else grouped['backlog'].push(issue);
});
// Sort each group: todo first, then by date
Object.keys(grouped).forEach(p => {
grouped[p].sort((a, b) => {
if (a.status === 'done' && b.status !== 'done') return 1;
if (a.status !== 'done' && b.status === 'done') return -1;
return new Date(b.created) - new Date(a.created);
});
});
let html = '';
priorityOrder.forEach(priority => {
const issues = grouped[priority];
if (issues.length === 0) return;
const isCollapsed = collapsedPriorities.has(priority);
const todoCount = issues.filter(i => i.status !== 'done').length;
html += `
<div class="priority-group">
<div class="priority-header ${isCollapsed ? 'collapsed' : ''}" onclick="togglePriority('${priority}')">
<i data-lucide="chevron-down"></i>
<span class="priority-dot ${priority}"></span>
<span>${priorityLabels[priority]}</span>
<span style="margin-left: auto; opacity: 0.7">${todoCount}/${issues.length}</span>
</div>
<div class="priority-content ${isCollapsed ? 'hidden' : ''}">
${issues.map(issue => renderIssueItem(issue)).join('')}
</div>
</div>
`;
});
html += `
<button class="add-issue-btn" onclick="showAddModal()">
<i data-lucide="plus"></i>
Adaugă issue
</button>
`;
body.innerHTML = html;
lucide.createIcons();
}
function renderIssueItem(issue) {
const isDone = issue.status === 'done';
const ownerIcon = issue.owner === 'clawdbot' ? '🤖' : '👤';
const dateStr = new Date(issue.created).toLocaleDateString('ro-RO', { day: 'numeric', month: 'short' });
return `
<div class="issue-item ${isDone ? 'done' : ''}" data-id="${issue.id}">
<div class="issue-checkbox ${isDone ? 'checked' : ''}" onclick="toggleIssue('${issue.id}')">
<i data-lucide="check"></i>
</div>
<div class="issue-content" onclick="editIssue('${issue.id}')">
<div class="issue-title">${issue.title}</div>
<div class="issue-meta">
${issue.program ? `<span class="issue-tag program">${issue.program}</span>` : ''}
<span class="issue-owner ${issue.owner}">${ownerIcon} ${issue.owner === 'clawdbot' ? 'Clawdbot' : 'Marius'}</span>
<span class="issue-date">${dateStr}</span>
</div>
</div>
</div>
`;
}
function togglePriority(priority) {
if (collapsedPriorities.has(priority)) {
collapsedPriorities.delete(priority);
} else {
collapsedPriorities.add(priority);
}
renderIssues();
}
async function toggleIssue(id) {
const issue = issuesData.issues.find(i => i.id === id);
if (!issue) return;
issue.status = issue.status === 'done' ? 'todo' : 'done';
if (issue.status === 'done') {
issue.completed = new Date().toISOString();
} else {
delete issue.completed;
}
renderIssues();
updateIssuesCount();
await saveIssues();
showToast(issue.status === 'done' ? 'Issue finalizat! ✓' : 'Issue redeschis');
}
function editIssue(id) {
const issue = issuesData.issues.find(i => i.id === id);
if (issue) {
alert(`Edit: ${issue.title}\n\n${issue.description || 'Fără descriere'}`);
}
}
// Filters
document.getElementById('issuesFilters').addEventListener('click', (e) => {
if (e.target.classList.contains('filter-btn')) {
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
currentFilter = e.target.dataset.filter;
renderIssues();
}
});
// Modal
function showAddModal() {
document.getElementById('addModal').classList.add('active');
document.getElementById('issueTitle').focus();
}
function hideAddModal() {
document.getElementById('addModal').classList.remove('active');
document.getElementById('issueTitle').value = '';
document.getElementById('issueDesc').value = '';
document.getElementById('issueProgram').value = '';
document.getElementById('issueOwner').value = 'marius';
document.getElementById('issuePriority').value = 'urgent-important';
document.getElementById('issueDeadline').value = '';
}
function populateProgramSelect() {
const select = document.getElementById('issueProgram');
select.innerHTML = '<option value="">— Selectează —</option>';
if (issuesData && issuesData.programs) {
issuesData.programs.forEach(p => {
select.innerHTML += `<option value="${p}">${p}</option>`;
});
}
}
async function addIssue() {
const title = document.getElementById('issueTitle').value.trim();
if (!title) {
showToast('Titlul este obligatoriu', 'error');
return;
}
const newIssue = {
id: 'ROA-' + String(issuesData.issues.length + 1).padStart(3, '0'),
title: title,
description: document.getElementById('issueDesc').value.trim(),
program: document.getElementById('issueProgram').value,
owner: document.getElementById('issueOwner').value,
priority: document.getElementById('issuePriority').value,
status: 'todo',
created: new Date().toISOString(),
deadline: document.getElementById('issueDeadline').value || null
};
issuesData.issues.unshift(newIssue);
hideAddModal();
renderIssues();
updateIssuesCount();
await saveIssues();
showToast('Issue adăugat!');
}
async function saveIssues() {
issuesData.lastUpdated = new Date().toISOString();
try {
const response = await fetch('/echo/api/files', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
path: 'kanban/issues.json',
content: JSON.stringify(issuesData, null, 2)
})
});
if (!response.ok) throw new Error('Save failed');
} catch (error) {
showToast('Eroare la salvare', 'error');
}
}
function showToast(message, type = 'success') {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = 'toast show';
setTimeout(() => toast.classList.remove('show'), 3000);
}
// Close modal on outside click
document.getElementById('addModal').addEventListener('click', (e) => {
if (e.target.id === 'addModal') hideAddModal();
});
// Init
loadStatus();
loadIssues();
loadActivity();
</script>
</body>
</html>