Travel Expense Report Generator

Trip Information

USD ($)

All expenses should be entered in this currency.

Log New Expense

Logged Expenses

DateCategoryDescriptionAmount ($)Receipt Ref.Action

Travel Expense Report

Expenses by Category:

CategoryTotal Amount ($)

Detailed Expenses:

DateCategoryDescriptionAmount ($)Receipt Ref.

Grand Total Expenses: $0.00

Traveler Name: ${tripInfo.travelerName}

Trip Purpose/Title: ${tripInfo.tripPurpose}

Destination(s): ${tripInfo.destination}

Travel Dates: ${tripInfo.startDate} to ${tripInfo.endDate}

Department/Project Code: ${tripInfo.projectCode}

Report Currency: USD ($)

`; const expensesByCategory = expenses.reduce((acc, exp) => { acc[exp.category] = (acc[exp.category] || 0) + exp.amount; return acc; }, {}); summaryByCategoryTableBody.innerHTML = ''; if (Object.keys(expensesByCategory).length === 0) { const row = summaryByCategoryTableBody.insertRow(); const cell = row.insertCell(); cell.colSpan = 2; cell.textContent = 'No expenses to categorize.'; cell.style.textAlign = 'center'; } else { for (const category in expensesByCategory) { const row = summaryByCategoryTableBody.insertRow(); row.insertCell().textContent = category; row.insertCell().textContent = expensesByCategory[category].toFixed(2); row.cells[1].style.textAlign = 'right'; } } summaryExpensesTableBody.innerHTML = ''; const sortedExpenses = [...expenses].sort((a, b) => new Date(a.date) - new Date(b.date)); if (sortedExpenses.length === 0) { const row = summaryExpensesTableBody.insertRow(); const cell = row.insertCell(); cell.colSpan = 5; cell.textContent = 'No expenses logged for this trip.'; cell.style.textAlign = 'center'; } else { sortedExpenses.forEach(exp => { const row = summaryExpensesTableBody.insertRow(); row.insertCell().textContent = exp.date; row.insertCell().textContent = exp.category; row.insertCell().textContent = exp.description; row.cells[2].style.whiteSpace = 'normal'; row.insertCell().textContent = exp.amount.toFixed(2); row.cells[3].style.textAlign = 'right'; row.insertCell().textContent = exp.receiptRef; }); } const grandTotal = expenses.reduce((sum, exp) => sum + exp.amount, 0); grandTotalAmountSpan.textContent = grandTotal.toFixed(2); } downloadPdfBtn.addEventListener('click', function() { collectTripInfo(); // Ensure tripInfo is up-to-date const { jsPDF } = window.jspdf; const doc = new jsPDF(); const primaryColor = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim(); const accentColor = getComputedStyle(document.documentElement).getPropertyValue('--accent-color').trim(); const categoryColor = getComputedStyle(document.documentElement).getPropertyValue('--expense-category-color').trim(); const whiteText = getComputedStyle(document.documentElement).getPropertyValue('--white-text').trim(); const darkText = getComputedStyle(document.documentElement).getPropertyValue('--dark-text').trim(); let YPosition = 15; const pageHeight = doc.internal.pageSize.height; const leftMargin = 14; const rightMargin = doc.internal.pageSize.width - 14; function checkAndAddPage(currentY) { if (currentY > pageHeight - 30) { // 30mm margin from bottom doc.addPage(); return 20; // Reset Y for new page } return currentY; } // Report Header doc.setFontSize(18); doc.setTextColor(primaryColor); doc.text("Travel Expense Report", doc.internal.pageSize.getWidth() / 2, YPosition, { align: 'center' }); YPosition += 10; // Trip Information Section doc.setFontSize(12); doc.setTextColor(darkText); YPosition = checkAndAddPage(YPosition); doc.setFont(undefined, 'bold'); doc.text("Trip Information", leftMargin, YPosition); YPosition += 6; doc.setFont(undefined, 'normal'); const tripDetails = [ ["Traveler Name:", tripInfo.travelerName], ["Trip Purpose/Title:", tripInfo.tripPurpose], ["Destination(s):", tripInfo.destination], ["Travel Dates:", `${tripInfo.startDate} to ${tripInfo.endDate}`], ["Project Code:", tripInfo.projectCode || "N/A"], ["Report Currency:", "USD ($)"] ]; doc.autoTable({ startY: YPosition, body: tripDetails, theme: 'plain', styles: { fontSize: 10, cellPadding: 1.5, textColor: darkText }, columnStyles: { 0: { fontStyle: 'bold', cellWidth: 50 } }, tableWidth: 'wrap' }); YPosition = doc.lastAutoTable.finalY + 8; // Detailed Expenses Section YPosition = checkAndAddPage(YPosition); doc.setFontSize(12); doc.setFont(undefined, 'bold'); doc.setTextColor(accentColor); doc.text("Detailed Expenses", leftMargin, YPosition); YPosition += 6; const expenseHeaders = [['Date', 'Category', 'Description', 'Amount ($)', 'Receipt Ref.']]; const sortedExpenses = [...expenses].sort((a, b) => new Date(a.date) - new Date(b.date)); const expenseBody = sortedExpenses.map(exp => [exp.date, exp.category, exp.description, exp.amount.toFixed(2), exp.receiptRef]); if (expenseBody.length > 0) { doc.autoTable({ startY: YPosition, head: expenseHeaders, body: expenseBody, theme: 'grid', headStyles: { fillColor: accentColor, textColor: whiteText, fontSize: 9 }, styles: { font: 'Arial', fontSize: 8, cellPadding: 1.5, textColor: darkText, overflow: 'linebreak' }, columnStyles: { 0: {cellWidth: 20}, // Date 1: {cellWidth: 35}, // Category 2: {cellWidth: 'auto'}, // Description (auto to fill space) 3: {cellWidth: 20, halign: 'right'}, // Amount 4: {cellWidth: 25} // Receipt Ref } }); YPosition = doc.lastAutoTable.finalY + 8; } else { doc.setFontSize(10); doc.setFont(undefined, 'normal'); doc.text("No expenses logged for this trip.", leftMargin, YPosition); YPosition += 8; } // Expenses by Category Section YPosition = checkAndAddPage(YPosition); doc.setFontSize(12); doc.setFont(undefined, 'bold'); doc.setTextColor(categoryColor); doc.text("Summary by Category", leftMargin, YPosition); YPosition += 6; const expensesByCategory = expenses.reduce((acc, exp) => { acc[exp.category] = (acc[exp.category] || 0) + exp.amount; return acc; }, {}); const categoryHeaders = [['Category', 'Total Amount ($)']]; const categoryBody = Object.entries(expensesByCategory).map(([cat, total]) => [cat, total.toFixed(2)]); if (categoryBody.length > 0) { doc.autoTable({ startY: YPosition, head: categoryHeaders, body: categoryBody, theme: 'striped', headStyles: { fillColor: categoryColor, textColor: whiteText, fontSize: 9 }, styles: { font: 'Arial', fontSize: 8, cellPadding: 1.5, textColor: darkText }, columnStyles: { 1: { halign: 'right' } } }); YPosition = doc.lastAutoTable.finalY + 8; } else { doc.setFontSize(10); doc.setFont(undefined, 'normal'); doc.text("No expenses to categorize.", leftMargin, YPosition); YPosition += 8; } // Grand Total YPosition = checkAndAddPage(YPosition); const grandTotal = expenses.reduce((sum, exp) => sum + exp.amount, 0); doc.setFontSize(12); doc.setFont(undefined, 'bold'); doc.setTextColor(darkText); doc.text("Grand Total Expenses:", leftMargin, YPosition); doc.text(`$${grandTotal.toFixed(2)}`, rightMargin, YPosition, { align: 'right' }); YPosition += 10; // Signature Lines YPosition = checkAndAddPage(YPosition); if (YPosition > pageHeight - 40) YPosition = checkAndAddPage(YPosition); // Ensure space for signatures doc.setFontSize(10); doc.setFont(undefined, 'normal'); const signatureY = pageHeight - 30; // Position signatures near bottom doc.text("____________________________", leftMargin, signatureY); doc.text("Traveler Signature", leftMargin, signatureY + 5); doc.text("Date: ____________", leftMargin, signatureY + 10); doc.text("____________________________", rightMargin - 60, signatureY); // Adjust X for right alignment doc.text("Approver Signature", rightMargin - 60, signatureY + 5); doc.text("Date: ____________", rightMargin - 60, signatureY + 10); doc.setFontSize(8); doc.setTextColor(100); const generatedDateText = `PDF Generated on: ${formatDateForDisplay(REF_DATE)} ${REF_DATE.toLocaleTimeString()}`; doc.text(generatedDateText, leftMargin, pageHeight - 10); doc.save(`Travel_Expense_Report_${tripInfo.travelerName.replace(/\s+/g, '_') || 'User'}_${tripInfo.startDate || 'NoDate'}.pdf`); }); updateTabDisplay(); renderExpensesTable(); // Set default for expense date based on trip start date initially if (startDateInput.value) { expenseDateInput.value = startDateInput.value; expenseDateInput.min = startDateInput.value; } if (endDateInput.value) { expenseDateInput.max = endDateInput.value; } });
Scroll to Top