Jan 17, 2019

Adding a budget feature to our expense tracker - Part 3

In part 2 we made some good progress on our new budget feature. Today we’ll finish off the core budget enhancement functionality.

Today’s objective

At this point we just need to add some interactive elements to the UI and I also want to add a site wide widget similar to the current income versus expenses widget. This new widget will provide a visual indicator to the user as to whether they are currently running within their budget.

Getting started

As previously mentionned I’m not sure this is really a “follow along” type of series, but it is possible to follow along if you wish.

If you’ve been following along you can continue with the code from part 2, or you can grab the code from GitHub.

Clone the Repo:

Terminal
git clone -b bfb-adding-content-to-the-budget-page https://github.com/riebeekn/wmcgy2 budget-app
cd budget-app

After grabbing the code we should set our local Ruby version and run bundle install.

Terminal
rbenv local 2.3.6
Terminal
bundle install

Next let’s create a new branch for today’s work.

Terminal
git checkout -b bfb-finishing-the-UI

With that out of the way, let’s get to it!

Where we’re starting from

Let’s fire up the server and have a quick look at our current budget page.

Terminal
rails s

So our page is looking pretty good. All we need to do at this point is add in our various totals. We’ll also want to make the calculation of the totals dynamic so that when a user updates a budget field, the totals are recalculated without needing to refresh the page.

Let’s get at it!

Adding totals to the budget page

The totals are going to be calculated via JavaScript and jQuery. Our generator from part 1 created a budget.js.coffee file, but we’re going to be sticking with plain JavaScript, so the first order of business is to rename the file.

Terminal
mv app/assets/javascripts/budget.js.coffee app/assets/javascripts/budget.js

We also need to make a few changes to the _budget_item.html.erb file. We’ll want to add a couple of classes so that we can easily access the spent and + / - fields with jQuery. I also want to display the spent value without the - sign, so we’ll add an .abs transform to it.

/app/views/budget/_budget_item.html.erb
<tr>
  <td><%= category.name %></td>
  <td>
    <%= best_in_place category, :budgeted,
      activator: "##{category.id}",
      :display_with => :number_to_currency %>
    <a id="<%= "#{category.id}" %>" href="#">Edit</a>
  </td>
  <td class="spent-item"><%= number_to_currency(category.spent.to_f.abs) %></td>
  <td class="plus-minus"></td>
</tr>

The only change above is the classes on the last two td tags and the aforementionned transform of the spent value to an absolute number.

The JavaScript

It’s now time to create our JavaScript. We’ll start off by creating functions for each total column. Let’s start off with the + / - column.

Calculating the plus / minus column

For the + / - column we just need to subtract the spent column from the budgeted column, and do this for all rows. Let’s see what this looks like.

/app/assets/javascripts/budget.js
jQuery(function($) {
  setPlusMinus();
});

function setPlusMinus() {
  $('tr').each(function () {
    var budgeted = $(this).find('.best_in_place').asNumber();
    var spent = $(this).find('.spent-item').asNumber();
    if (budgeted !== null && spent !== null) {
      var plusMinus = budgeted - spent;
      var $field = $(this).find('.plus-minus');
      if (plusMinus < 0) {
        $field.addClass('debit');
      } else {
        $field.removeClass('debit');
      }
      $field.html(plusMinus).formatCurrency();
    }
  });
}

All we’ve done above is call a function we’ve created, setPlusMinus, on page load. The function itself is pretty simple. For each row we just subtract spent from budgeted and place the result in the + / - column. We’re using the jQuery format currency plugin for converting the values on the UI to numbers (via asNumber()) so we can perform the necessary arithmetic on them. We then use the formatCurrency() function to display the result as a currency.

Notice we are also changing the class of the + / - field depending on whether the value is positive or negative. Let’s add the styles we are referencing.

First we need to update application.css.scss.

/app/assets/stylesheets/application.css.scss
@import "compass/utilities";
...
...
@import "bootstrap_and_overrides";
@import "budget";
@import "layout";
...
...

The only change above is the addition of the budget stylesheet.

Now let’s update the budget stylesheet.

