/web-app-features

How to Add Expense Tracking to Your Web App

Learn how to easily add expense tracking to your web app with this step-by-step guide. Manage finances smarter and faster!

Book a free  consultation
4.9
Clutch rating 🌟
600+
Happy partners
17+
Countries served
190+
Team members
Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

How to Add Expense Tracking to Your Web App

How to Add Expense Tracking to Your Web App

 

Why Expense Tracking Matters

 

Expense tracking isn't just another feature checkbox—it's often the financial backbone of business applications. Whether you're building software for freelancers managing client expenses, teams tracking project costs, or businesses monitoring departmental spending, a well-implemented expense system can transform financial visibility and decision-making.

 

Core Components of an Effective Expense Tracking System

 

1. Data Structure: The Foundation

 

Before writing a single line of code, you need a solid data model. A typical expense tracking system requires:

 

  • Expenses table - The central entity containing amount, date, description, and category
  • Categories table - For organizing expenses (travel, office supplies, software, etc.)
  • Users/Accounts table - Who incurred the expense
  • Projects/Clients table (optional) - For associating expenses with specific projects
  • Receipts table - For storing receipt images/files

 

Here's a simplified example of what your database schema might look like:

 

CREATE TABLE categories (
  id INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(100) NOT NULL,
  description VARCHAR(255),
  color VARCHAR(7) // For UI representation, e.g., #FF5733
);

CREATE TABLE expenses (
  id INT PRIMARY KEY AUTO_INCREMENT,
  user_id INT NOT NULL,
  amount DECIMAL(10,2) NOT NULL,
  description VARCHAR(255),
  category_id INT,
  date_incurred DATE NOT NULL,
  date_submitted DATE,
  status ENUM('draft', 'submitted', 'approved', 'rejected', 'reimbursed') DEFAULT 'draft',
  project_id INT,
  payment_method VARCHAR(100),
  is_billable BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  FOREIGN KEY (category_id) REFERENCES categories(id),
  FOREIGN KEY (user_id) REFERENCES users(id),
  FOREIGN KEY (project_id) REFERENCES projects(id)
);

CREATE TABLE receipts (
  id INT PRIMARY KEY AUTO_INCREMENT,
  expense_id INT NOT NULL,
  file_path VARCHAR(255) NOT NULL,
  file_type VARCHAR(50),
  upload_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (expense_id) REFERENCES expenses(id) ON DELETE CASCADE
);

 

2. Backend Implementation

 

Your backend needs to handle several critical operations:

 

  • CRUD operations - Create, read, update, and delete expenses
  • Authentication & authorization - Ensuring users can only access their own expenses or those they're authorized to view
  • File handling - For receipt uploads and storage
  • Data validation - Ensuring expense data is valid (positive amounts, reasonable dates, required fields)
  • Business logic - Implementing approval workflows, spending limits, etc.

 

Here's a simplified Express.js controller example:

 

// expenses.controller.js
const ExpenseService = require('../services/expense.service');
const ReceiptService = require('../services/receipt.service');

class ExpenseController {
  async createExpense(req, res) {
    try {
      // Extract user ID from authenticated request
      const userId = req.user.id;
      
      // Validate and create the expense
      const expenseData = {
        user_id: userId,
        amount: req.body.amount,
        description: req.body.description,
        category_id: req.body.category_id,
        date_incurred: req.body.date_incurred,
        project_id: req.body.project_id,
        payment_method: req.body.payment_method,
        is_billable: req.body.is_billable || false
      };
      
      // Basic validation
      if (!expenseData.amount || expenseData.amount <= 0) {
        return res.status(400).json({ error: 'Amount must be greater than zero' });
      }
      
      const expense = await ExpenseService.create(expenseData);
      
      // Handle receipt upload if present
      if (req.files && req.files.receipt) {
        await ReceiptService.uploadReceipt(expense.id, req.files.receipt);
      }
      
      return res.status(201).json({ expense });
    } catch (error) {
      console.error('Error creating expense:', error);
      return res.status(500).json({ error: 'Failed to create expense' });
    }
  }
  
