|
|
| |
| let currentDateRange = 'this-week'; |
| let selectedCells = new Set(); |
| let isCommandPanelOpen = false; |
| let teamMembers = [ |
| { |
| id: 'alice', |
| name: 'Alice Smith', |
| avatar: 'http://static.photos/people/200x200/1', |
| capacity: 8, |
| role: 'Frontend Developer', |
| status: 'available' |
| }, |
| { |
| id: 'bob', |
| name: 'Bob Wilson', |
| avatar: 'http://static.photos/people/200x200/2', |
| capacity: 6, |
| role: 'Backend Developer', |
| status: 'available' |
| }, |
| { |
| id: 'charlie', |
| name: 'Charlie Brown', |
| avatar: 'http://static.photos/people/200x200/3', |
| capacity: 8, |
| role: 'QA Engineer', |
| status: 'available' |
| }, |
| { |
| id: 'diana', |
| name: 'Diana Prince', |
| avatar: 'http://static.photos/people/200x200/4', |
| capacity: 7, |
| role: 'Product Manager', |
| status: 'available' |
| } |
| ]; |
|
|
| |
| let columnOrder = teamMembers.map(member => member.id); |
| let columnWidths = {}; |
| teamMembers.forEach(member => { |
| columnWidths[member.id] = 200; |
| }); |
| let tasks = [ |
| { id: 1, title: 'API Integration', assignee: 'alice', date: getDateString(new Date()), status: 'in-progress', priority: 'high', timeEstimate: 4, links: 2 }, |
| { id: 2, title: 'Database Migration', assignee: 'bob', date: getDateString(new Date()), status: 'not-started', priority: 'medium', timeEstimate: 6, links: 0 }, |
| { id: 3, title: 'Frontend Refactor', assignee: 'charlie', date: getDateString(new Date()), status: 'complete', priority: 'low', timeEstimate: 8, links: 1 }, |
| { id: 4, title: 'Security Audit', assignee: 'diana', date: getDateString(new Date()), status: 'blocked', priority: 'high', timeEstimate: 3, links: 3 } |
| ]; |
|
|
| |
| let sprintBoundaries = [ |
| { name: 'Sprint 1', start: new Date(), end: new Date(new Date().setDate(new Date().getDate() + 14)) } |
| ]; |
| |
| function getDateString(date) { |
| return date.toISOString().split('T')[0]; |
| } |
|
|
| function formatDateDisplay(date) { |
| const options = { weekday: 'short', month: 'numeric', day: 'numeric' }; |
| return date.toLocaleDateString('en-US', options); |
| } |
|
|
| function getDateRange(rangeType) { |
| const today = new Date(); |
| const start = new Date(today); |
| const end = new Date(today); |
| |
| switch(rangeType) { |
| case 'this-week': |
| start.setDate(today.getDate() - today.getDay() + 1); |
| end.setDate(start.getDate() + 4); |
| break; |
| case 'next-2-weeks': |
| start.setDate(today.getDate() - today.getDay() + 1); |
| end.setDate(start.getDate() + 9); |
| break; |
| case 'this-quarter': |
| const quarter = Math.floor(today.getMonth() / 3); |
| start.setMonth(quarter * 3, 1); |
| end.setMonth((quarter + 1) * 3, 0); |
| break; |
| default: |
| start.setDate(today.getDate() - today.getDay() + 1); |
| end.setDate(start.getDate() + 4); |
| } |
| |
| return { start, end }; |
| } |
|
|
| function generateDates(startDate, endDate) { |
| const dates = []; |
| const current = new Date(startDate); |
| |
| while (current <= endDate) { |
| dates.push(new Date(current)); |
| current.setDate(current.getDate() + 1); |
| } |
| |
| return dates; |
| } |
|
|
| |
| function initializeCalendar() { |
| const calendarGrid = document.getElementById('calendar-grid'); |
| const { start, end } = getDateRange(currentDateRange); |
| const dates = generateDates(start, end); |
| |
| let calendarHTML = ` |
| <div class="calendar-grid grid" style=" |
| grid-template-columns: 120px repeat(${teamMembers.length}, 200px) 180px; |
| min-width: ${120 + (teamMembers.length * 200) + 180}px; |
| "> |
| <!-- Date Column Header --> |
| <div class="sticky left-0 z-20 bg-gray-50 border-r border-gray-200 p-3 font-semibold text-gray-700"> |
| Date |
| </div> |
| `; |
| |
| teamMembers.forEach(member => { |
| calendarHTML += ` |
| <div class="team-member-header bg-white border-r border-gray-200 p-3 flex items-center justify-between sticky top-0 z-10"> |
| <div class="flex items-center space-x-3"> |
| <img src="${member.avatar}" alt="${member.name}" class="w-8 h-8 rounded-full"> |
| <span class="font-semibold text-gray-800">${member.id}</span> |
| </div> |
| <button class="text-gray-400 hover:text-gray-600 transition-colors"> |
| <i data-feather="x" class="w-4 h-4"></i> |
| </button> |
| </div> |
| `; |
| }); |
| |
| calendarHTML += ` |
| <div class="sticky right-0 z-20 bg-blue-50 border-l border-blue-200 p-3 font-semibold text-blue-800"> |
| Milestones |
| </div> |
| </div> |
| `; |
| |
| dates.forEach((date, index) => { |
| const dateStr = getDateString(date); |
| const isWeekend = date.getDay() === 0 || date.getDay() === 6; |
| const isSprintBoundary = sprintBoundaries.some(sprint => |
| getDateString(sprint.start) === dateStr || getDateString(sprint.end) === dateStr |
| ); |
| |
| |
| const rowClasses = isSprintBoundary ? 'sprint-boundary' : ''; |
| |
| calendarHTML += ` |
| <!-- Date Cell --> |
| <div class="sticky left-0 z-10 bg-gray-50 border-r border-gray-200 p-2 text-sm text-gray-600 ${isWeekend ? 'bg-gray-100' : ''} ${rowClasses}"> |
| ${formatDateDisplay(date)} |
| </div> |
| `; |
| |
| teamMembers.forEach(member => { |
| const cellTasks = tasks.filter(task => |
| task.assignee === member.id && task.date === dateStr |
| ); |
| |
| calendarHTML += ` |
| <div class="cell bg-white border-r border-gray-200 p-1 min-h-16 relative ${isWeekend ? 'bg-gray-50' : ''} ${rowClasses}" |
| data-date="${dateStr}" |
| data-assignee="${member.id}" |
| onclick="handleCellClick(event, '${dateStr}', '${member.id}')" |
| ondragover="handleDragOver(event)" |
| ondrop="handleDrop(event, '${dateStr}', '${member.id}')" |
| > |
| ${renderCellContent(cellTasks)} |
| </div> |
| `; |
| }); |
| |
| calendarHTML += ` |
| <div class="sticky right-0 z-10 bg-blue-50 border-l border-blue-200 p-2 ${isWeekend ? 'bg-blue-100' : ''} ${rowClasses}"> |
| <!-- Milestone content --> |
| </div> |
| `; |
| }); |
| calendarGrid.innerHTML = calendarHTML; |
| feather.replace(); |
| } |
| function renderCellContent(cellTasks) { |
| if (cellTasks.length === 0) return ''; |
| |
| let content = ''; |
| const visibleTasks = cellTasks.slice(0, 3); |
| const hiddenCount = cellTasks.length - 3; |
| |
| visibleTasks.forEach(task => { |
| content += ` |
| <div class="task-item bg-white border border-gray-200 rounded p-1 mb-1 text-xs cursor-pointer" draggable="true" |
| ondragstart="handleDragStart(event, ${task.id})" |
| > |
| <div class="flex items-center justify-between"> |
| <div class="flex items-center space-x-1"> |
| <div class="status-indicator w-2 h-2 rounded-full status-${task.status}"></div> |
| <span class="task-title flex-1 truncate">${task.title}</span> |
| </div> |
| </div> |
| <div class="flex items-center justify-between mt-1"> |
| <span class="time-estimate text-gray-500">${task.timeEstimate}h</span> |
| ${task.links > 0 ? `<span class="link-count text-blue-500">🔗 ${task.links}</span>` : ''} |
| </div> |
| </div> |
| `; |
| }); |
| if (hiddenCount > 0) { |
| content += ` |
| <div class="more-indicator text-center text-gray-400 text-xs py-1"> |
| +${hiddenCount} more |
| </div> |
| `; |
| } |
| return content; |
| } |
|
|
| |
| function handleCellClick(event, date, assignee) { |
| event.stopPropagation(); |
| |
| const cell = event.currentTarget; |
| const cellKey = `${date}-${assignee}`; |
| |
| if (event.ctrlKey || event.metaKey) { |
| |
| if (selectedCells.has(cellKey)) { |
| selectedCells.delete(cellKey); |
| cell.classList.remove('cell-selected'); |
| } else { |
| selectedCells.add(cellKey); |
| cell.classList.add('cell-selected'); |
| } |
| } else if (event.shiftKey && selectedCells.size > 0) { |
| |
| |
| } else { |
| |
| clearCellSelection(); |
| selectedCells.add(cellKey); |
| cell.classList.add('cell-selected'); |
| } |
| } |
|
|
| function clearCellSelection() { |
| document.querySelectorAll('.cell').forEach(cell => { |
| cell.classList.remove('cell-selected'); |
| }); |
| selectedCells.clear(); |
| } |
|
|
| function handleDragStart(event, taskId) { |
| event.dataTransfer.setData('text/plain', taskId.toString()); |
| event.currentTarget.classList.add('dragging'); |
| } |
|
|
| function handleDragOver(event) { |
| event.preventDefault(); |
| event.currentTarget.classList.add('drag-over-valid'); |
| } |
|
|
| function handleDrop(event, date, assignee) { |
| event.preventDefault(); |
| event.currentTarget.classList.remove('drag-over-valid', 'dragging'); |
| |
| const taskId = parseInt(event.dataTransfer.getData('text/plain')); |
| const task = tasks.find(t => t.id === taskId); |
| |
| if (task) { |
| |
| task.assignee = assignee; |
| task.date = date; |
| initializeCalendar(); |
| } |
| } |
|
|
| |
| document.getElementById('quick-add-btn')?.addEventListener('click', function() { |
| |
| const commandPanel = document.querySelector('custom-command-panel'); |
| if (commandPanel && commandPanel.openPanel) { |
| commandPanel.openPanel('task-create'); |
| } |
| }); |
|
|
| |
| document.addEventListener('keydown', function(event) { |
| |
| if ((event.ctrlKey || event.metaKey) && event.key === 'k') { |
| event.preventDefault(); |
| const commandPanel = document.querySelector('custom-command-panel'); |
| if (commandPanel && commandPanel.togglePanel) { |
| commandPanel.togglePanel(); |
| } |
| } |
| |
| |
| if (event.key === 'Escape') { |
| clearCellSelection(); |
| } |
| }); |
|
|
| |
| window.CalendarUtils = { |
| getDateString, |
| formatDateDisplay, |
| getDateRange, |
| generateDates, |
| teamMembers, |
| tasks, |
| selectedCells, |
| clearCellSelection |
| }; |