/app/assets/stylesheets/budget.css.scss
.budget {
  .debit {
    color: #dc3912;
  }
  .credit {
    color: #333333;
  }
  a {
    padding-left: 10px;
  }
}

The above will display debit values in black, credit values in red. We’re also adding a bit of padding to a links so that the Edit links are spaced a little nicer.

With all the above changes in place, a page refresh (you might also need to clear your browser cache) yields:

Not bad!

Now we just need to do something similar with all the values for the Total row. I won’t go thru an explanation of each function, they are all variations on the general theme of selecting all the appropriate values and adding them together.

/app/assets/javascripts/budget.js
jQuery(function($) {
  setPlusMinus();
  setBudgetedTotal();
  setSpentTotal();
  setPlusMinusTotal();
});

function setPlusMinus() {
  $('tr').each(function () {
    var budgeted = $(this).find('.best_in_place').asNumber();
    var spent = $(this).find('.spent-item').asNumber();
    if (budgeted !== null && spent !== null) {
      var plusMinus = budgeted - spent;
      var $field = $(this).find('.plus-minus');
      if (plusMinus < 0) {
        $field.addClass('debit');
      } else {
        $field.removeClass('debit');
      }
      $field.html(plusMinus).formatCurrency();
    }
  });
}

function setBudgetedTotal() {
  var sum = 0;
  $('.best_in_place').each(function() {
    sum += $(this).asNumber();
  });

  $('#budget-total').html(sum).formatCurrency();
}

function setSpentTotal() {
  var sum = 0;
  $('.spent-item').each(function() {
    sum += $(this).asNumber();
  });

  $('#spent-total').html(sum).formatCurrency();
}

function setPlusMinusTotal() {
  var sum = 0;
  $('.plus-minus').each(function() {
    sum += $(this).asNumber();
  });

  if (sum < 0) {
    $('#plus-minus-total').addClass('debit');
  } else {
    $('#plus-minus-total').removeClass('debit');
  }
  $('#plus-minus-total').html(sum).formatCurrency();
}

A page refresh will show that our page is now looking pretty much complete:

Making the totals dynamic

One problem with the current implementation is that when the user updates a budgeted value, the totals don’t update to reflect the change.

This can be fixed pretty easily, we just need to bind to the best_in_place success event.

Once again we’ll just update our JavaScript file, adding a new function bindBudgetValueUpdateSuccessEvent, and calling into that function on page load.

/app/assets/javascripts/budget.js
jQuery(function($) {
  bindBudgetValueUpdateSuccessEvent();
  setPlusMinus();
  setBudgetedTotal();
  setSpentTotal();
  setPlusMinusTotal();
});

function bindBudgetValueUpdateSuccessEvent() {
  $('.best_in_place').bind("ajax:success", function (){
    $(this).formatCurrency();
    setPlusMinus();
    setBudgetedTotal();
    setPlusMinusTotal();
  });
}
...
...

With the above change the totals now update on a budget change.

So that takes care of the budget page functionality!

Adding a “budget” widget

One additional change I would like to make is to add a widget similar to the current income versus expenses widget.

The first step is to add the new widget file:

Terminal
touch app/views/shared/_budget_status.html.erb

The contents of the file will be:

/app/views/shared/_budget_status.html.erb
<% user ||= current_user %>
<% budget_left = user.monthly_budget_remaining %>
<section class="budget-status">
  <header class="<%= budget_left < 0 ? 'debit' : 'credit'%>">
    <%= budget_left < 0 ? 'OVER BUDGET' : 'ON BUDGET'%>
  </header>
</section>

Pretty simple! We’ve created a new monthly_budget_remaining method (which we’ll need to create) and depending on whether we get a positive or negative number from this method we display either OVER BUDGET or ON BUDGET for our status.

We also need to make a few styling updates. We’ll be adding some specific styles for the budget-status class, and then we also want to pull out the debit and credit styles from the .mtd-ytd style definitions as we are going to be using these for the budget widget so want them to be scoped globally. The changes to layout.css.scss are below:

/app/assets/stylesheets/layout.css.scss …line 44
// shared/budget status
.budget-status {
  float: right;
  @extend .well;
  padding: 10px;
  margin-right: 10px;
  header {
    font-weight: bold;
  }
}