  async getExpenses(req, res) {
    try {
      const userId = req.user.id;
      const filters = {
        user_id: userId,
        // Optional filters from query params
        category_id: req.query.category_id,
        start_date: req.query.start_date,
        end_date: req.query.end_date,
        project_id: req.query.project_id,
        status: req.query.status
      };
      
      const expenses = await ExpenseService.findAll(filters);
      return res.json({ expenses });
    } catch (error) {
      console.error('Error fetching expenses:', error);
      return res.status(500).json({ error: 'Failed to fetch expenses' });
    }
  }
  
  // Other methods: getExpenseById, updateExpense, deleteExpense, etc.
}

module.exports = new ExpenseController();

 

3. Frontend Components

 

The UI for expense tracking typically includes:

 

  • Expense entry form - With fields for amount, date, category, description, and receipt upload
  • Expense list/table - Sortable and filterable view of expenses
  • Dashboard - Summary of spending by category, time period, etc.
  • Reports - Visualizations and exportable data

 

Here's a React component example for an expense form:

 

// ExpenseForm.jsx
import React, { useState, useEffect } from 'react';
import { useCategories } from '../hooks/useCategories';
import DatePicker from 'react-datepicker';
import CurrencyInput from 'react-currency-input-field';

const ExpenseForm = ({ onSubmit, initialData = {} }) => {
  const { categories, loading: categoriesLoading } = useCategories();
  const [formData, setFormData] = useState({
    amount: initialData.amount || '',
    description: initialData.description || '',
    category_id: initialData.category_id || '',
    date_incurred: initialData.date_incurred ? new Date(initialData.date_incurred) : new Date(),
    project_id: initialData.project_id || '',
    payment_method: initialData.payment_method || '',
    is_billable: initialData.is_billable || false
  });
  const [receipt, setReceipt] = useState(null);
  const [errors, setErrors] = useState({});
  
  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    setFormData({
      ...formData,
      [name]: type === 'checkbox' ? checked : value
    });
  };
  
  const handleFileChange = (e) => {
    if (e.target.files && e.target.files[0]) {
      setReceipt(e.target.files[0]);
    }
  };
  
  const validateForm = () => {
    const newErrors = {};
    if (!formData.amount || parseFloat(formData.amount) <= 0) {
      newErrors.amount = 'Amount must be greater than zero';
    }
    if (!formData.description) {
      newErrors.description = 'Description is required';
    }
    if (!formData.category_id) {
      newErrors.category_id = 'Category is required';
    }
    
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    if (!validateForm()) {
      return;
    }
    
    // Create FormData for multipart form submission (for receipt file)
    const submitData = new FormData();
    
    // Add all form fields
    Object.keys(formData).forEach(key => {
      submitData.append(key, formData[key]);
    });
    
    // Add receipt if present
    if (receipt) {
      submitData.append('receipt', receipt);
    }
    
    await onSubmit(submitData);
  };
  
  return (
    <form onSubmit={handleSubmit} className="expense-form">
      <div className="form-group">
        <label htmlFor="amount">Amount*</label>
        <CurrencyInput
          id="amount"
          name="amount"
          placeholder="0.00"
          value={formData.amount}
          decimalsLimit={2}
          onValueChange={(value) => setFormData({...formData, amount: value})}
          className={errors.amount ? 'is-invalid' : ''}
        />
        {errors.amount && <div className="error-message">{errors.amount}</div>}
      </div>
      
      <div className="form-group">
        <label htmlFor="description">Description*</label>
        <input
          type="text"
          id="description"
          name="description"
          value={formData.description}
          onChange={handleChange}
          className={errors.description ? 'is-invalid' : ''}
        />
        {errors.description && <div className="error-message">{errors.description}</div>}
      </div>
      
      <div className="form-group">
        <label htmlFor="category_id">Category*</label>
        <select
          id="category_id"
          name="category_id"
          value={formData.category_id}
          onChange={handleChange}
          className={errors.category_id ? 'is-invalid' : ''}
        >
          <option value="">Select a category</option>
          {!categoriesLoading && categories.map(category => (
            <option key={category.id} value={category.id}>{category.name}</option>
          ))}
        </select>
        {errors.category_id && <div className="error-message">{errors.category_id}</div>}
      </div>
      
      <div className="form-group">
        <label htmlFor="date_incurred">Date</label>
        <DatePicker
          id="date_incurred"
          selected={formData.date_incurred}
          onChange={(date) => setFormData({...formData, date_incurred: date})}
          className="form-control"
        />
      </div>
      
      <div className="form-group">
        <label htmlFor="receipt">Receipt</label>
        <input
          type="file"
          id="receipt"
          name="receipt"
          onChange={handleFileChange}
          accept="image/*, application/pdf"
        />
      </div>
      
      <div className="form-group">
        <label htmlFor="payment_method">Payment Method</label>
        <select
          id="payment_method"
          name="payment_method"
          value={formData.payment_method}
          onChange={handleChange}
        >
          <option value="">Select payment method</option>
          <option value="credit_card">Credit Card</option>
          <option value="cash">Cash</option>
          <option value="bank_transfer">Bank Transfer</option>
          <option value="other">Other</option>
        </select>
      </div>
      
      <div className="form-group checkbox-group">
        <input
          type="checkbox"
          id="is_billable"
          name="is_billable"
          checked={formData.is_billable}
          onChange={handleChange}
        />
        <label htmlFor="is_billable">Billable to client</label>
      </div>
      
      <button type="submit" className="btn btn-primary">
        {initialData.id ? 'Update Expense' : 'Add Expense'}
      </button>
    </form>
  );
};

