Dashboard

Total Team Members 0
Total Tasks 0
Unassigned Tasks 0
Overdue Tasks 0

Team Workload Overview (Assigned Effort vs. Capacity)

High Priority Unassigned Tasks

  • No high priority unassigned tasks.

Manage Team & Tasks

Team Members

Existing Members:

    Tasks

    All Tasks:

      Assign Tasks

      Unassigned Tasks

        Task Details & Suggestions

        Select an unassigned task to see details and suggestions.

        Assigned Tasks by Team Member

        Description: ${task.description || 'N/A'}

        Effort: ${task.effort}h | Priority: ${task.priority} ${task.dueDate ? ` | Due: ${new Date(task.dueDate + 'T00:00:00').toLocaleDateString()}` : ''}

        Required Skills: ${task.requiredSkills.length > 0 ? ttp_getSkillTagsHTML(task.requiredSkills) : 'None specified'}

        `; document.getElementById('ttp-suggestionsContainer').style.display = 'block'; ttp_generateSuggestions(task); ttp_populateFullAssigneeDropdown(task); } function ttp_generateSuggestions(task) { const suggestionsListEl = document.getElementById('ttp-suggestedAssigneesList'); suggestionsListEl.innerHTML = ''; const suggestions = ttp_teamMembers.map(member => { const skillMatchCount = task.requiredSkills.filter(skill => member.skills.includes(skill)).length; const hasAllRequiredSkills = skillMatchCount === task.requiredSkills.length; const currentLoad = ttp_tasks.filter(t => t.assignedTo === member.id && t.status !== 'Done') .reduce((sum, t) => sum + (parseFloat(t.effort) || 0), 0); const loadPercent = member.capacity > 0 ? (currentLoad / member.capacity) * 100 : (currentLoad > 0 ? 999 : 0); // 999 if capacity 0 but has load let score = 0; if (task.requiredSkills.length > 0) { // Skill-based scoring only if skills are required if(hasAllRequiredSkills) score += 100; // Strong preference for all skills else score += skillMatchCount * 20; // Points per matching skill } else { score += 50; // Base score if no specific skills required (anybody can do it) } score -= loadPercent / 5; // Penalize for higher load (adjust divisor for sensitivity) return { ...member, skillMatchCount, hasAllRequiredSkills, currentLoad, loadPercent, score }; }).sort((a,b) => b.score - a.score); // Sort by score descending if (suggestions.length === 0) { suggestionsListEl.innerHTML = '
      • No team members available to suggest.
      • '; return; } suggestions.slice(0, 5).forEach(member => { // Show top 5 suggestions const li = document.createElement('li'); let skillMatchText = ''; if (task.requiredSkills.length > 0) { skillMatchText = member.hasAllRequiredSkills ? 'Matches all skills!' : member.skillMatchCount > 0 ? `Matches ${member.skillMatchCount} skill(s)` : 'No skill match'; } else { skillMatchText = 'No specific skills required'; } li.innerHTML = `
        ${member.name} (${member.role}) - Load: ${member.loadPercent.toFixed(0)}% (${member.currentLoad}h / ${member.capacity}h)
        ${skillMatchText}
        Member Skills: ${ttp_getSkillTagsHTML(member.skills)}
        `; suggestionsListEl.appendChild(li); }); } function ttp_populateFullAssigneeDropdown() { const selectEl = document.getElementById('ttp-assignToMemberDropdown'); selectEl.innerHTML = ''; ttp_teamMembers.forEach(member => { const option = document.createElement('option'); option.value = member.id; option.textContent = `${member.name} (Load: ${(ttp_tasks.filter(t => t.assignedTo === member.id && t.status !== 'Done').reduce((s, t) => s + t.effort, 0) / member.capacity * 100 || 0).toFixed(0)}%)`; selectEl.appendChild(option); }); document.getElementById('ttp-confirmAssignmentBtn').onclick = () => { const memberId = selectEl.value; if (memberId) ttp_confirmAssignTask(memberId); else alert("Please select a team member."); }; } function ttp_confirmAssignTask(memberId) { if (!ttp_selectedTaskForAssignment) { alert("No task selected for assignment."); return; } const task = ttp_tasks.find(t => t.id === ttp_selectedTaskForAssignment.id); if (task) { task.assignedTo = memberId; task.status = (task.status === 'To Do' || !task.status) ? 'In Progress' : task.status; // Auto-set to In Progress if was To Do ttp_saveData(); ttp_selectedTaskForAssignment = null; // Clear selection ttp_renderAssignTasksView(); ttp_renderTaskList(); ttp_renderDashboard(); alert(`Task "${task.name}" assigned successfully.`); } else { alert("Error assigning task. Task not found."); } } function ttp_renderAssignedTasksByMember() { const containerEl = document.getElementById('ttp-assignedTasksByMemberContainer'); containerEl.innerHTML = ''; ttp_teamMembers.forEach(member => { const memberSection = document.createElement('div'); memberSection.classList.add('ttp-section'); memberSection.innerHTML = `

        ${member.name} (Load: ${(ttp_tasks.filter(t => t.assignedTo === member.id && t.status !== 'Done').reduce((s, t) => s + t.effort, 0) / member.capacity * 100 || 0).toFixed(0)}%)

        `; const assignedList = document.createElement('ul'); assignedList.classList.add('ttp-list'); const tasksForMember = ttp_tasks.filter(t => t.assignedTo === member.id) .sort((a,b) => ({"High":0, "Medium":1, "Low":2})[a.priority] - ({"High":0, "Medium":1, "Low":2})[b.priority]); if (tasksForMember.length === 0) { assignedList.innerHTML = '
      • No tasks assigned.
      • '; } else { tasksForMember.forEach(task => { const li = document.createElement('li'); li.style.borderLeft = `3px solid ${task.status === 'Done' ? '#bdc3c7' : (task.priority === 'High' ? '#E74C3C' : (task.priority === 'Medium' ? '#F39C12' : '#3498DB'))}`; li.innerHTML = `
        ${task.name} (Effort: ${task.effort}h, Status: ${task.status})
        Priority: ${task.priority} ${task.dueDate ? ` - Due: ${new Date(task.dueDate + 'T00:00:00').toLocaleDateString()}` : ''}
        `; assignedList.appendChild(li); }); } memberSection.appendChild(assignedList); containerEl.appendChild(memberSection); }); } function ttp_editTaskFromAssignmentView(taskId) { // Open Manage Data tab and trigger edit for that task ttp_openTab(null, 'ttp-manageDataTab'); setTimeout(() => ttp_editTask(taskId), 50); // Ensure tab is rendered } function ttp_unassignTask(taskId) { const task = ttp_tasks.find(t => t.id === taskId); if (task) { task.assignedTo = null; task.status = 'To Do'; // Reset status ttp_saveData(); ttp_renderAssignTasksView(); ttp_renderTaskList(); ttp_renderDashboard(); } } // PDF Download document.getElementById('ttp-downloadPdfBtn').onclick = async () => { const { jsPDF } = window.jspdf; const pdf = new jsPDF('p', 'mm', 'a4'); let currentY = 15; const margin = 15; const pageWidth = pdf.internal.pageSize.getWidth(); const contentWidth = pageWidth - 2 * margin; const accentColor = '#2ECC71'; const textColor = '#333333'; // Title pdf.setFontSize(20); pdf.setTextColor(accentColor); pdf.text("Team Task Distribution Plan", pageWidth / 2, currentY, { align: 'center' }); currentY += 8; pdf.setFontSize(10); pdf.setTextColor(textColor); pdf.text(`Generated: ${new Date().toLocaleString()}`, pageWidth / 2, currentY, { align: 'center' }); currentY += 10; // Team Roster pdf.setFontSize(14); pdf.setTextColor(accentColor); pdf.text("Team Roster", margin, currentY); currentY += 7; const teamHeaders = [["Name", "Role", "Skills", "Capacity (h)", "Assigned (h)", "Load (%)"]]; const teamBody = ttp_teamMembers.map(m => { const assignedEffort = ttp_tasks.filter(t => t.assignedTo === m.id && t.status !== 'Done').reduce((s, t) => s + t.effort, 0); const loadPercent = m.capacity > 0 ? (assignedEffort / m.capacity * 100).toFixed(0) + '%' : (assignedEffort > 0 ? 'N/A (Cap 0)' : '0%'); return [m.name, m.role || '-', m.skills.join(', ') || '-', m.capacity, assignedEffort, loadPercent]; }); pdf.autoTable({ head: teamHeaders, body: teamBody, startY: currentY, theme: 'grid', headStyles: { fillColor: accentColor } }); currentY = pdf.lastAutoTable.finalY + 10; // Task Distribution by Member pdf.setFontSize(14); pdf.setTextColor(accentColor); if (currentY > pdf.internal.pageSize.getHeight() - 30) { pdf.addPage(); currentY = margin; } pdf.text("Task Distribution by Member", margin, currentY); currentY += 7; ttp_teamMembers.forEach(member => { if (currentY > pdf.internal.pageSize.getHeight() - 40) { pdf.addPage(); currentY = margin; } pdf.setFontSize(12); pdf.setTextColor(textColor); pdf.text(member.name, margin, currentY); currentY += 6; const memberTasks = ttp_tasks.filter(t => t.assignedTo === member.id); if (memberTasks.length > 0) { const taskHeaders = [["Task", "Priority", "Effort (h)", "Due Date", "Status"]]; const taskBody = memberTasks.map(t => [ t.name, t.priority, t.effort, t.dueDate ? new Date(t.dueDate + 'T00:00:00').toLocaleDateString() : '-', t.status ]); pdf.autoTable({ head: taskHeaders, body: taskBody, startY: currentY, theme: 'striped', headStyles: { fillColor: '#E8F8F5', textColor: '#28B463' }, styles: { fontSize: 9 } }); currentY = pdf.lastAutoTable.finalY + 8; } else { pdf.setFontSize(9); pdf.text(" - No tasks assigned.", margin + 5, currentY); currentY += 6; } }); // Unassigned Tasks if (currentY > pdf.internal.pageSize.getHeight() - 30) { pdf.addPage(); currentY = margin; } pdf.setFontSize(14); pdf.setTextColor(accentColor); pdf.text("Unassigned Tasks", margin, currentY); currentY += 7; const unassignedTasksPdf = ttp_tasks.filter(t => !t.assignedTo && t.status !== 'Done'); if (unassignedTasksPdf.length > 0) { const unassignedHeaders = [["Task", "Priority", "Effort (h)", "Req. Skills", "Due Date"]]; const unassignedBody = unassignedTasksPdf.map(t => [ t.name, t.priority, t.effort, t.requiredSkills.join(', ') || '-', t.dueDate ? new Date(t.dueDate + 'T00:00:00').toLocaleDateString() : '-' ]); pdf.autoTable({ head: unassignedHeaders, body: unassignedBody, startY: currentY, theme: 'grid', headStyles: {fillColor: accentColor } }); currentY = pdf.lastAutoTable.finalY + 10; } else { pdf.setFontSize(10); pdf.text("No unassigned tasks.", margin, currentY); currentY += 7; } // Overdue Tasks const todayStr = new Date().toISOString().split('T')[0]; const overdueTasksPdf = ttp_tasks.filter(t => t.dueDate && t.dueDate < todayStr && t.status !== 'Done'); if (overdueTasksPdf.length > 0) { if (currentY > pdf.internal.pageSize.getHeight() - 30) { pdf.addPage(); currentY = margin; } pdf.setFontSize(14); pdf.setTextColor('#E74C3C'); // Red for overdue pdf.text("Overdue Tasks", margin, currentY); currentY += 7; const overdueHeaders = [["Task", "Assigned To", "Due Date", "Priority"]]; const overdueBody = overdueTasksPdf.map(t => { const assignee = t.assignedTo ? ttp_teamMembers.find(m => m.id === t.assignedTo) : null; return [t.name, assignee ? assignee.name : 'Unassigned', new Date(t.dueDate + 'T00:00:00').toLocaleDateString(), t.priority]; }); pdf.autoTable({ head: overdueHeaders, body: overdueBody, startY: currentY, theme: 'grid', headStyles: {fillColor: '#E74C3C'} }); } pdf.save(`Team_Task_Plan_${new Date().toISOString().slice(0,10)}.pdf`); alert('PDF report is being downloaded.'); }; // Initial Load document.addEventListener('DOMContentLoaded', () => { ttp_openTab(null, ttp_tabs[0]); // Open Dashboard by default const firstTabButton = document.querySelector(`.ttp-tab-button[onclick*="${ttp_tabs[0]}"]`); if (firstTabButton) firstTabButton.classList.add('active'); ttp_updateNavButtons(); });
        Scroll to Top