// shared/mtd ytd
.mtd-ytd {
  float: right;
  @extend .well;
  padding: 10px;
  header {
    font-weight: bold;
  }
  label {
    padding-right: 5px;
  }
  label.first {
    float: left;
    display: block;
  }
  label.last {
    display: inline-block;
  }
  span.amount {
    font-weight: bold;
  }
}

.debit {
  color: #dc3912;
}
.credit {
  color: #333333;
}

We’ve added some styling for the budget-status class and pulled the debit and credit classes out of the mtd-ytd scope.

Since these classes are now global we can remove the debit and credit classes from budget.css.

/app/assets/stylesheets/budget.css.scss
.budget {
  a {
    padding-left: 10px;
  }
}

Now we need to create our new method in user.rb.

/app/models/user.rb …line 63
def monthly_budget_remaining
  expenses = transactions.
    select("SUM(amount)").
    where("date_trunc('month', date) = date_trunc('month', now()) AND is_debit = true").
    first.sum
  budgeted = categories.sum(:budgeted)
  budgeted.to_f.abs - expenses.to_f.abs
end

Fairly simple, all we are doing is summing up our expenses for the month and subtracting that value from the sum of the budgeted fields for our categories.

Note the budget status widget is an overall budget status indicator, i.e. it is possible to be over budget in a particular category but not have the status indicate the user is over-budget if overall they are not over budget.

One final change we need to make in order to see our widget in action is to add it to the following pages:

  • app/views/budget/index.html.erb
  • app/views/categories/index.html.erb
  • app/views/reports/index.html.erb
  • app/views/transactions/edit.html.erb
  • app/views/transactions/index.html.erb
  • app/views/transactions/new.html.erb
  • app/views/users/edit.html.erb

Adding the widget is straight-forward, we’ll just place it after the current render 'shared/mtd_ytd' call. For example:

/app/views/budget/index.html.erb
<% provide(:title, 'Budget') %>
<div class="budget">
  <%= render 'shared/mtd_ytd' %>
  <%= render 'shared/budget_status' %>
  ...
  ...

With these changes in place, we have a budget widget!

A few issues

We have a few minor issues with the current implementation of the budget functionality. One is that the budget widget will not update without a page refresh so won’t automatically reflect changes the user makes to their budget values. This is a pretty minor issue and pretty easy to fix but for now we’ll leave it as is.

The other issue is a bit more annoying. We currently show all categories on the budget page. This doesn’t always make sense, for instance when we have a category that is exclusively for income. For example:

So we see the ‘Pay’ category showing up on the budget page which doesn’t really make sense. There are a few ways we could deal with this:

  • Leave it as is, and live with it.
  • Change categories so that they are either expense or income categories. This would be a pretty big change to the way we currently handle things where individual transactions are tagged as being either income or expenses. It would also mean we couldn’t have categories that have both income and expense transactions in them. For instance, I use a ‘Miscellaneous’ category which contains both expenses and income.
  • Add a new field to categories that indicates whether or not it should be included in the budget.

I think the third solution is likely the best approach. Again, for now we’re going to leave things as is… but next time out we’ll implement this third approach.

We have once again been slacking on making sure we have tests in place for our new functionality. So let’s add some more tests before finishing up for the day.

Adding some tests

We’ll add some tests for the new user.rb method we created and also for the new budget widget. Let’s start with the user tests.

Adding tests for the user method

I won’t provide an explanation for the tests below as I think they are pretty self explanatory. We’re just creating some data and then ensuring we get the expected value back from the monthly_budget_remaining method.