export default ExpenseForm;

 

Advanced Features to Consider

 

1. OCR for Receipt Scanning

 

Implementing Optical Character Recognition (OCR) can dramatically improve the user experience by automatically extracting expense information from receipts.

 

// receipt-processor.service.js
const { createWorker } = require('tesseract.js');
const ExpenseService = require('./expense.service');

class ReceiptProcessorService {
  async extractDataFromReceipt(receiptFilePath) {
    const worker = await createWorker();
    
    // Initialize worker with English language
    await worker.loadLanguage('eng');
    await worker.initialize('eng');
    
    // Process the image
    const { data } = await worker.recognize(receiptFilePath);
    await worker.terminate();
    
    // Extract relevant information using regex patterns
    const extractedData = {
      amount: this.extractAmount(data.text),
      date: this.extractDate(data.text),
      vendor: this.extractVendor(data.text),
      // Other extracted fields
    };
    
    return extractedData;
  }
  
  extractAmount(text) {
    // Example regex for matching common amount formats
    const amountRegex = /\$?\s*(\d{1,3}(?:,\d{3})*(?:\.\d{2})?)/g;
    const matches = text.match(amountRegex);
    
    if (matches && matches.length > 0) {
      // Clean up and return the likely total amount
      // This is simplified - real implementations would be more sophisticated
      return matches[matches.length - 1].replace(/[^\d.]/g, '');
    }
    
    return null;
  }
  
  // Similar methods for extracting date, vendor, etc.
  
  async processReceipt(receiptId) {
    try {
      const receipt = await ReceiptService.findById(receiptId);
      const extractedData = await this.extractDataFromReceipt(receipt.file_path);
      
      // Update the expense with extracted data
      if (extractedData) {
        await ExpenseService.update(receipt.expense_id, {
          amount: extractedData.amount || undefined,
          date_incurred: extractedData.date || undefined,
          description: extractedData.vendor || undefined
        });
      }
      
      return extractedData;
    } catch (error) {
      console.error('Error processing receipt:', error);
      throw error;
    }
  }
}

