Spaces:
Paused
Paused
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Priority 3: Phase 1.C Fast-Track Kanban</title> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <style> | |
| :root { | |
| --bg-color: #0f172a; | |
| --card-bg: #1e293b; | |
| --border-color: #334155; | |
| --primary: #3b82f6; | |
| --success: #10b981; | |
| --warning: #f59e0b; | |
| --danger: #ef4444; | |
| --text: #f1f5f9; | |
| --text-muted: #cbd5e1; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background-color: var(--bg-color); | |
| color: var(--text); | |
| margin: 0; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 1600px; | |
| margin: 0 auto; | |
| } | |
| .header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 30px; | |
| border-bottom: 2px solid var(--primary); | |
| padding-bottom: 20px; | |
| } | |
| .header h1 { | |
| margin: 0; | |
| color: var(--primary); | |
| font-size: 32px; | |
| } | |
| .header-right { | |
| display: flex; | |
| gap: 20px; | |
| align-items: center; | |
| } | |
| .card { | |
| background-color: var(--card-bg); | |
| border-radius: 8px; | |
| box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); | |
| padding: 20px; | |
| margin-bottom: 20px; | |
| border: 1px solid var(--border-color); | |
| } | |
| .progress-bar { | |
| height: 12px; | |
| background-color: var(--border-color); | |
| border-radius: 6px; | |
| overflow: hidden; | |
| margin: 15px 0; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--primary), var(--success)); | |
| border-radius: 6px; | |
| } | |
| .grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); | |
| gap: 20px; | |
| } | |
| .kanban { | |
| display: grid; | |
| grid-template-columns: repeat(4, 1fr); | |
| gap: 15px; | |
| margin-top: 20px; | |
| } | |
| @media (max-width: 1400px) { | |
| .kanban { | |
| grid-template-columns: repeat(2, 1fr); | |
| } | |
| } | |
| @media (max-width: 768px) { | |
| .kanban { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| .kanban-column { | |
| background-color: rgba(15, 23, 42, 0.8); | |
| border: 2px solid var(--border-color); | |
| border-radius: 8px; | |
| padding: 15px; | |
| min-height: 500px; | |
| } | |
| .kanban-column.todo { | |
| border-top: 4px solid var(--border-color); | |
| } | |
| .kanban-column.in-progress { | |
| border-top: 4px solid var(--warning); | |
| } | |
| .kanban-column.blocked { | |
| border-top: 4px solid var(--danger); | |
| } | |
| .kanban-column.completed { | |
| border-top: 4px solid var(--success); | |
| } | |
| .kanban-header { | |
| font-weight: bold; | |
| margin-bottom: 15px; | |
| text-align: center; | |
| color: var(--primary); | |
| font-size: 14px; | |
| text-transform: uppercase; | |
| border-bottom: 2px solid var(--primary); | |
| padding-bottom: 10px; | |
| } | |
| .kanban-column.in-progress .kanban-header { | |
| color: var(--warning); | |
| border-bottom-color: var(--warning); | |
| } | |
| .kanban-column.blocked .kanban-header { | |
| color: var(--danger); | |
| border-bottom-color: var(--danger); | |
| } | |
| .kanban-column.completed .kanban-header { | |
| color: var(--success); | |
| border-bottom-color: var(--success); | |
| } | |
| .kanban-item { | |
| background-color: var(--card-bg); | |
| border-left: 4px solid var(--primary); | |
| border-radius: 4px; | |
| padding: 12px; | |
| margin-bottom: 10px; | |
| font-size: 13px; | |
| word-wrap: break-word; | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); | |
| } | |
| .kanban-item.in-progress { | |
| border-left-color: var(--warning); | |
| } | |
| .kanban-item.blocked { | |
| border-left-color: var(--danger); | |
| opacity: 0.8; | |
| } | |
| .kanban-item.completed { | |
| border-left-color: var(--success); | |
| opacity: 0.7; | |
| } | |
| .kanban-item-title { | |
| font-weight: bold; | |
| margin-bottom: 5px; | |
| } | |
| .kanban-item-points { | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| margin-top: 5px; | |
| padding-top: 5px; | |
| border-top: 1px solid var(--border-color); | |
| } | |
| .status-indicator { | |
| display: inline-block; | |
| width: 12px; | |
| height: 12px; | |
| border-radius: 50%; | |
| margin-right: 8px; | |
| } | |
| .status-active { | |
| background-color: var(--success); | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.7; } | |
| } | |
| .status-badge { | |
| display: inline-block; | |
| padding: 8px 16px; | |
| border-radius: 20px; | |
| font-size: 12px; | |
| font-weight: bold; | |
| text-transform: uppercase; | |
| background-color: rgba(16, 185, 129, 0.2); | |
| color: var(--success); | |
| border: 1px solid var(--success); | |
| } | |
| .sprint-info { | |
| display: flex; | |
| gap: 20px; | |
| margin-bottom: 20px; | |
| } | |
| .sprint-info-item { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 5px; | |
| } | |
| .sprint-info-label { | |
| font-size: 12px; | |
| color: var(--text-muted); | |
| text-transform: uppercase; | |
| } | |
| .sprint-info-value { | |
| font-size: 14px; | |
| font-weight: bold; | |
| color: var(--text); | |
| } | |
| .column-count { | |
| display: inline-block; | |
| background-color: rgba(59, 130, 246, 0.2); | |
| color: var(--primary); | |
| padding: 2px 8px; | |
| border-radius: 12px; | |
| font-size: 11px; | |
| margin-left: 8px; | |
| } | |
| .column-count.warning { | |
| background-color: rgba(245, 158, 11, 0.2); | |
| color: var(--warning); | |
| } | |
| .column-count.danger { | |
| background-color: rgba(239, 68, 68, 0.2); | |
| color: var(--danger); | |
| } | |
| .column-count.success { | |
| background-color: rgba(16, 185, 129, 0.2); | |
| color: var(--success); | |
| } | |
| /* Accessibility improvements */ | |
| *:focus-visible { | |
| outline: 3px solid var(--primary); | |
| outline-offset: 2px; | |
| } | |
| .sr-only { | |
| position: absolute; | |
| width: 1px; | |
| height: 1px; | |
| padding: 0; | |
| margin: -1px; | |
| overflow: hidden; | |
| clip: rect(0, 0, 0, 0); | |
| white-space: nowrap; | |
| border-width: 0; | |
| } | |
| /* Progress bar animation */ | |
| .progress-fill { | |
| transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| /* Loading states */ | |
| .loading { | |
| position: relative; | |
| opacity: 0.6; | |
| pointer-events: none; | |
| } | |
| .loading::after { | |
| content: 'Loading...'; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| color: var(--text); | |
| font-weight: bold; | |
| } | |
| /* Tooltip styles */ | |
| .tooltip { | |
| position: relative; | |
| cursor: help; | |
| } | |
| .tooltip::after { | |
| content: attr(data-tooltip); | |
| position: absolute; | |
| bottom: 100%; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| padding: 8px 12px; | |
| background: var(--card-bg); | |
| border: 1px solid var(--border-color); | |
| border-radius: 4px; | |
| font-size: 12px; | |
| white-space: nowrap; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.2s; | |
| z-index: 1000; | |
| } | |
| .tooltip:hover::after { | |
| opacity: 1; | |
| } | |
| /* Responsive improvements */ | |
| @media (max-width: 768px) { | |
| .header { | |
| flex-direction: column; | |
| gap: 15px; | |
| align-items: flex-start; | |
| } | |
| .header-right { | |
| width: 100%; | |
| flex-direction: column; | |
| } | |
| .sprint-info { | |
| width: 100%; | |
| flex-direction: column; | |
| } | |
| } | |
| /* Print styles */ | |
| @media print { | |
| body { | |
| background-color: white; | |
| color: black; | |
| } | |
| .status-indicator { | |
| display: none; | |
| } | |
| .card { | |
| break-inside: avoid; | |
| box-shadow: none; | |
| border: 1px solid #ddd; | |
| } | |
| } | |
| /* Drag and drop styles */ | |
| .kanban-item { | |
| cursor: move; | |
| -webkit-user-select: none; | |
| user-select: none; | |
| } | |
| .kanban-item.dragging { | |
| opacity: 0.5; | |
| } | |
| .kanban-column.drag-over { | |
| background-color: rgba(59, 130, 246, 0.1); | |
| } | |
| /* Error state */ | |
| .error-message { | |
| padding: 16px; | |
| background-color: rgba(239, 68, 68, 0.1); | |
| border: 1px solid var(--danger); | |
| border-radius: 4px; | |
| color: var(--danger); | |
| margin: 16px 0; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>🎯 Priority 3: Phase 1.C Fast-Track Kanban</h1> | |
| <div class="header-right"> | |
| <div class="sprint-info"> | |
| <div class="sprint-info-item"> | |
| <span class="sprint-info-label">Phase</span> | |
| <span class="sprint-info-value">Priority 3</span> | |
| </div> | |
| <div class="sprint-info-item"> | |
| <span class="sprint-info-label">Timeline</span> | |
| <span class="sprint-info-value">Nov 18-22, 2024</span> | |
| </div> | |
| <div class="sprint-info-item"> | |
| <span class="sprint-info-label">Story Points</span> | |
| <span class="sprint-info-value">21.0 pts</span> | |
| </div> | |
| </div> | |
| <span class="status-indicator status-active"></span> | |
| <div id="project-status" class="status-badge">Active</div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h2>Priority 3 Progress (21.0 Story Points)</h2> | |
| <div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="21" aria-label="Sprint progress"> | |
| <div class="progress-fill" id="overall-progress" style="width: 0%"></div> | |
| </div> | |
| <div id="progress-text" style="display: flex; justify-content: space-between; align-items: center;"> | |
| <span aria-live="polite" aria-atomic="true">0 / 21.0 points completed</span> | |
| <span style="font-weight: bold; color: var(--primary);" aria-label="Completion percentage">0%</span> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h2>Sprint Kanban Board</h2> | |
| <div class="kanban" id="kanban-board" role="region" aria-label="Kanban board with task columns"> | |
| <!-- Kanban columns will be rendered by JavaScript --> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h2>Sprint Timeline</h2> | |
| <canvas id="timeline-chart" style="max-height: 200px;" role="img" aria-label="Sprint timeline bar chart showing story points per block"></canvas> | |
| <div id="timeline-error" class="error-message" style="display: none;" role="alert"></div> | |
| </div> | |
| <div class="card"> | |
| <h2>Task Distribution</h2> | |
| <canvas id="distribution-chart" style="max-height: 300px;" role="img" aria-label="Task distribution doughnut chart showing story points by block"></canvas> | |
| <div id="distribution-error" class="error-message" style="display: none;" role="alert"></div> | |
| </div> | |
| <div id="upcoming-sprints" style="margin-top: 40px; border-top: 3px solid var(--primary); padding-top: 30px;"><h2>📋 Upcoming Sprints</h2></div> | |
| </div> | |
| <script> | |
| // Priority 3 Sprint Data (21.0 story points across 6 blocks) | |
| const sprintData = { | |
| status: 'ACTIVE', | |
| timeline: 'Nov 18-22, 2024', | |
| totalPoints: 21.0, | |
| completedPoints: 0, | |
| tasks: { | |
| 'Block 1: Dashboard Shell': { | |
| points: 1.8, | |
| status: 'in-progress', | |
| subtasks: [ | |
| 'Shell UI refinement - 0.6 pts', | |
| 'Layout system fixes - 0.4 pts', | |
| 'Widget placement validation - 0.8 pts' | |
| ] | |
| }, | |
| 'Block 2: Widget Registry 2.0': { | |
| points: 4.2, | |
| status: 'todo', | |
| subtasks: [ | |
| 'Type-safe discovery - 1.4 pts', | |
| 'Versioning system - 1.2 pts', | |
| 'Capability-based filtering - 1.6 pts' | |
| ] | |
| }, | |
| 'Block 3: Audit Log Hash-Chain': { | |
| points: 4.0, | |
| status: 'todo', | |
| subtasks: [ | |
| 'SHA-256 hash chain - 1.8 pts', | |
| 'GDPR compliance - 1.4 pts', | |
| 'Audit trail UI - 0.8 pts' | |
| ] | |
| }, | |
| 'Block 4: Foundation Systems': { | |
| points: 5.0, | |
| status: 'todo', | |
| subtasks: [ | |
| 'Database migration plan - 1.6 pts', | |
| 'Auth architecture design - 1.8 pts', | |
| 'Observability framework - 1.6 pts' | |
| ] | |
| }, | |
| 'Block 5: E2E Testing': { | |
| points: 3.2, | |
| status: 'todo', | |
| subtasks: [ | |
| 'Test acceleration - 1.6 pts', | |
| 'Coverage improvement - 1.0 pts', | |
| 'Performance testing - 0.6 pts' | |
| ] | |
| }, | |
| 'Block 6: Security & Compliance': { | |
| points: 2.8, | |
| status: 'todo', | |
| subtasks: [ | |
| 'Security review - 1.2 pts', | |
| 'Compliance audit - 1.0 pts', | |
| 'Remediation - 0.6 pts' | |
| ] | |
| } | |
| } | |
| }; | |
| // Calculate status counts | |
| function getStatusCounts() { | |
| let todo = 0; | |
| let inProgress = 0; | |
| let blocked = 0; | |
| let completed = 0; | |
| let totalCompleted = 0; | |
| for (const [taskName, taskData] of Object.entries(sprintData.tasks)) { | |
| if (taskData.status === 'completed') { | |
| completed++; | |
| totalCompleted += taskData.points; | |
| } else if (taskData.status === 'in-progress') { | |
| inProgress++; | |
| } else if (taskData.status === 'blocked') { | |
| blocked++; | |
| } else { | |
| todo++; | |
| } | |
| } | |
| sprintData.completedPoints = totalCompleted; | |
| return { todo, inProgress, blocked, completed }; | |
| } | |
| // Render Kanban board organized by status | |
| function renderKanban() { | |
| const kanbanBoard = document.getElementById('kanban-board'); | |
| kanbanBoard.innerHTML = ''; | |
| const statuses = ['todo', 'in-progress', 'blocked', 'completed']; | |
| const statusLabels = { | |
| 'todo': 'TO DO', | |
| 'in-progress': 'IN PROGRESS', | |
| 'blocked': 'BLOCKED', | |
| 'completed': 'COMPLETED' | |
| }; | |
| const statusColors = { | |
| 'todo': '#334155', | |
| 'in-progress': '#f59e0b', | |
| 'blocked': '#ef4444', | |
| 'completed': '#10b981' | |
| }; | |
| statuses.forEach(status => { | |
| const column = document.createElement('div'); | |
| column.className = `kanban-column ${status}`; | |
| const header = document.createElement('div'); | |
| header.className = 'kanban-header'; | |
| const tasksInStatus = Object.entries(sprintData.tasks).filter(([_, task]) => task.status === status); | |
| const pointsInStatus = tasksInStatus.reduce((sum, [_, task]) => sum + task.points, 0); | |
| const countBadge = document.createElement('span'); | |
| countBadge.className = `column-count ${status === 'in-progress' ? 'warning' : status === 'blocked' ? 'danger' : status === 'completed' ? 'success' : ''}`; | |
| countBadge.textContent = `${tasksInStatus.length} (${pointsInStatus} pts)`; | |
| header.innerHTML = `${statusLabels[status]}`; | |
| header.appendChild(countBadge); | |
| column.appendChild(header); | |
| const itemsContainer = document.createElement('div'); | |
| tasksInStatus.forEach(([taskName, taskData]) => { | |
| const item = document.createElement('div'); | |
| item.className = `kanban-item ${status}`; | |
| const title = document.createElement('div'); | |
| title.className = 'kanban-item-title'; | |
| title.textContent = taskName; | |
| item.appendChild(title); | |
| taskData.subtasks.forEach(subtask => { | |
| const subtaskText = document.createElement('div'); | |
| subtaskText.style.fontSize = '11px'; | |
| subtaskText.style.color = 'var(--text-muted)'; | |
| subtaskText.style.marginTop = '4px'; | |
| subtaskText.textContent = '• ' + subtask; | |
| item.appendChild(subtaskText); | |
| }); | |
| const points = document.createElement('div'); | |
| points.className = 'kanban-item-points'; | |
| points.textContent = `${taskData.points} story points`; | |
| item.appendChild(points); | |
| itemsContainer.appendChild(item); | |
| }); | |
| if (tasksInStatus.length === 0) { | |
| const emptyMsg = document.createElement('div'); | |
| emptyMsg.className = 'kanban-item'; | |
| emptyMsg.textContent = 'No tasks'; | |
| emptyMsg.style.opacity = '0.5'; | |
| itemsContainer.appendChild(emptyMsg); | |
| } | |
| column.appendChild(itemsContainer); | |
| kanbanBoard.appendChild(column); | |
| }); | |
| } | |
| // Update progress bar with accessibility | |
| function updateProgressBar() { | |
| const percentage = (sprintData.completedPoints / sprintData.totalPoints) * 100; | |
| const progressBar = document.querySelector('.progress-bar'); | |
| const progressFill = document.getElementById('overall-progress'); | |
| progressBar.setAttribute('aria-valuenow', sprintData.completedPoints); | |
| progressFill.style.width = percentage + '%'; | |
| document.getElementById('progress-text').innerHTML = ` | |
| <span aria-live="polite" aria-atomic="true">${sprintData.completedPoints} / ${sprintData.totalPoints} points completed</span> | |
| <span style="font-weight: bold; color: var(--primary);" aria-label="Completion percentage">${Math.round(percentage)}%</span> | |
| `; | |
| } | |
| // Chart: Timeline with error handling | |
| function renderTimelineChart() { | |
| try { | |
| const canvas = document.getElementById('timeline-chart'); | |
| canvas.classList.add('loading'); | |
| const timelineCtx = canvas.getContext('2d'); | |
| const labels = Object.keys(sprintData.tasks); | |
| const points = Object.values(sprintData.tasks).map(task => task.points); | |
| new Chart(timelineCtx, { | |
| type: 'bar', | |
| data: { | |
| labels: labels, | |
| datasets: [{ | |
| label: 'Story Points', | |
| data: points, | |
| backgroundColor: 'rgba(59, 130, 246, 0.6)', | |
| borderColor: 'rgb(59, 130, 246)', | |
| borderWidth: 2, | |
| borderRadius: 4 | |
| }] | |
| }, | |
| options: { | |
| indexAxis: 'y', | |
| responsive: true, | |
| maintainAspectRatio: true, | |
| plugins: { | |
| legend: { display: false } | |
| }, | |
| scales: { | |
| x: { | |
| ticks: { color: 'var(--text-muted)' }, | |
| grid: { color: 'var(--border-color)' } | |
| }, | |
| y: { | |
| ticks: { color: 'var(--text)' }, | |
| grid: { display: false } | |
| } | |
| } | |
| } | |
| }); | |
| canvas.classList.remove('loading'); | |
| } catch (error) { | |
| console.error('Timeline chart error:', error); | |
| document.getElementById('timeline-error').textContent = 'Failed to load timeline chart: ' + error.message; | |
| document.getElementById('timeline-error').style.display = 'block'; | |
| document.getElementById('timeline-chart').style.display = 'none'; | |
| } | |
| } | |
| // Chart: Distribution with error handling | |
| function renderDistributionChart() { | |
| try { | |
| const canvas = document.getElementById('distribution-chart'); | |
| canvas.classList.add('loading'); | |
| const distributionCtx = canvas.getContext('2d'); | |
| const labels = Object.keys(sprintData.tasks); | |
| const points = Object.values(sprintData.tasks).map(task => task.points); | |
| const colors = [ | |
| 'rgba(59, 130, 246, 0.8)', | |
| 'rgba(16, 185, 129, 0.8)', | |
| 'rgba(245, 158, 11, 0.8)', | |
| 'rgba(139, 92, 246, 0.8)', | |
| 'rgba(236, 72, 153, 0.8)', | |
| 'rgba(14, 165, 233, 0.8)' | |
| ]; | |
| new Chart(distributionCtx, { | |
| type: 'doughnut', | |
| data: { | |
| labels: labels, | |
| datasets: [{ | |
| data: points, | |
| backgroundColor: colors, | |
| borderColor: 'var(--card-bg)', | |
| borderWidth: 2 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: true, | |
| plugins: { | |
| legend: { | |
| position: 'bottom', | |
| labels: { color: 'var(--text)' } | |
| } | |
| } | |
| } | |
| }); | |
| canvas.classList.remove('loading'); | |
| } catch (error) { | |
| console.error('Distribution chart error:', error); | |
| document.getElementById('distribution-error').textContent = 'Failed to load distribution chart: ' + error.message; | |
| document.getElementById('distribution-error').style.display = 'block'; | |
| document.getElementById('distribution-chart').style.display = 'none'; | |
| } | |
| } | |
| // Local Storage: Save state | |
| function saveToLocalStorage() { | |
| try { | |
| localStorage.setItem('widgetTDC_sprintData', JSON.stringify(sprintData)); | |
| localStorage.setItem('widgetTDC_lastUpdate', new Date().toISOString()); | |
| } catch (error) { | |
| console.warn('Failed to save to localStorage:', error); | |
| } | |
| } | |
| // Local Storage: Load state | |
| function loadFromLocalStorage() { | |
| try { | |
| const saved = localStorage.getItem('widgetTDC_sprintData'); | |
| if (saved) { | |
| const parsed = JSON.parse(saved); | |
| Object.assign(sprintData, parsed); | |
| return true; | |
| } | |
| } catch (error) { | |
| console.warn('Failed to load from localStorage:', error); | |
| } | |
| return false; | |
| } | |
| // Export: CSV | |
| function exportToCSV() { | |
| const rows = [['Block', 'Points', 'Status', 'Subtasks']]; | |
| for (const [taskName, taskData] of Object.entries(sprintData.tasks)) { | |
| rows.push([ | |
| taskName, | |
| taskData.points, | |
| taskData.status, | |
| taskData.subtasks.join('; ') | |
| ]); | |
| } | |
| const csv = rows.map(row => row.map(cell => `"${cell}"`).join(',')).join('\n'); | |
| const blob = new Blob([csv], { type: 'text/csv' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `sprint-data-${new Date().toISOString().split('T')[0]}.csv`; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| } | |
| // Export: JSON | |
| function exportToJSON() { | |
| const json = JSON.stringify(sprintData, null, 2); | |
| const blob = new Blob([json], { type: 'application/json' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `sprint-data-${new Date().toISOString().split('T')[0]}.json`; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| } | |
| // Drag and Drop: Setup | |
| function setupDragAndDrop() { | |
| const kanbanItems = document.querySelectorAll('.kanban-item'); | |
| const columns = document.querySelectorAll('.kanban-column'); | |
| kanbanItems.forEach(item => { | |
| item.setAttribute('draggable', 'true'); | |
| item.addEventListener('dragstart', (e) => { | |
| item.classList.add('dragging'); | |
| e.dataTransfer.effectAllowed = 'move'; | |
| e.dataTransfer.setData('text/plain', item.querySelector('.kanban-item-title').textContent); | |
| }); | |
| item.addEventListener('dragend', () => { | |
| item.classList.remove('dragging'); | |
| }); | |
| }); | |
| columns.forEach(column => { | |
| column.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| column.classList.add('drag-over'); | |
| }); | |
| column.addEventListener('dragleave', () => { | |
| column.classList.remove('drag-over'); | |
| }); | |
| column.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| column.classList.remove('drag-over'); | |
| const taskName = e.dataTransfer.getData('text/plain'); | |
| const newStatus = column.classList.contains('todo') ? 'todo' : | |
| column.classList.contains('in-progress') ? 'in-progress' : | |
| column.classList.contains('blocked') ? 'blocked' : 'completed'; | |
| if (sprintData.tasks[taskName]) { | |
| sprintData.tasks[taskName].status = newStatus; | |
| saveToLocalStorage(); | |
| initDashboard(); | |
| } | |
| }); | |
| }); | |
| } | |
| // Keyboard navigation | |
| function setupKeyboardNavigation() { | |
| document.addEventListener('keydown', (e) => { | |
| // Export shortcuts | |
| if (e.ctrlKey && e.key === 'e') { | |
| e.preventDefault(); | |
| exportToJSON(); | |
| } | |
| if (e.ctrlKey && e.shiftKey && e.key === 'E') { | |
| e.preventDefault(); | |
| exportToCSV(); | |
| } | |
| }); | |
| } | |
| // Initialize dashboard | |
| function initDashboard() { | |
| loadFromLocalStorage(); | |
| getStatusCounts(); | |
| renderKanban(); | |
| updateProgressBar(); | |
| renderTimelineChart(); | |
| renderDistributionChart(); | |
| // Setup interactions after render | |
| setTimeout(() => { | |
| setupDragAndDrop(); | |
| }, 100); | |
| } | |
| // Run on load | |
| window.addEventListener('load', () => { | |
| initDashboard(); | |
| setupKeyboardNavigation(); | |
| }); | |
| // Expose API for updating tasks from external systems | |
| window.updateTaskStatus = function(taskName, newStatus) { | |
| if (sprintData.tasks[taskName]) { | |
| sprintData.tasks[taskName].status = newStatus; | |
| initDashboard(); | |
| return true; | |
| } | |
| return false; | |
| }; | |
| window.getSprintData = function() { | |
| return sprintData; | |
| }; | |
| </script> | |
| </body> | |
| </html> | |