Carpooling vs. Public Transport Expense Tracker
Step 1: Define Your Primary Commute & Costs
Global Settings
Route Details
Carpooling Details
Public Transport Details
Step 2: Time & Other Considerations
Commute Time (One-Way in Minutes)
Qualitative Factors (For Personal Consideration & PDF Notes)
Rate the following for each mode (1=Very Poor, 5=Neutral, 10=Excellent). These are not used in cost calculation but can be noted in your PDF.
Carpooling
Public Transport
Step 3: Projected Cost Analysis
Enter details in Tab 1 & 2 and click "Calculate/Refresh Analysis" to see the analysis.
Step 4: Summary & Download
Complete the analysis on Tab 3 to view the summary.
No active commute methods selected. Please go to Tab 1 (Commute Profile) and select a 'Role in Carpool' (Driver/Passenger) or set 'Do you use Public Transport?' to 'Yes', then fill in the relevant details.
"; document.getElementById('downloadCommutePdfButton').disabled = true; return; } const valuePerHour = getNumValue('valuePerHourCT', 0); const distUnit = getStrValue('distanceUnit'); const fuelVolUnit = getStrValue('fuelVolumeUnit'); const routeName = getStrValue('routeNameCT', 'Primary Commute'); const oneWayDistance = getNumValue('oneWayDistanceCT', 0); const roundTripsPerWeek = getNumValue('roundTripsPerWeekCT', 0); const monthlyRoundTrips = roundTripsPerWeek * 4.3333; // More precise average const totalMonthlyDistance = oneWayDistance * 2 * monthlyRoundTrips; if (oneWayDistance <= 0 || roundTripsPerWeek <= 0) { document.getElementById('commuteAnalysisResultsContainer').innerHTML = "Incomplete route details. Please ensure 'One-way Distance' and 'Round Trips per Week' are greater than zero in Tab 1.
"; document.getElementById('downloadCommutePdfButton').disabled = true; return; } commuteAnalysisData.inputs = { routeName, oneWayDistance, roundTripsPerWeek, totalMonthlyDistance, distUnit, fuelVolUnit, valuePerHour, carpool: {}, publicTransport: {}, timeQualitative:{} }; let results = []; // Carpooling Calculations const carpoolRole = getStrValue('carpoolRoleCT'); if (carpoolRole === 'Driver') { const fuelEfficiencyInput = getNumValue('fuelEfficiencyCT', 0); const fuelCostPerUnit = getNumValue('fuelCostCT', 0); const maintenancePerDist = getNumValue('maintenanceCostCT', 0); const parkingMonthly = getNumValue('parkingCostMonthlyCT', 0); const tollsPerRound = getNumValue('tollsPerRoundTripCT', 0); let carpoolSize = getNumValue('carpoolSizeCT', 1); if (carpoolSize < 1) carpoolSize = 1; // Driver is always 1 const contribPerPassengerMonthly = getNumValue('contributionPerPassengerMonthlyCT', 0); let fuelEfficiency = fuelEfficiencyInput; // If L/100km, we need to ensure it's not 0 for division if it was defaulted. // If MPG, same. if (fuelEfficiency === 0) { // Prevent division by zero if user entered 0 mistakenly results.push({ mode: "Carpooling (Driver) - ERROR", monthlyCost: Infinity, // Indicate error error: "Fuel efficiency cannot be zero." }); } else { let totalFuelConsumedMonthly; if (distUnit === 'miles' && fuelVolUnit === 'gallon') { // MPG totalFuelConsumedMonthly = totalMonthlyDistance / fuelEfficiency; } else if (distUnit === 'km' && fuelVolUnit === 'liter') { // L/100km totalFuelConsumedMonthly = (totalMonthlyDistance / 100) * fuelEfficiency; } else { totalFuelConsumedMonthly = totalMonthlyDistance / fuelEfficiency; } const fuelMonthlyCost = totalFuelConsumedMonthly * fuelCostPerUnit; const maintenanceMonthlyCost = totalMonthlyDistance * maintenancePerDist; const tollsMonthlyCost = tollsPerRound * monthlyRoundTrips; const grossDrivingCostMonthly = fuelMonthlyCost + maintenanceMonthlyCost + parkingMonthly + tollsMonthlyCost; const totalContributionsMonthly = carpoolSize > 1 ? (carpoolSize - 1) * contribPerPassengerMonthly : 0; const netDrivingCostMonthly = grossDrivingCostMonthly - totalContributionsMonthly; results.push({ mode: "Carpooling (Driver)", monthlyCost: netDrivingCostMonthly, costPerTrip: monthlyRoundTrips > 0 ? netDrivingCostMonthly / (monthlyRoundTrips * 2) : 0, // per one-way trip costPerDistance: totalMonthlyDistance > 0 ? netDrivingCostMonthly / totalMonthlyDistance : 0, }); } commuteAnalysisData.inputs.carpool = { role: 'Driver', fuelEfficiency: fuelEfficiencyInput, fuelCostPerUnit, maintenancePerDist, parkingMonthly, tollsPerRound, carpoolSize, contribPerPassengerMonthly }; } else if (carpoolRole === 'Passenger') { const costToDriverMonthly = getNumValue('costToDriverMonthlyCT', 0); results.push({ mode: "Carpooling (Passenger)", monthlyCost: costToDriverMonthly, costPerTrip: monthlyRoundTrips > 0 ? costToDriverMonthly / (monthlyRoundTrips * 2) : 0, costPerDistance: totalMonthlyDistance > 0 ? costToDriverMonthly / totalMonthlyDistance : 0, }); commuteAnalysisData.inputs.carpool = { role: 'Passenger', costToDriverMonthly }; } // Public Transport Calculations const ptUsage = getStrValue('publicTransportUsageCT'); if (ptUsage === 'Yes') { const ptCostPerTrip = getNumValue('ptCostPerTripCT', 0); const ptPassCost = getNumValue('ptPassCostCT', 0); const ptPassType = getStrValue('ptPassTypeCT', 'Monthly'); let ptMonthlyCost = 0; if (ptPassType === "None" || ptPassCost === 0) { ptMonthlyCost = ptCostPerTrip * 2 * monthlyRoundTrips; } else { if (ptPassType === 'Monthly') ptMonthlyCost = ptPassCost; else if (ptPassType === 'Weekly') ptMonthlyCost = ptPassCost * 4.3333; else if (ptPassType === 'Daily') ptMonthlyCost = ptPassCost * monthlyRoundTrips; } results.push({ mode: "Public Transport", monthlyCost: ptMonthlyCost, costPerTrip: monthlyRoundTrips > 0 ? ptMonthlyCost / (monthlyRoundTrips * 2) : 0, costPerDistance: totalMonthlyDistance > 0 ? ptMonthlyCost / totalMonthlyDistance : 0, }); commuteAnalysisData.inputs.publicTransport = { usage: 'Yes', ptCostPerTrip, ptPassCost, ptPassType }; } else { commuteAnalysisData.inputs.publicTransport = { usage: 'No' }; } const timeCarpoolMinutes = getNumValue('timeCarpoolCT', 0); const timePublicTransportMinutes = getNumValue('timePublicTransportCT', 0); commuteAnalysisData.inputs.timeQualitative = { timeCarpoolMinutes, timePublicTransportMinutes, carpoolComfort: getNumValue('carpoolComfortCT',0), carpoolConvenience: getNumValue('carpoolConvenienceCT',0), carpoolReliability: getNumValue('carpoolReliabilityCT',0), ptComfort: getNumValue('ptComfortCT',0), ptConvenience: getNumValue('ptConvenienceCT',0), ptReliability: getNumValue('ptReliabilityCT',0), additionalNotes: getStrValue('additionalNotesCT') }; if (valuePerHour > 0) { results.forEach(r => { if (r.error) { // Skip if there was an error like zero fuel efficiency r.monthlyTimeHours = 0; r.monetizedTimeCost = 0; r.grandTotalCost = r.monthlyCost; // or Infinity to show it's problematic return; } let oneWayTimeMinutes = 0; if (r.mode.includes("Carpooling")) oneWayTimeMinutes = timeCarpoolMinutes; else if (r.mode.includes("Public Transport")) oneWayTimeMinutes = timePublicTransportMinutes; const monthlyTimeHours = (oneWayTimeMinutes / 60) * 2 * monthlyRoundTrips; r.monthlyTimeHours = monthlyTimeHours; r.monetizedTimeCost = monthlyTimeHours * valuePerHour; r.grandTotalCost = r.monthlyCost + r.monetizedTimeCost; }); } commuteAnalysisData.results = results; displayCommuteAnalysisResults(results); document.getElementById('downloadCommutePdfButton').disabled = results.filter(r => !r.error).length === 0; } function displayCommuteAnalysisResults(results) { const container = document.getElementById('commuteAnalysisResultsContainer'); if (!results || results.length === 0) { // Enhanced check container.innerHTML = "No valid commute methods configured or essential data (like distance/trips) is missing. Please check inputs in Tab 1.
"; return; } const validResults = results.filter(r => !r.error); if (validResults.length === 0 && results.length > 0 && results[0].error) { // Only error results container.innerHTML = `Error in calculation: ${results[0].error} Please check your inputs in Tab 1.
`; return; } if (validResults.length === 0) { // No valid results even if no explicit error was caught earlier container.innerHTML = "Could not calculate analysis for any mode. Please verify all inputs in Tab 1, especially distance, trips, and mode-specific costs/efficiencies.
"; return; } let tableHTML = `| Commute Mode | Est. Monthly Cost ($) | Est. Cost/Trip ($) | Est. Cost/${commuteAnalysisData.inputs.distUnit} ($) | `; if (commuteAnalysisData.inputs.valuePerHour > 0) { tableHTML += `Est. Monthly Time (hrs) | Monetized Time Cost ($) | Grand Total Cost ($) | `; } tableHTML += `
|---|---|---|---|---|---|---|
| ${r.mode} | ${r.monthlyCost.toFixed(2)} | ${r.costPerTrip.toFixed(2)} | ${r.costPerDistance.toFixed(3)} | `; if (commuteAnalysisData.inputs.valuePerHour > 0) { tableHTML += `${(r.monthlyTimeHours || 0).toFixed(1)} | ${(r.monetizedTimeCost || 0).toFixed(2)} | ${(r.grandTotalCost || 0).toFixed(2)} | `; } tableHTML += `
Please calculate the analysis on Tab 3 first. Ensure valid inputs are provided in Tab 1 & 2.
"; return; } const validResults = commuteAnalysisData.results.filter(r => !r.error); let summaryHTML = `Projected Monthly Costs for ${commuteAnalysisData.inputs.routeName}:
- `;
let minCost = Infinity;
let bestOption = "N/A";
validResults.forEach(r => {
const costToCompare = (commuteAnalysisData.inputs.valuePerHour > 0 && r.grandTotalCost !== undefined) ? r.grandTotalCost : r.monthlyCost;
summaryHTML += `
- ${r.mode}: $${r.monthlyCost.toFixed(2)}`; if(commuteAnalysisData.inputs.valuePerHour > 0 && r.grandTotalCost !== undefined) { summaryHTML += ` (Monetary) / $${r.grandTotalCost.toFixed(2)} (Incl. Time Cost)`; } summaryHTML += ` `; if (costToCompare < minCost) { minCost = costToCompare; bestOption = r.mode; } }); summaryHTML += `
Based on your inputs, ${bestOption} appears to be the most economical option with an estimated cost of $${minCost.toFixed(2)}/month ${commuteAnalysisData.inputs.valuePerHour > 0 ? '(including time value)' : '(monetary only)'}.
`; } else { summaryHTML += `Could not determine the most economical option based on current inputs.
`; } if(validResults.length > 1) { const costs = validResults.map(r => (commuteAnalysisData.inputs.valuePerHour > 0 && r.grandTotalCost !== undefined) ? r.grandTotalCost : r.monthlyCost); const maxCost = Math.max(...costs.filter(c => isFinite(c))); // Filter out Infinity if any error mode has it if (isFinite(minCost) && isFinite(maxCost) && minCost < maxCost) { summaryHTML += `Potential monthly savings by choosing the cheapest option: $${(maxCost - minCost).toFixed(2)}.
`; } } container.innerHTML = summaryHTML; } function downloadCommutePdf() { if (!commuteAnalysisData.results || commuteAnalysisData.results.filter(r => !r.error).length === 0) { alert("Please calculate a valid analysis first."); return; } if (typeof window.jspdf === 'undefined' || typeof window.jspdf.jsPDF === 'undefined') { alert('PDF generation library (jsPDF) is not loaded.'); return; } const jsPDFConstructor = window.jspdf.jsPDF; const doc = new jsPDFConstructor(); if (typeof doc.autoTable !== 'function') { alert('jsPDF AutoTable plugin not loaded.'); return; } const data = commuteAnalysisData; const inputs = data.inputs; const validResults = data.results.filter(r => !r.error); const primaryColor = '#007bff', textColor = '#212529', tableHeaderColor = '#e9ecef'; let yPos = 22; doc.setFontSize(18); doc.setTextColor(primaryColor); doc.text("Carpooling vs. Public Transport Analysis", 14, yPos); yPos += 8; doc.setFontSize(10); doc.setTextColor(textColor); doc.text(`Report Date: ${new Date().toLocaleDateString()}`, 14, yPos); yPos += 7; doc.text(`Route: ${inputs.routeName}`, 14, yPos); yPos += 5; doc.text(`One-Way Distance: ${inputs.oneWayDistance} ${inputs.distUnit}, Round Trips/Week: ${inputs.roundTripsPerWeek}`, 14, yPos); yPos += 5; doc.text(`Value of Time: $${inputs.valuePerHour.toFixed(2)}/hour`, 14, yPos); yPos += 10; // Carpool Inputs if(inputs.carpool.role && inputs.carpool.role !== "NotCarpooling") { doc.setFontSize(12); doc.setTextColor(primaryColor); doc.text("Carpooling Details", 14, yPos); yPos += 6; let carpoolBody = [['Role:', inputs.carpool.role]]; if(inputs.carpool.role === "Driver") { const fuelEffDisplayUnit = (inputs.distUnit === 'miles' && inputs.fuelVolUnit === 'gallon' ? 'MPG' : (inputs.distUnit === 'km' && inputs.fuelVolUnit === 'liter' ? 'L/100km' : `${inputs.distUnit}/${inputs.fuelVolUnit}` )); carpoolBody.push(['Vehicle Fuel Efficiency:', `${inputs.carpool.fuelEfficiency} ${fuelEffDisplayUnit}`]); carpoolBody.push(['Fuel Cost:', `$${inputs.carpool.fuelCostPerUnit.toFixed(2)} per ${inputs.fuelVolUnit}`]); carpoolBody.push(['Maintenance Cost:', `$${inputs.carpool.maintenancePerDist.toFixed(2)} per ${inputs.distUnit}`]); carpoolBody.push(['Monthly Parking Cost:', `$${inputs.carpool.parkingMonthly.toFixed(2)}`]); carpoolBody.push(['Tolls per Round Trip:', `$${inputs.carpool.tollsPerRound.toFixed(2)}`]); carpoolBody.push(['Carpool Size (incl. driver):', `${inputs.carpool.carpoolSize}`]); carpoolBody.push(['Contribution per Passenger/Month:', `$${inputs.carpool.contribPerPassengerMonthly.toFixed(2)}`]); } else { carpoolBody.push(['Monthly Cost Paid to Driver:', `$${inputs.carpool.costToDriverMonthly.toFixed(2)}`]); } doc.autoTable({startY: yPos, body: carpoolBody, theme:'plain', styles:{fontSize:9}, columnStyles:{0:{fontStyle:'bold'}}}); yPos = doc.lastAutoTable.finalY + 7; } if (yPos > 260 && inputs.publicTransport.usage === 'Yes') { doc.addPage(); yPos = 20; } if(inputs.publicTransport.usage === 'Yes') { doc.setFontSize(12); doc.setTextColor(primaryColor); doc.text("Public Transport Details", 14, yPos); yPos += 6; let ptBody = [ ['Cost per One-Way Trip:', `$${inputs.publicTransport.ptCostPerTrip.toFixed(2)}`], ['Pass Cost:', `$${inputs.publicTransport.ptPassCost.toFixed(2)}`], ['Pass Type:', inputs.publicTransport.ptPassType] ]; doc.autoTable({startY: yPos, body: ptBody, theme:'plain', styles:{fontSize:9}, columnStyles:{0:{fontStyle:'bold'}}}); yPos = doc.lastAutoTable.finalY + 10; } if (yPos > 260) { doc.addPage(); yPos = 20; } doc.setFontSize(12); doc.setTextColor(primaryColor); doc.text("Time & Qualitative Factors", 14, yPos); yPos +=6; let timeQualBody = [ ['Avg. One-Way Carpool Time:', `${inputs.timeQualitative.timeCarpoolMinutes} minutes`], ['Avg. One-Way Public Transport Time:', `${inputs.timeQualitative.timePublicTransportMinutes} minutes`], ['--- Carpooling Ratings (1-10) ---',''], ['Comfort:', `${inputs.timeQualitative.carpoolComfort}`], ['Convenience:', `${inputs.timeQualitative.carpoolConvenience}`], ['Reliability:', `${inputs.timeQualitative.carpoolReliability}`], ['--- Public Transport Ratings (1-10) ---',''], ['Comfort:', `${inputs.timeQualitative.ptComfort}`], ['Convenience:', `${inputs.timeQualitative.ptConvenience}`], ['Reliability:', `${inputs.timeQualitative.ptReliability}`], ]; if(inputs.timeQualitative.additionalNotes) { timeQualBody.push(['Additional Notes:', {content: inputs.timeQualitative.additionalNotes, styles: {cellWidth: 'wrap'}}]); } doc.autoTable({startY: yPos, body: timeQualBody, theme:'plain', styles:{fontSize:9}, columnStyles:{0:{fontStyle:'bold'}, 1:{cellWidth:100}} }); yPos = doc.lastAutoTable.finalY + 10; if (yPos > 240) { doc.addPage(); yPos = 20; } doc.setFontSize(12); doc.setTextColor(primaryColor); doc.text("Projected Cost Analysis", 14, yPos); yPos += 6; const head = [['Mode', 'Monthly Cost ($)', 'Cost/Trip ($)', `Cost/${inputs.distUnit} ($)`]]; if (inputs.valuePerHour > 0) { head[0].push('Monthly Time (hrs)', 'Monetized Time ($)', 'Grand Total ($)'); } const body = validResults.map(r => { const row = [r.mode, r.monthlyCost.toFixed(2), r.costPerTrip.toFixed(2), r.costPerDistance.toFixed(3)]; if (inputs.valuePerHour > 0) { row.push((r.monthlyTimeHours || 0).toFixed(1), (r.monetizedTimeCost || 0).toFixed(2), (r.grandTotalCost || 0).toFixed(2)); } return row; }); let minCostVal = Infinity; let minGrandTotalVal = Infinity; validResults.forEach(r => { if(r.monthlyCost < minCostVal) minCostVal = r.monthlyCost; if(r.grandTotalCost !== undefined && r.grandTotalCost < minGrandTotalVal) minGrandTotalVal = r.grandTotalCost; }); doc.autoTable({ startY: yPos, head: head, body: body, theme: 'grid', headStyles: { fillColor: tableHeaderColor, textColor: textColor, fontStyle: 'bold', fontSize: 8 }, styles: { fontSize: 7.5, cellPadding: 1.5 }, didDrawCell: function(dataHook) { if (dataHook.section === 'body') { const rowData = validResults[dataHook.row.index]; if(rowData) { let isMin = false; if (inputs.valuePerHour > 0 && rowData.grandTotalCost !== undefined) { isMin = Math.abs(rowData.grandTotalCost - minGrandTotalVal) < 0.01; } else { isMin = Math.abs(rowData.monthlyCost - minCostVal) < 0.01; } if (isMin) { doc.setFillColor(212, 237, 218); // Light green doc.setTextColor(21, 87, 36); // Dark green text doc.setFont(undefined, 'bold'); } } } }, willDrawCell: function(dataHook) { // Reset for next cell doc.setTextColor(textColor); doc.setFont(undefined, 'normal'); } }); yPos = doc.lastAutoTable.finalY + 10; doc.setFontSize(10); doc.setTextColor(textColor); const summaryContainer = document.createElement('div'); summaryContainer.innerHTML = document.getElementById('commuteOverallSummaryContainer').innerHTML; const summaryLines = Array.from(summaryContainer.querySelectorAll('p, li')).map(el => el.innerText); doc.text("Summary:", 14, yPos); yPos += 6; summaryLines.forEach(line => { if (yPos > 275) { doc.addPage(); yPos = 20;} // Replace multiple spaces with one for better PDF formatting, handle bold tags if any const cleanedLine = line.replace(/\s\s+/g, ' ').replace(/\*\*(.*?)\*\*/g, '$1'); doc.text(cleanedLine, 14, yPos, {maxWidth: 180}); yPos += (line.toLowerCase().includes("potential monthly savings") ? 7 : 5); }); doc.save("Commute_Cost_Comparison.pdf"); } // Initial calls updateCommuteNavButtons(); updateUnitLabels(); toggleCarpoolFields(); togglePublicTransportFields();