module.exports = new ReceiptProcessorService();

 

2. Approval Workflows

 

For team or enterprise settings, you'll need approval workflows:

 

// approval.service.js
class ApprovalService {
  async submitForApproval(expenseId, userId) {
    // Update expense status to 'submitted'
    await ExpenseService.update(expenseId, {
      status: 'submitted',
      date_submitted: new Date()
    });
    
    // Find appropriate approvers based on company policies
    const approvers = await this.findApprovers(userId);
    
    // Create notification/task for approvers
    await NotificationService.notifyApprovers(approvers, expenseId);
    
    return { success: true };
  }
  
  async approveExpense(expenseId, approverId, comments = '') {
    // Verify approver has permission
    const hasPermission = await this.verifyApproverPermission(expenseId, approverId);
    
    if (!hasPermission) {
      throw new Error('Unauthorized approval attempt');
    }
    
    // Update expense status
    await ExpenseService.update(expenseId, {
      status: 'approved',
      approver_id: approverId,
      approval_date: new Date(),
      approval_comments: comments
    });
    
    // Create audit log entry
    await AuditService.logAction({
      action: 'expense_approved',
      user_id: approverId,
      resource_id: expenseId,
      details: { comments }
    });
    
    // Notify expense owner
    const expense = await ExpenseService.findById(expenseId);
    await NotificationService.notifyUser(
      expense.user_id,
      'Expense Approved',
      `Your expense of $${expense.amount} has been approved.`
    );
    
    return { success: true };
  }
  
  // Similar methods for rejection, requesting changes, etc.
}

module.exports = new ApprovalService();

 

3. Reporting and Analytics

 

Robust reporting capabilities transform raw expense data into actionable insights:

 

// reporting.service.js
class ReportingService {
  async generateExpenseSummary(filters = {}) {
    const { 
      user_id, 
      start_date, 
      end_date, 
      group_by = 'category' // Possible values: category, month, project
    } = filters;
    
    let query = 'SELECT ';
    
    if (group_by === 'category') {
      query += `
        c.name as category,
        c.color as color,
        SUM(e.amount) as total,
        COUNT(e.id) as count
      FROM expenses e
      JOIN categories c ON e.category_id = c.id
      WHERE e.status != 'rejected'`;
    } else if (group_by === 'month') {
      query += `
        DATE_FORMAT(e.date_incurred, '%Y-%m') as month,
        SUM(e.amount) as total,
        COUNT(e.id) as count
      FROM expenses e
      WHERE e.status != 'rejected'`;
    } else if (group_by === 'project') {
      query += `
        p.name as project,
        SUM(e.amount) as total,
        COUNT(e.id) as count
      FROM expenses e
      JOIN projects p ON e.project_id = p.id
      WHERE e.status != 'rejected'`;
    }
    
    // Add common filters
    const params = [];
    if (user_id) {
      query += ' AND e.user_id = ?';
      params.push(user_id);
    }
    
    if (start_date) {
      query += ' AND e.date_incurred >= ?';
      params.push(start_date);
    }
    
    if (end_date) {
      query += ' AND e.date_incurred <= ?';
      params.push(end_date);
    }
    
    // Group by the selected dimension
    if (group_by === 'category') {
      query += ' GROUP BY e.category_id';
    } else if (group_by === 'month') {
      query += ' GROUP BY DATE_FORMAT(e.date_incurred, "%Y-%m")';
    } else if (group_by === 'project') {
      query += ' GROUP BY e.project_id';
    }
    
    query += ' ORDER BY total DESC';
    
    // Execute the query
    const results = await db.query(query, params);
    
    // Calculate total
    const total = results.reduce((sum, item) => sum + parseFloat(item.total), 0);
    
    return {
      data: results,
      total,
      filters
    };
  }
  
