Skip to content

Build fintech application with Spring Boot, Vavr and Vue.js (Part 5 – Invoices and bills Vue part)

Hello again! This time we will continue to work on invoices and bills and implement client-side logic for it. We defined these documents same, so in this tutorial we will overview bills only and invoices will be similar. You can grab them from project repository.

Table of contents

What we will do in this part

This part is about creating client side for documents management – invoices and bills. This is basically heart of any accounting app. We already did their backend implementation, so now we will views that will create documents, display them as list and display a single document. In our example, invoices and bills are same, so in this tutorial we will look only on bills – and invoices are implemented equally.

Step 1. Add new dependency

Before we will dive deep into code, I want you to add a new dependency to our project – Moment.js library. This is one of most popular way to deal with dates and time in JS and we will use in our app. Use Vue UI and go to Dependencies tab. Press Install dependency button and search for Moment:

Image 1. Add dependency with Vue UI

Step 2. Create BillsNewView.vue

First view that we will implement in this part we be BillsNewView.vue. It works for creating new bill, so it has following functionality:

  1. Select vendor from list of companies (vendors)
  2. Set up dueDate and issuedDate
  3. Define a list of bill’s items – user can also adds new items
  4. Bill automatically calculates subtotal (without taxes) and total (with taxes) of the bill

As we did before, we will start from defining a layout, implement a logic and then connect it with backend using Axios.

Step 2.1. Define a layout

Each <template> has only one direct child element, in our case, as you remember, we use our own CSS class .app-content. It has descendants that are Bulma boxes – a very nice, card-like container for any content. Also I added our custom class .app-block for some space between boxes. Take a look on the code snippet below:

<template>
    <div class="app-content">
        <div class="box app-block">
            <b-field label="Select a vendor" >
                <b-select v-model="bill.vendorId" expanded>
                    <option v-for="vendor in vendors"
                        v-bind:value="vendor.companyId"
                        :key="vendor.companyId">{{vendor.name}} {{vendor.taxId}}</option>
                </b-select>
            </b-field>
        </div>
        <div class="box app-block">
            <div class="columns">
                <div class="column is-one-half">
                    <b-field label="Issue date">
                        <b-datepicker placeholder="Select a date"></b-datepicker>
                    </b-field>
                </div>
                <div class="column is-one-half">
                    <b-field label="Issue date">
                        <b-datepicker placeholder="Select a date"></b-datepicker>
                    </b-field>
                </div>
            </div>
        </div>
        <div class="box app-block">
            <table class="table is-fullwidth is-striped">
                <thead>
                    <tr>
                        <td>Position</td>
                        <td>Description</td>
                        <td>Rate</td>
                        <td>Quantity</td>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td>1</td>
                        <td>
                            <b-field>
                                <b-input type="text"></b-input>
                            </b-field>
                        </td>
                        <td>
                            <b-field>
                                <b-input pattern="[0-9]*" validation-message="Only number!"></b-input>
                            </b-field>
                        </td>
                        <td>
                            <b-field>
                                <b-numberinput></b-numberinput>
                            </b-field>
                        </td>
                    </tr>
                </tbody>
            </table>
            <div class="app-block">
                <b-button type="is-fullwidth">Add item</b-button>
            </div>
        </div>
        <div class="box app-block has-text-right">
            <p>Subtotal: 0.00</p>
            <p>Taxes: 0.00</p>
            <p class="is-size-4 has-text-primary">Total: 0.00</p>
        </div>
        <div class="app-block">
            <b-button type="is-primary is-fullwidth">Save bill</b-button>
        </div>
    </div>
</template>

What to note here?

  • We use select to provide the user a list of vendors and to be able to choose one. If you are more advanced Vue user, I highly recommend to you to check Buefy’s autocomplete, as it brings an incredible user experience, comparing to just select
  • We have datepicker components from Buefy to get dueDate and issuedDate fields of bill. Original Bulma datepickers use native implementations and most browsers (Chrome, Safari, Opera, Edge) are ugly. In Firefox mas o menos, but it is better to unify
  • To display bill’s items we use a basic Bulma table with familiar to us v-for directive to display list data.
  • We use input components from Buefy instead of native Bulma – they give us more features, including client-side validation (here we use pattern to assert, that only numbers are entered)

