`;
// Render details table
if (cases.length === 0) {
dashboardDetails.innerHTML = `
`;
dashboardDetails.innerHTML = detailsHTML;
// Render PDF button
pdfDownloadContainer.innerHTML = `
`;
// Re-attach listener as button is re-rendered
document.getElementById('download-pdf-btn').addEventListener('click', generatePDF);
}
// --- EVENT HANDLERS & LOGIC ---
function handleAddRole(e) {
e.preventDefault();
const nameInput = document.getElementById('role-name');
const rateInput = document.getElementById('hourly-rate');
const newRole = {
id: nextRoleId++,
name: nameInput.value.trim(),
rate: parseFloat(rateInput.value)
};
teamRoles.push(newRole);
addRoleForm.reset();
renderAll();
}
function handleRoleActions(e) {
if (e.target.classList.contains('delete-role-btn')) {
const roleId = parseInt(e.target.dataset.id);
if(confirm('Are you sure you want to delete this role? This will also remove all allocations with this role from all cases.')) {
deleteRole(roleId);
}
}
}
function deleteRole(roleId) {
// Remove role from main array
teamRoles = teamRoles.filter(r => r.id !== roleId);
// Remove any allocations using this role
cases.forEach(caseItem => {
caseItem.allocations = caseItem.allocations.filter(alloc => alloc.roleId !== roleId);
});
renderAll();
}
function handleAddCase(e) {
e.preventDefault();
const nameInput = document.getElementById('case-name');
const newCase = {
id: nextCaseId++,
name: nameInput.value.trim(),
allocations: []
};
cases.push(newCase);
addCaseForm.reset();
renderAll();
}
function handleCaseOrAllocationActions(e) {
// Delete Case
if (e.target.classList.contains('delete-case-btn')) {
const caseId = parseInt(e.target.dataset.id);
if (confirm('Are you sure you want to delete this entire case and all its allocations?')) {
cases = cases.filter(c => c.id !== caseId);
renderAll();
}
}
// Delete Allocation
if (e.target.classList.contains('delete-allocation-btn')) {
const caseId = parseInt(e.target.dataset.caseId);
const allocIndex = parseInt(e.target.dataset.allocIndex);
const caseItem = cases.find(c => c.id === caseId);
if (caseItem) {
caseItem.allocations.splice(allocIndex, 1);
renderAll();
}
}
}
function handleAllocationFormSubmit(e) {
if(e.target.classList.contains('add-allocation-form')) {
e.preventDefault();
const form = e.target;
const caseId = parseInt(form.elements.caseId.value);
const roleId = parseInt(form.elements.roleId.value);
const hours = parseInt(form.elements.hours.value);
if (isNaN(roleId) || isNaN(hours)) return;
const caseItem = cases.find(c => c.id === caseId);
if (caseItem) {
// Check if role already allocated, if so, add hours. Otherwise, create new allocation.
const existingAllocation = caseItem.allocations.find(a => a.roleId === roleId);
if (existingAllocation) {
existingAllocation.hours += hours;
} else {
caseItem.allocations.push({ roleId, hours });
}
form.reset();
renderAll();
}
}
}
// --- UTILITY FUNCTIONS ---
function calculateCaseCost(caseId) {
const caseItem = cases.find(c => c.id === caseId);
if (!caseItem) return 0;
return caseItem.allocations.reduce((sum, alloc) => {
const role = teamRoles.find(r => r.id === alloc.roleId);
return sum + (role ? alloc.hours * role.rate : 0);
}, 0);
}
function switchTab(tabName) {
// Update buttons
tabNavigation.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.toggle('active', btn.dataset.tab === tabName);
});
// Update content
tabContents.querySelectorAll('.tab-content').forEach(content => {
content.classList.toggle('active', content.id === `${tabName}-tab`);
});
updateNavButtons();
}
function navigateTabs(direction) {
currentTabIndex += direction;
if (currentTabIndex < 0) currentTabIndex = 0;
if (currentTabIndex >= tabs.length) currentTabIndex = tabs.length - 1;
switchTab(tabs[currentTabIndex]);
}
function updateNavButtons() {
prevBtn.disabled = currentTabIndex === 0;
prevBtn.classList.toggle('opacity-50', prevBtn.disabled);
prevBtn.classList.toggle('cursor-not-allowed', prevBtn.disabled);
nextBtn.disabled = currentTabIndex === tabs.length - 1;
nextBtn.classList.toggle('opacity-50', nextBtn.disabled);
nextBtn.classList.toggle('cursor-not-allowed', nextBtn.disabled);
}
// --- PDF GENERATION ---
function generatePDF() {
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
// Title
doc.setFontSize(20);
doc.setFont("helvetica", "bold");
doc.text("Legal Firm Resource Allocation Summary", 105, 20, { align: 'center' });
doc.setFontSize(10);
doc.setFont("helvetica", "normal");
doc.text(`Generated on: ${new Date().toLocaleDateString()}`, 105, 26, { align: 'center' });
// Overall Summary
const grandTotalCost = cases.reduce((sum, c) => sum + calculateCaseCost(c.id), 0);
const totalCases = cases.length;
const totalHours = cases.reduce((sum, c) => sum + c.allocations.reduce((s, a) => s + a.hours, 0), 0);
doc.autoTable({
startY: 35,
head: [['Metric', 'Value']],
body: [
['Total Projected Cost', `$${grandTotalCost.toFixed(2)}`],
['Total Number of Cases', totalCases],
['Total Allocated Hours', totalHours],
],
theme: 'grid',
headStyles: { fillColor: [41, 128, 185] }, // A professional blue
});
// Detailed breakdown per case
doc.setFontSize(16);
doc.setFont("helvetica", "bold");
doc.text("Detailed Case Breakdown", 14, doc.autoTable.previous.finalY + 15);
const caseData = cases.map(caseItem => {
const head = [['Role', 'Hourly Rate', 'Allocated Hours', 'Subtotal Cost']];
const body = caseItem.allocations.map(alloc => {
const role = teamRoles.find(r => r.id === alloc.roleId);
if (!role) return [];
const subtotal = role.rate * alloc.hours;
return [role.name, `$${role.rate.toFixed(2)}`, alloc.hours, `$${subtotal.toFixed(2)}`];
});
const totalCost = calculateCaseCost(caseItem.id);
body.push([{ content: 'Case Total', colSpan: 3, styles: { fontStyle: 'bold', halign: 'right' } }, { content: `$${totalCost.toFixed(2)}`, styles: { fontStyle: 'bold' } }]);
return {
name: caseItem.name,
head: head,
body: body,
};
});
let currentY = doc.autoTable.previous.finalY + 20;
caseData.forEach(data => {
doc.setFontSize(12);
doc.setFont("helvetica", "bold");
doc.text(`Case: ${data.name}`, 14, currentY);
doc.autoTable({
startY: currentY + 2,
head: data.head,
body: data.body,
theme: 'striped',
headStyles: { fillColor: [52, 73, 94] } // Darker blue/gray
});
currentY = doc.autoTable.previous.finalY + 10;
});
doc.save('Legal_Firm_Resource_Plan.pdf');
}
// --- START THE APP ---
init();
});
No data to display. Add cases and allocations to see the dashboard.
`; pdfDownloadContainer.innerHTML = ''; return; } let detailsHTML = `Case Cost Breakdown
| Case Name | Total Hours | Total Cost |
|---|---|---|
| ${caseItem.name} | ${totalHours} | $${totalCost.toFixed(2)} |