  async exportExpenses(filters = {}, format = 'csv') {
    const expenses = await ExpenseService.findAll(filters, {
      include: ['category', 'user', 'project', 'receipts']
    });
    
    if (format === 'csv') {
      return this.generateCSV(expenses);
    } else if (format === 'pdf') {
      return this.generatePDF(expenses);
    } else {
      throw new Error(`Unsupported export format: ${format}`);
    }
  }
  
  generateCSV(expenses) {
    const headers = [
      'ID', 'Date', 'Amount', 'Description', 'Category', 
      'Project', 'Status', 'User', 'Payment Method'
    ];
    
    const rows = expenses.map(expense => [
      expense.id,
      new Date(expense.date_incurred).toLocaleDateString(),
      expense.amount.toFixed(2),
      expense.description,
      expense.category ? expense.category.name : '',
      expense.project ? expense.project.name : '',
      expense.status,
      expense.user ? `${expense.user.first_name} ${expense.user.last_name}` : '',
      expense.payment_method
    ]);
    
    // Generate CSV content
    let csv = headers.join(',') + '\n';
    rows.forEach(row => {
      csv += row.map(cell => `"${cell}"`).join(',') + '\n';
    });
    
    return csv;
  }
  
  // Methods for PDF generation, visualizations, etc.
}

module.exports = new ReportingService();

 

Implementation Strategy

 

1. Start with Core Functionality

 

Begin with a minimal viable product (MVP) that includes:

 

  • Basic expense entry form
  • Simple listing of expenses
  • Categorization capabilities
  • Receipt attachment functionality

 

2. Iterative Enhancement

 

After your MVP is working, add features in order of business value:

 

  • Phase 1: Reporting and export capabilities
  • Phase 2: OCR for receipt scanning
  • Phase 3: Approval workflows
  • Phase 4: Advanced analytics and integrations

 

3. Performance Considerations

 

As your expense database grows, consider:

 

  • Pagination and lazy loading for expense lists
  • Caching for frequently accessed reports
  • Asynchronous processing for OCR and other intensive tasks
  • Database indexes on frequently queried fields (date, user_id, category_id)

 

Integration Opportunities

 

Accounting Software Integration

 

Connect your expense tracking system with popular accounting platforms to eliminate manual data entry:

 

// quickbooks-integration.service.js
const QuickBooks = require('node-quickbooks');

class QuickBooksIntegrationService {
  constructor(companyId) {
    // Get company's QuickBooks connection details
    this.companySettings = CompanySettingsService.getQuickBooksSettings(companyId);
    
    this.qbo = new QuickBooks(
      this.companySettings.consumerKey,
      this.companySettings.consumerSecret,
      this.companySettings.oauthToken,
      this.companySettings.oauthTokenSecret,
      this.companySettings.realmId,
      this.companySettings.useSandbox
    );
  }
  
  async syncExpense(expenseId) {
    const expense = await ExpenseService.findById(expenseId, {
      include: ['category', 'user', 'project']
    });
    
    // Only sync approved expenses
    if (expense.status !== 'approved') {
      throw new Error('Only approved expenses can be synced');
    }
    
    // Map local category to QuickBooks account
    const accountRef = await this.mapCategoryToAccount(expense.category_id);
    
    // Create expense in QuickBooks
    const purchaseData = {
      AccountRef: accountRef,
      PaymentType: this.mapPaymentMethod(expense.payment_method),
      TotalAmt: expense.amount,
      TxnDate: expense.date_incurred,
      PurchaseEx: {
        AccountBasedExpenseLineDetail: {
          AccountRef: accountRef,
          BillableStatus: expense.is_billable ? 'Billable' : 'NotBillable',
          CustomerRef: expense.project ? await this.mapProjectToCustomer(expense.project_id) : null,
          TaxCodeRef: { value: 'NON' }
        }
      },
      // Other QuickBooks-specific fields
    };
    
    return new Promise((resolve, reject) => {
      this.qbo.createPurchase(purchaseData, (err, purchase) => {
        if (err) {
          reject(err);
          return;
        }
        
        // Update local expense with QuickBooks ID
        ExpenseService.update(expenseId, {
          external_id: purchase.Id,
          external_system: 'quickbooks',
          sync_date: new Date()
        });
        
        resolve(purchase);
      });
    });
  }
  