/spec/models/user_spec.rb …line 359 to 408
describe "budgeting" do
  before do
    @user = FactoryGirl.create(:user, active: true)
  end

  describe "with no transactions" do
    it "should return 0" do
      @user.monthly_budget_remaining.should eq 0
    end
  end

  describe "when over budget" do
    before do
      @cat_gro = FactoryGirl.create(:category, user: @user, name: 'Groceries', budgeted: 800)
      FactoryGirl.create(:transaction, date: 1.hour.ago,
        description: 'A transaction', amount: 801, is_debit: true, user: @user,
        category: @cat_gro)
    end

    it "should return a negative value" do
      @user.monthly_budget_remaining.should eq -1
    end
  end

  describe "when under budget" do
    before do
      @cat_gro = FactoryGirl.create(:category, user: @user, name: 'Groceries', budgeted: 800)
      FactoryGirl.create(:transaction, date: 1.hour.ago,
        description: 'A transaction', amount: 799, is_debit: true, user: @user,
        category: @cat_gro)
    end

    it "should return a negative value" do
      @user.monthly_budget_remaining.should eq 1
    end
  end

  describe "when exactly on budget" do
    before do
      @cat_gro = FactoryGirl.create(:category, user: @user, name: 'Groceries', budgeted: 800)
      FactoryGirl.create(:transaction, date: 1.hour.ago,
        description: 'A transaction', amount: 800, is_debit: true, user: @user,
        category: @cat_gro)
    end

    it "should return a negative value" do
      @user.monthly_budget_remaining.should eq 0
    end
  end
end

Let’s make sure the user tests are passing:

Terminal
bundle exec rspec spec/models/user_spec.rb

Looks good! Note that these are rather long running tests, we might want to look into improving the performance of these tests down the line. For now we’ll leave them as is however.

Adding tests for the widget

We already have some tests for the existing mtd / ytd widget in the shared_examples_for_widget.rb file. Since we now have two widgets, let’s start by renaming our existing file to be more specific to the mtd / ytd widget.

Terminal
mv spec/support/shared_examples_for_widget.rb spec/support/shared_examples_for_mtd_ytd_widget.rb

Now we can create a new file for the budget widget.

Terminal
touch spec/support/shared_examples_for_budget_status_widget.rb

The tests for the budget widget are very similar to the tests we created for the user method. We’re just creating some data and then ensuring we get the expected value displaying in the budget widget.

/spec/support/shared_examples_for_budget_status_widget.rb
shared_examples_for 'budget status widget' do

  describe "with no transactions" do
    it "should show the correct status" do
      page.should have_content('ON BUDGET')
    end

    it "should have the right class" do
      page.should have_css('header.credit')
    end
  end

  describe "when over budget" do
    before(:all) {
      @cat = FactoryGirl.create(:category, user: user, name: "Rent", budgeted: 800)
      FactoryGirl.create(:transaction, date: 1.hour.ago,
        description: 'A transaction', amount: 801, is_debit: true, user: user,
        category: @cat)
    }
    after(:all) { User.destroy_all }

    it "should show the correct status" do
      page.should have_content('OVER BUDGET')
    end

    it "should have the right class" do
      page.should have_css('header.debit')
    end
  end

  describe "when under budget" do
    before(:all) {
      @cat = FactoryGirl.create(:category, user: user, name: "Rent", budgeted: 800)
      FactoryGirl.create(:transaction, date: 1.hour.ago,
        description: 'A transaction', amount: 799, is_debit: true, user: user,
        category: @cat)
    }
    after(:all) { User.destroy_all }

    it "should show the correct status" do
      page.should have_content('ON BUDGET')
    end

    it "should have the right class" do
      page.should have_css('header.credit')
    end
  end
end

All pretty much self explanatory!

Now we just need to add the shared budget widget tests to the following files:

  • spec/requests/budget_spec.rb
  • spec/requests/categories_spec.rb
  • spec/requests/reports_spec.rb
  • spec/requests/transactions_spec.rb
  • spec/requests/users_spec.rb

Doing so is pretty simple. For example:

/spec/requests/budget_spec.rb …line 22
context "budget widget" do
  before { visit budget_path }
  it_behaves_like 'budget status widget'
end

With all that out of the way, let’s run our full test suite to ensure everything is passing.

Terminal
bundle exec rspec spec/

Sweet!

So that’s if for today, we can merge our code into our budget feature branch.

Terminal
git add .
git commit -am "finished UI budget functionality"
git checkout budget-feature-branch
git merge bfb-finishing-the-UI
git branch -d bfb-finishing-the-UI

Summary

So we’ve essentially finished our new budget feature. We just have one annoying little issue to fix up (specifying which categories show up on the budget page) which we’ll take care of next time.

Thanks for reading and I hope you enjoyed the post!



Comment on this post!