Legal Firm Resource Allocation Planner

Legal Firm Resource Allocation Planner

Allocation Dashboard

Add New Case

Case List & Allocations

No cases added yet. Add a case to begin allocating resources.

Add Team Role

Current Team Roles

Role Name Hourly Rate Actions

${totalAllocatedHours}

`; // Render details table if (cases.length === 0) { dashboardDetails.innerHTML = `

No data to display. Add cases and allocations to see the dashboard.

`; pdfDownloadContainer.innerHTML = ''; return; } let detailsHTML = `

Case Cost Breakdown

`; cases.forEach(caseItem => { const totalHours = caseItem.allocations.reduce((sum, alloc) => sum + alloc.hours, 0); const totalCost = calculateCaseCost(caseItem.id); detailsHTML += ` `; }); detailsHTML += `
Case Name Total Hours Total Cost
${caseItem.name} ${totalHours} $${totalCost.toFixed(2)}
`; 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(); });
Scroll to Top