  // Helper methods for mapping categories, payment methods, etc.
  async mapCategoryToAccount(categoryId) {
    const mapping = await CategoryMappingService.findByCategoryId(categoryId);
    return { value: mapping.external_account_id };
  }
  
  // Other helper methods
}

module.exports = QuickBooksIntegrationService;

 

Common Pitfalls to Avoid

 

1. Overlooking Multi-Currency Support

 

If your application serves international users or businesses, currency handling is crucial:

 

// Basic currency handling middleware
const handleCurrency = (req, res, next) => {
  // Get user's preferred currency from profile or settings
  const userCurrency = req.user.preferred_currency || 'USD';
  
  // Get current exchange rates (cached)
  const exchangeRates = ExchangeRateService.getCurrentRates();
  
  // Attach to request object for controllers to use
  req.currency = {
    code: userCurrency,
    exchangeRates,
    format: (amount, sourceCurrency = 'USD') => {
      if (sourceCurrency === userCurrency) {
        return amount;
      }
      
      // Convert amount to user's currency
      const rate = exchangeRates[userCurrency] / exchangeRates[sourceCurrency];
      return amount * rate;
    }
  };
  
  next();
};

 

2. Neglecting Receipt Storage Security

 

Receipts often contain sensitive information and must be securely stored:

 

// secure-storage.service.js
const AWS = require('aws-sdk');
const crypto = require('crypto');
const fs = require('fs');

class SecureStorageService {
  constructor() {
    this.s3 = new AWS.S3({
      accessKeyId: process.env.AWS_ACCESS_KEY,
      secretAccessKey: process.env.AWS_SECRET_KEY
    });
    
    this.bucketName = process.env.S3_BUCKET_NAME;
    this.encryptionKey = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');
  }
  
  async storeReceipt(receiptFile, expenseId, userId) {
    // Generate a unique file name
    const fileName = `${userId}/${expenseId}/${Date.now()}-${crypto.randomBytes(8).toString('hex')}`;
    const fileExtension = receiptFile.name.split('.').pop();
    const fullFileName = `${fileName}.${fileExtension}`;
    
    // Read file data
    const fileData = fs.readFileSync(receiptFile.path);
    
    // Encrypt the file
    const encryptedData = this.encryptFile(fileData);
    
    // Upload to S3 with encryption
    const params = {
      Bucket: this.bucketName,
      Key: fullFileName,
      Body: encryptedData,
      ContentType: receiptFile.type,
      Metadata: {
        'x-amz-meta-encrypted': 'true'
      },
      // Server-side encryption
      ServerSideEncryption: 'AES256'
    };
    
    const result = await this.s3.upload(params).promise();
    
    // Return the file location and metadata
    return {
      file_path: result.Location,
      file_key: fullFileName,
      file_type: receiptFile.type,
      size: encryptedData.length
    };
  }
  
  encryptFile(fileData) {
    const iv = crypto.randomBytes(16);
    const cipher = crypto.createCipheriv('aes-256-cbc', this.encryptionKey, iv);
    
    const encrypted = Buffer.concat([
      iv,
      cipher.update(fileData),
      cipher.final()
    ]);
    
    return encrypted;
  }
  
  async retrieveReceipt(fileKey) {
    const params = {
      Bucket: this.bucketName,
      Key: fileKey
    };
    
    const { Body } = await this.s3.getObject(params).promise();
    
    // Decrypt the file
    const decrypted = this.decryptFile(Body);
    
    return decrypted;
  }
  