Let run this template and access http://localhost:8080/#/bills/new. It looks like this:

Image 2. New bill layout

Now move on and do some scripting

Step 2.2. Add new item

Our bill is empty – we need to add some data there. We already have a button Add item that is expected to create a new row in the items table. Let add a bill model first add data() function to component:

data() {
    return {
        bill: {
            tax: 0.25,
            dueDate: new Date(),
            issuedDate: new Date(),
            items: [
                {position: 1, description: '', rate: 0, quantity: 1}
            ],
            vendorId: ''
        },
        vendors: []
    }
}

Basically a data model replicates Java’s entities we implemented on backend side. Now implement a method addNewItem() that will create a new row in a table by click on a button:

addNewItem(){
   let position = this.bill.items.length + 1
   this.bill.items.push(
           {
               position: position, description: '', rate: 0, quantity: 1
           })
}

NB, as we use array to store documents’ items, we remember, that they start indexing from 0, but positions in financial documents start from 1. Because we add elements to the end, using push(), its logical index is equal to length of array, but because we need a position before insertion, we get a length and add 1 to it.

Step 2.3. Compute total and subtotal

We have two values – total and subtotal that we need to update in real time as we change a list of items. For this task, Vue offers us computed properties. Basically they are like methods, but they always should return something, as we use them in a template. We use Dinero.js to do money computations, so first you need to import it:

import Dinero from 'dinero.js'


Based on our objectives, it is a good idea to have two types of methods:

  1. Ordinary methods that return Dinero-specific entities that can be than used in other computations
  2. Computed properties return formatted monetary data to display in a template

Add these two methods in methods block:

calculateSubtotal(){
    let subtotal = Dinero({amount: 0, currency: 'EUR'})
    this.bill.items.forEach(item=>{
        let rate = item.rate * 100
        let value = Dinero({amount: rate, currency: 'EUR'}).multiply(item.quantity)
        subtotal = subtotal.add(value)
    })
    return subtotal
},
calculateTotal(){
    let subtotal = this.calculateSubtotal()
    let tax = subtotal.multiply(this.bill.tax);
    let total = tax.add(subtotal)
    return total
}

Next, add new propery computed with following logic:

computed: {
    getSubtotal() {
        let subtotal = this.calculateSubtotal()
        return subtotal.toFormat('$0,0.00')
    },
    getTotal() {
        let total = this.calculateTotal()
        return total.toFormat('$0,0.00')
    }
}

You see, that subtotal value returned by calculateSubtotal is actually a Dinero object. But computed propery getSubtotal returns pre-formated value. Note:

  • Dinero uses minor currency unit, e.g. it stores numbers as integers, not decimals to avoid losing of decimal precision. E.g. if you have 99.99 EUR, Dinero stores it as 9999 (99.99 x 100)
  • We use Euro in our example, but you can note this '$0,0.00' argument in toFormat() methods. This has nothing with dollars, it is just one of patterns, that tells Dinero: put currency first, than monetary value with two decimal positions. Don’t change $ to your own currency symbol – Dinero will do it. Take a look on all possible formats in Dinero instead

Once you run this component, you can check that data is updated on the go:

Image 3. Compute total and subtotal values

Step 2.4. Connect to API

In this view we need to do 2 API calls:

  1. To retrieve a list of vendors for user
  2. To post a new bill

As you remember, first we need to import AxiosClient instance and also Moment library (go there a bit later):

import {AxiosClient} from '@/http/AxiosClient.js'
import moment from 'moment'

First thing is to get vendors. Create a new data() array to store these entities:

data() {
    return {
        //..
        vendors: []
    }
}

Next, we need to create method that is going to retrive a data from backend:

getVendors() {
    let url = 'companies/vendors/' + this.userId
    AxiosClient.get(`${url}`).then(res => {this.vendors = res.data})
}

We have user ID hardcoded on this stage. Finally let run it during created hook, so user will get a list of vendors to populate select input:

created() {
    this.getVendors()
}

Now we can actually save a new bill. That is fairy easy: create a method and add it as click event to the corresponding button Save Bill:

saveBill(){
    this.bill.userId = this.userId
    this.bill.total = this.calculateTotal().toUnit()
    this.bill.subtotal = this.calculateSubtotal().toUnit()      
    this.bill.issuedDate = moment(this.bill.issuedDate).unix()
    this.bill.dueDate = moment(this.bill.dueDate).unix()

    AxiosClient.post(`bills/`, this.bill).then(res => {
        this.$buefy.toast.open('Bill created sucessfully')
        console.log(res.data)
    }).catch(err => {
        this.$buefy.toast.open({message: 'Unable to save bill!', type: 'is-danger'})
        console.log(err)
    })
},

Note here:

  • We get numeric representation of total and subtotal with toUnit() method, that returns values that can be used by BigDecimals on backend
  • Buefy date pickers return “normal” JS date objects, although in our backend we agreed to use Unix timestamps, so we need to convert JS dates to unix timestamps using unix() method from Moment

Run and check that everything is now running:

Image 4. Create new bill (test with Spring Boot running)

Code source for BillsNewView.vue

You can access a final result (complete code) for this component here

Step 3. Create BillsListView.vue

This view displays list of bills that user have. This will be a typical table, but it also should allow to delete the selected document from database and also open detailed bill view.

Step 3.1. Define a layout

This component is basically a table that uses our known v-for directive to display an array of entities. We also added two buttons and we will add respective methods for them next:

<template>
    <div class="app-content">
        <table class="table is-fullwidth is-striped">
            <thead>
                <tr>
                    <td>Issued date</td>
                    <td>Due date</td>
                    <td>Total</td>
                    <td>Actions</td>
                </tr>
            </thead>
            <tbody>
                <tr v-for="bill in bills" :key="bill.billId">
                    <td>bill.issuedDate</td>
                    <td>bill.dueDate</td>
                    <td>EUR {{bill.total}}</td>
                    <td>
                        <b-button type="is-primary" v-on:click="openBill(bill.billId)">Open</b-button>
                        <b-button type="is-danger" v-on:click="removeBill(bill.billId)">Delete</b-button>
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
</template>

You can check it by running:

Image 5. Bills list layout

Step 3.2. Force date format

For now we display dates as-is, how they arrived from backend (and we hope they be like unix timestamp values). Much better idea to create JS dates from these raw values and output formatted dates using toLocaleDateString. The only problem is that now output depends on concrete user’s locale settings. Finance apps should have some unified way to work with data in locale-independent way. The even better approach is to use Moment library to output dates in all locales in same manner.

I have a Estonian company and have to work with Estonia-specific accounting platform. However my laptop has English as a language and my default locale is Czech. But I need to have all formats in accounting app to be explicitly Estonian, not Czech. For this it is good to use library as Moment

First import Moment:

import moment from 'moment'

Second step is to create a helper method formatDate that accepts date as timestamp, converts it to Moment object and output in defined format:

formatDate(date){
    return moment.unix(date).format('DD.MM.YYYY')
}

And last step is to actually add it to our layout:

<td>{{formatDate(bill.issuedDate)}}</td>
<td>{{formatDate(bill.dueDate)}}</td>

Step 3.3. Connect with API

We have 2 actions for this component to done with API:

  • Get list of bills for user
  • Delete a bill from list

Both these features require to access backend, so we will use Axios here. To start, import AxiosClient:

import {AxiosClient} from '@/http/AxiosClient.js'

Then let move to concrete actions. We can delete something only if we actually have it, so I think, it is logically first action to retrieve list of bills from API. Allocate it as getBills method:

getBills(){
    let url = 'bills/all/' + this.userId
    AxiosClient.get(`${url}`).then(response => {
        this.bills = response.data
    }).catch(err => {
        console.log(err)
    })
}

and don’t forget to call in created lifecycle hook:

created() {
    this.getBills()
}

Then we have to implement removeBill action. NB that it requires the bill’s unique ID as an argument:

removeBill(id){
    let url = 'bills/' + id
    AxiosClient.delete(`${url}`).then(response => {
        this.$buefy.toast.open('Bill was removed')
        this.bills = this.bills.filter(bill => {return bill.billId !== id})
    }).catch(err => {console.log(err)})
}

Note, than we also remove a bill from array, if entity was successfully deleted from backend.

Step 3.4. Open bill’s detailed view

The only missing item here is an action for the Open button. In our application it is responsible to open a bill’s detailed view (as url/#/bill/:id). We already talk about props and id path param is actually a prop, that we need to get inside created lifecycle hook and retrieve data and finally display it to user.

So, in other words, when user presses this Open button, app should drive user to the url we mentioned earlier. We learned already how to define routes, let see how to open it not from app. There are two ways: from template or from code. In this part we will see only how to do it from code and next time we will see how to do it from template.

From a technical point of view, Vue.js router has an array routes, and this is point we will play with. All what we need is to push new item to the router. Take a look on this implementation for openBill method:

openBill(val){
    this.$router.push({name: 'bill-one', params: {id: val}})
}

Note, that we specify here name of component and params (props) we want to pass using notation key: value, e.g. here id: val. NB params is an object.

Code source for BillsListView.vue

You can access a final result (complete code) for this component here

Step 4. BillOneView.vue

And the logically last component in this part is the one that displays detailed view of bill (e.g. implementation of step 3.4).

Step 4.1. Define a layout

This component has relatively easy layout, let immediately do it in this code snippet:

<template>
  <div class="app-content">
    <div class="hero">
      <div class="hero-body columns">
        <div class="column">
            <p class="is-size-5">Vendor</p>
            <p>Name: {{vendor.name}}</p>
            <p>Tax ID: {{vendor.taxId}}</p>
            <p>Address: {{vendor.address.postalCode}} {{vendor.address.addressLine}} {{vendor.address.city}}</p>
        </div>
        <div class="column">
            <p>Issued date: {{formatDate(bill.issuedDate)}}</p>
            <p>Due date: {{formatDate(bill.dueDate)}}</p>
        </div>
      </div>
    </div>
    <div class="app-block">
      <table class="table is-fullwidth is-striped">
        <thead>
          <tr>
            <td>Position</td>
            <td>Description</td>
            <td>Rate</td>
            <td>Quantity</td>
          </tr>
        </thead>
        <tbody>
          <tr v-for="item in bill.items" :key="item.position">
            <td>{{item.position}}</td>
            <td>{{item.description}}</td>
            <td>EUR {{item.rate}}</td>
            <td>x {{item.quantity}}</td>
          </tr>
        </tbody>
      </table>
    </div>
    <div class="app-block has-text-right">
        <p>Subtotal: {{bill.subtotal}}</p>
        <p>Taxes: {{bill.tax}}</p>
        <p class="is-size-4 has-text-primary">Total: {{bill.total}}</p>
    </div>
  </div>
</template>

What I want to note here:

  • We use formatDate method, that we already did in previous component; just copy it
  • Bulma Hero is cool fullwidth layout container that is good for bill’s header where we put vendor data and dates
  • Here we need to have two entities: Bill and Vendor

As we actually did date formatting, let concentrate only on connecting to API.

Step 4.2. Connect to API

As usual, start with importing our old good friend AxiosClient:

import {AxiosClient} from '@/http/AxiosClient.js'

Then create two entities inside data() function:

data() {
    return {
        bill: {},
        vendor: {}
    }
}

We need to chain two methods – we retrieve a vendor only if we have obtained bill successfully:

getBill(){
    let url = 'bills/one/' + this.id
    AxiosClient.get(`${url}`).then(res => {
        this.bill = res.data
        let vendorId = this.bill.vendorId
        this.getVendor(vendorId)
    })
},
getVendor(id){
    let url = 'companies/one/' + id
    AxiosClient.get(`${url}`).then(res=>{
        this.vendor = res.data
    })
}

And final step to call it inside created:

created() {
    this.getBill()
}

Run this component and validate results.

Code source for BillOneView.vue

You can access a final result (complete code) for this component here

Copy link
Powered by Social Snap