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:
Assign Tasks
Task Details & Suggestions
Select an unassigned task to see details and suggestions.
Suggested Team Members:
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();
});