  decryptFile(encryptedData) {
    // Extract the IV (first 16 bytes)
    const iv = encryptedData.slice(0, 16);
    const encryptedContent = encryptedData.slice(16);
    
    const decipher = crypto.createDecipheriv('aes-256-cbc', this.encryptionKey, iv);
    
    return Buffer.concat([
      decipher.update(encryptedContent),
      decipher.final()
    ]);
  }
}

module.exports = new SecureStorageService();

 

3. Poor Tax Handling

 

Tax management is often an afterthought but can be critical for business expense tracking:

 

// Expense form with tax handling
const ExpenseFormWithTax = ({ onSubmit, initialData = {} }) => {
  const [formData, setFormData] = useState({
    // Basic expense fields...
    amount_pretax: initialData.amount_pretax || '',
    tax_amount: initialData.tax_amount || '',
    tax_rate: initialData.tax_rate || '',
    tax_included: initialData.tax_included ?? true
  });
  
  // Calculate total or tax based on user input
  const calculateAmounts = (field, value) => {
    const updates = { [field]: value };
    
    if (field === 'amount_pretax' && formData.tax_rate) {
      // User updated pre-tax amount, calculate tax
      const pretax = parseFloat(value) || 0;
      const taxRate = parseFloat(formData.tax_rate) / 100;
      updates.tax_amount = (pretax * taxRate).toFixed(2);
      
      if (formData.tax_included) {
        // Tax inclusive pricing (total already includes tax)
        updates.amount = pretax;
      } else {
        // Tax exclusive pricing (add tax to get total)
        updates.amount = (pretax + parseFloat(updates.tax_amount)).toFixed(2);
      }
    } else if (field === 'tax_rate') {
      // User updated tax rate, recalculate tax amount
      const pretax = parseFloat(formData.amount_pretax) || 0;
      const taxRate = parseFloat(value) / 100;
      updates.tax_amount = (pretax * taxRate).toFixed(2);
      
      if (formData.tax_included) {
        updates.amount = pretax;
      } else {
        updates.amount = (pretax + parseFloat(updates.tax_amount)).toFixed(2);
      }
    } else if (field === 'tax_amount') {
      // User manually entered tax amount
      const pretax = parseFloat(formData.amount_pretax) || 0;
      const taxAmount = parseFloat(value) || 0;
      
      if (pretax > 0) {
        // Calculate implied tax rate
        updates.tax_rate = ((taxAmount / pretax) * 100).toFixed(2);
      }
      
      if (formData.tax_included) {
        updates.amount = pretax;
      } else {
        updates.amount = (pretax + taxAmount).toFixed(2);
      }
    } else if (field === 'tax_included') {
      // User toggled between tax inclusive/exclusive
      const pretax = parseFloat(formData.amount_pretax) || 0;
      const taxAmount = parseFloat(formData.tax_amount) || 0;
      
      if (value) { // Tax inclusive
        updates.amount = pretax;
      } else { // Tax exclusive
        updates.amount = (pretax + taxAmount).toFixed(2);
      }
    }
    
    return updates;
  };
  
  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    const actualValue = type === 'checkbox' ? checked : value;
    
    // For tax-related fields, calculate dependent values
    if (['amount_pretax', 'tax_rate', 'tax_amount', 'tax_included'].includes(name)) {
      setFormData({
        ...formData,
        ...calculateAmounts(name, actualValue)
      });
    } else {
      // Normal field handling
      setFormData({
        ...formData,
        [name]: actualValue
      });
    }
  };
  
  // Rest of the component...
};

 

Conclusion

 

A well-implemented expense tracking system does more than just record costs—it transforms financial management by providing visibility, automating tedious tasks, and enabling data-driven decisions. As with most business features, the technical implementation is only half the story—understanding the workflow and financial needs of your users is equally important.

 

