No workforce data to display.
`;
}
renderRoleChart(metrics.roleData);
renderDeptChart(metrics.deptData);
};
const renderRoleChart = (roleData) => {
const ctx = elements.roleChartCanvas?.getContext('2d');
if (!ctx) return;
const labels = Object.keys(roleData);
if (roleChart) roleChart.destroy();
roleChart = new Chart(ctx, {
type: 'bar',
data: {
labels,
datasets: [
{ label: 'Current Headcount', data: labels.map(l => roleData[l].current), backgroundColor: '#a5b4fc' },
{ label: 'Target Headcount', data: labels.map(l => roleData[l].target), backgroundColor: '#4f46e5' }
]
},
options: { responsive: true, maintainAspectRatio: true, indexAxis: 'y' }
});
};
const renderDeptChart = (deptData) => {
const ctx = elements.deptChartCanvas?.getContext('2d');
if (!ctx) return;
const labels = Object.keys(deptData);
const data = Object.values(deptData);
if (deptChart) deptChart.destroy();
deptChart = new Chart(ctx, {
type: 'pie',
data: { labels, datasets: [{ label: 'Salary Budget', data, backgroundColor: ['#3b82f6', '#10b981', '#ef4444', '#f59e0b', '#8b5cf6'], hoverOffset: 4 }] },
options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { position: 'top' }, tooltip: { callbacks: { label: (c) => `${c.label}: ${formatCurrency(c.raw)}` } } } }
});
};
const renderConfigPanel = () => {
const container = elements.editableList;
if (!container) return;
container.innerHTML = '';
workforceData.forEach(d => {
const card = document.createElement('div');
card.className = 'grid grid-cols-1 md:grid-cols-4 gap-4 items-center bg-white p-3 rounded-lg border';
card.innerHTML = `
`;
container.appendChild(card);
});
};
const handleAddLog = (e) => {
e.preventDefault();
try {
const getNumericValue = (id) => parseInt(document.getElementById(id).value) || 0;
const roleName = document.getElementById('roleName').value;
const existingRecord = workforceData.find(d => d.role.toLowerCase() === roleName.toLowerCase());
if (existingRecord) {
// Update existing record
existingRecord.department = document.getElementById('department').value;
existingRecord.current = getNumericValue('currentHeadcount');
existingRecord.target = getNumericValue('targetHeadcount');
existingRecord.salary = getNumericValue('avgSalary');
showToast('Role updated!');
} else {
// Add new record
const newLog = {
id: workforceData.length > 0 ? Math.max(...workforceData.map(d => d.id)) + 1 : 1,
role: roleName,
department: document.getElementById('department').value,
current: getNumericValue('currentHeadcount'),
target: getNumericValue('targetHeadcount'),
salary: getNumericValue('avgSalary'),
};
workforceData.push(newLog);
showToast('Role added!');
}
elements.addForm.reset();
renderAll();
} catch (error) {
console.error("Error adding record:", error);
showToast("Error: Could not add record.");
}
};
const handleConfigListClick = (e) => {
const target = e.target.closest('button');
if (!target) return;
const action = target.dataset.action;
const id = parseInt(target.dataset.id, 10);
if (action === 'update') {
const index = workforceData.findIndex(d => d.id === id);
if (index === -1) return;
const inputs = target.closest('.grid').querySelectorAll('input');
inputs.forEach(input => {
const field = input.dataset.field;
let value = input.value;
if (input.type === 'number') value = parseInt(value) || 0;
workforceData[index][field] = value;
});
} else if (action === 'delete') {
workforceData = workforceData.filter(d => d.id !== id);
}
renderAll();
showToast(`Record ${action}d successfully!`);
};
const handleGeneratePDF = () => {
try {
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
const metrics = calculateMetrics();
doc.setFontSize(20);
doc.text("Workforce Planning Report", 105, 20, null, null, 'center');
doc.setFontSize(10);
doc.text(`Generated on: ${new Date().toLocaleDateString()}`, 105, 26, null, null, 'center');
doc.autoTable({ startY: 35, head: [['Metric', 'Value']], body: [ ['Total Headcount', metrics.headcount], ['Salary Budget (Annual)', formatCurrency(metrics.budget)], ['Open Positions', metrics.open], ['Headcount vs. Target', `${metrics.targetRate.toFixed(0)}%`] ], theme: 'grid' });
let finalY = doc.lastAutoTable.finalY || 60;
doc.setFontSize(14);
doc.text("Headcount vs. Target by Role", 14, finalY + 15);
doc.addImage(elements.roleChartCanvas.toDataURL('image/png', 1.0), 'PNG', 14, finalY + 20, 180, 90);
finalY += 105;
doc.setFontSize(14);
doc.text("Role Breakdown", 14, finalY);
const sortedData = [...workforceData].sort((a, b) => b.current - a.current);
const tableBody = sortedData.map(d => [d.role, d.department, d.current, d.target, formatCurrency(d.salary)]);
doc.autoTable({ head: [['Role', 'Department', 'Current', 'Target', 'Avg. Salary']], body: tableBody, startY: finalY + 5, theme: 'striped', headStyles: { fillColor: [220, 38, 38] } });
doc.save('Workforce-Planning-Report.pdf');
} catch (error) {
console.error("Failed to generate PDF:", error);
showToast("Error: Could not generate PDF.");
}
};
const switchTab = (tabName) => {
currentTab = tabName;
Object.values(elements.tabs).forEach(tab => tab.classList.add('hidden'));
Object.values(elements.tabButtons).forEach(btn => btn.classList.remove('active'));
elements.tabs[tabName].classList.remove('hidden');
elements.tabButtons[tabName].classList.add('active');
updateNavButtons();
};
const navigateTabs = (direction) => {
const currentIndex = tabOrder.indexOf(currentTab);
const newIndex = direction === 'next' ? Math.min(currentIndex + 1, tabOrder.length - 1) : Math.max(currentIndex - 1, 0);
if (newIndex !== currentIndex) switchTab(tabOrder[newIndex]);
};
const updateNavButtons = () => {
const currentIndex = tabOrder.indexOf(currentTab);
elements.navButtons.prev.disabled = currentIndex === 0;
elements.navButtons.next.disabled = currentIndex === tabOrder.length - 1;
};
const showToast = (message) => {
if (!elements.toast) return;
elements.toast.textContent = message;
elements.toast.classList.add('show');
setTimeout(() => { elements.toast.classList.remove('show'); }, 3000);
}
elements.addForm.addEventListener('submit', handleAddLog);
elements.editableList.addEventListener('click', handleConfigListClick);
elements.pdfButton.addEventListener('click', handleGeneratePDF);
elements.tabButtons.dashboard.addEventListener('click', () => switchTab('dashboard'));
elements.tabButtons.config.addEventListener('click', () => switchTab('config'));
elements.navButtons.prev.addEventListener('click', () => navigateTabs('prev'));
elements.navButtons.next.addEventListener('click', () => navigateTabs('next'));
renderAll();
updateNavButtons();
});