By building your expense tracking module incrementally and focusing on the user experience, you can deliver a system that becomes an essential part of your application's value proposition rather than just another feature checkbox.

 

Remember that expense management touches sensitive financial data, so prioritize security, data validation, and auditability throughout your implementation. A successful expense tracking feature will reduce administrative burden, improve financial accuracy, and ultimately contribute to better business decisions for your users.

Ship Expense Tracking 10x Faster with RapidDev

Connect with our team to unlock the full potential of code solutions with a no-commitment consultation!

Book a Free Consultation

Top 3 Expense Tracking Usecases

Explore the top 3 practical use cases for adding expense tracking to your web app.

 

Financial Decision Making

 

  • Real-time visibility into spending patterns allows leadership to make informed budgeting decisions without waiting for month-end reports. When engineering managers can see that AWS costs are trending 30% higher mid-month, they can course-correct before overruns become problematic.
  • Expense tracking creates a data-driven foundation for resource allocation, helping you identify which projects or departments generate the highest ROI relative to their spending—transforming finance from a reactive function into a strategic advantage.

 

 

Operational Efficiency

 

  • Automated categorization and approval workflows eliminate hours of manual processing—turning what was once a Friday afternoon paperwork marathon into a background process that happens continuously. This reduces finance team overhead and accelerates reimbursement cycles.
  • By integrating expense tracking with accounting systems, you create a single source of truth that prevents duplicate entries and reconciliation headaches, while ensuring audit-ready financial records are maintained without additional effort.

 

 

Compliance & Governance

 

  • Policy enforcement at the point of submission catches non-compliant expenses before they enter your system, rather than discovering them during quarterly reviews. This creates a guided experience where users learn boundaries in real-time instead of through rejection emails.
  • Granular expense tracking provides detailed audit trails and compliance documentation that satisfy both internal governance requirements and external regulatory obligations—turning potential audit anxiety into confidence backed by comprehensive records.

 


Recognized by the best

Trusted by 600+ businesses globally

From startups to enterprises and everything in between, see for yourself our incredible impact.

RapidDev was an exceptional project management organization and the best development collaborators I've had the pleasure of working with.

They do complex work on extremely fast timelines and effectively manage the testing and pre-launch process to deliver the best possible product. I'm extremely impressed with their execution ability.

Arkady
CPO, Praction
Working with Matt was comparable to having another co-founder on the team, but without the commitment or cost.

He has a strategic mindset and willing to change the scope of the project in real time based on the needs of the client. A true strategic thought partner!

Donald Muir
Co-Founder, Arc
RapidDev are 10/10, excellent communicators - the best I've ever encountered in the tech dev space.

They always go the extra mile, they genuinely care, they respond quickly, they're flexible, adaptable and their enthusiasm is amazing.

Mat Westergreen-Thorne
Co-CEO, Grantify
RapidDev is an excellent developer for custom-code solutions.

We’ve had great success since launching the platform in November 2023. In a few months, we’ve gained over 1,000 new active users. We’ve also secured several dozen bookings on the platform and seen about 70% new user month-over-month growth since the launch.

Emmanuel Brown
Co-Founder, Church Real Estate Marketplace
Matt’s dedication to executing our vision and his commitment to the project deadline were impressive. 

This was such a specific project, and Matt really delivered. We worked with a really fast turnaround, and he always delivered. The site was a perfect prop for us!

Samantha Fekete
Production Manager, Media Production Company
The pSEO strategy executed by RapidDev is clearly driving meaningful results.

Working with RapidDev has delivered measurable, year-over-year growth. Comparing the same period, clicks increased by 129%, impressions grew by 196%, and average position improved by 14.6%. Most importantly, qualified contact form submissions rose 350%, excluding spam.

Appreciation as well to Matt Graham for championing the collaboration!

Michael W. Hammond
Principal Owner, OCD Tech

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We’ll discuss your project and provide a custom quote at no cost.Â