Thursday, 28 May 2026

Web Tutorial: VueJS Financial Projector (Part 3/3)

At long last, we get to the exciting part! No more CRUD functions - now it's purely display.

Add this to the data. monthItems is an array of data from January to December. monthItemsTemplate is an array that shows us what that data is supposed to look like. The idea here is that the display will refresh when monthItems changes... but I don't want it to render constantly when I recalculate, so I have a temporary array to hold the new completely recalculated array in until I'm ready to replace monthItems.
data: {
    currentIndex: 0,
    errors: {
        name: "",
        amount: "",
    },
    items:[
        {
            name: "",
            month: 0,
            amount: 0
        }
    ],
    monthItems: [],
    monthItemsTemplate: [

    ],

    months: [
        "All", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
    ]
},


So right now, this is it. The table looks like this. The first element at index 0 is an object that's just a placeholder. The other rows correspond with the month names - index 1 for "Jan", index 2 for "Feb", etc.
monthItems: [],
monthItemsTemplate: [
    { monthName: "", itemsIn:[], itemsInTotal:"", itemsOut:[], itemsOutTotal:"", cumulative: "" },
    { monthName: "Jan", itemsIn:[], itemsInTotal:0, itemsOut:[], itemsOutTotal:0, cumulative: 0 },
    { monthName: "Feb", itemsIn:[], itemsInTotal:0, itemsOut:[], itemsOutTotal:0, cumulative: 0 },
    { monthName: "Mar", itemsIn:[], itemsInTotal:0, itemsOut:[], itemsOutTotal:0, cumulative: 0 },
    { monthName: "Apr", itemsIn:[], itemsInTotal:0, itemsOut:[], itemsOutTotal:0, cumulative: 0 },
    { monthName: "May", itemsIn:[], itemsInTotal:0, itemsOut:[], itemsOutTotal:0, cumulative: 0 },
    { monthName: "Jun", itemsIn:[], itemsInTotal:0, itemsOut:[], itemsOutTotal:0, cumulative: 0 },
    { monthName: "Jul", itemsIn:[], itemsInTotal:0, itemsOut:[], itemsOutTotal:0, cumulative: 0 },
    { monthName: "Aug", itemsIn:[], itemsInTotal:0, itemsOut:[], itemsOutTotal:0, cumulative: 0 },
    { monthName: "Sep", itemsIn:[], itemsInTotal:0, itemsOut:[], itemsOutTotal:0, cumulative: 0 },
    { monthName: "Oct", itemsIn:[], itemsInTotal:0, itemsOut:[], itemsOutTotal:0, cumulative: 0 },
    { monthName: "Nov", itemsIn:[], itemsInTotal:0, itemsOut:[], itemsOutTotal:0, cumulative: 0 },
    { monthName: "Dec", itemsIn:[], itemsInTotal:0, itemsOut:[], itemsOutTotal:0, cumulative: 0 }

],


After this, we need a new method, sortItemOrder(). This populates the monthItems array with the correct data. Because elements added, updated or removed in items will trigger a recalculation of cumulative values. The idea for monthItems is that each month, from January to December, will show totals after calculating incoming and outgoing funds.
    setCurrentIndex: function(val) {
        this.currentIndex = val;
    },
    sortItemOrder: function() {

    }

}


We declare a temporary array, tempMonthItems. As its value, we use a copy of monthItemsTemplate using the structuredClone() function. At the end of this method, we're going to replace monthItems with tempMonthItems after processing it. (For more about the structuredClone() function, follow this link.)
sortItemOrder: function() {
    let tempMonthItems = structuredClone(this.monthItemsTemplate);

    this.monthItems = structuredClone(tempMonthItems);

}


As part of that process, we're going to fix the elements in tempMonthItems corresponding to months 1 to 12, i.e, January to December. Index 0 is the placeholder element.
sortItemOrder: function() {
    let tempMonthItems = structuredClone(this.monthItemsTemplate);

    for (let i = 1; i <= 12; i++) {

    }


    this.monthItems = structuredClone(tempMonthItems);
}


We populate itemsIn and itemsOut. itemsIn in each element of tempMonthItems, is the array of every element in items where month is "All" or the current month, and amount is more than 0. itemsOut in each element of tempMonthItems, is the array of every element in items where month is "All" or the current month, and amount is more than 0. To get this data, we use the filter() method.
sortItemOrder: function() {
    let tempMonthItems = structuredClone(this.monthItemsTemplate);

    for (let i = 1; i <= 12; i++) {
        tempMonthItems[i].itemsIn = this.items.filter((item) => {return item.amount > 0 && [0, i].indexOf(item.month) != -1});
        tempMonthItems[i].itemsOut = this.items.filter((item) => {return item.amount < 0 && [0, i].indexOf(item.month) != -1});
    }

    this.monthItems = structuredClone(tempMonthItems);
}


Then we want the sum of all items in itemsIn and itemsOut, and assign those values to the itemsInTotal and itemsOutTotal properties, using the reduce() method. (For more about the reduce() method, follow this link.)
sortItemOrder: function() {
    let tempMonthItems = structuredClone(this.monthItemsTemplate);

    for (let i = 1; i <= 12; i++) {
        tempMonthItems[i].itemsIn = this.items.filter((item) => {return item.amount > 0 && [0, i].indexOf(item.month) != -1});
        tempMonthItems[i].itemsOut = this.items.filter((item) => {return item.amount < 0 && [0, i].indexOf(item.month) != -1});

        tempMonthItems[i].itemsInTotal = tempMonthItems[i].itemsIn.reduce((sum, item) => sum + item.amount, 0);
        tempMonthItems[i].itemsOutTotal = tempMonthItems[i].itemsOut.reduce((sum, item) => sum + item.amount, 0);

    }

    this.monthItems = structuredClone(tempMonthItems);
}


The cumulative property is 0 if we're looking at January because we haven't accumulated anything yet at the start of the year. Thus, if i is 1 or less, cumulative is 0.
sortItemOrder: function() {
    let tempMonthItems = structuredClone(this.monthItemsTemplate);

    for (let i = 1; i <= 12; i++) {
        tempMonthItems[i].itemsIn = this.items.filter((item) => {return item.amount > 0 && [0, i].indexOf(item.month) != -1});
        tempMonthItems[i].itemsOut = this.items.filter((item) => {return item.amount < 0 && [0, i].indexOf(item.month) != -1});

        tempMonthItems[i].itemsInTotal = tempMonthItems[i].itemsIn.reduce((sum, item) => sum + item.amount, 0);
        tempMonthItems[i].itemsOutTotal = tempMonthItems[i].itemsOut.reduce((sum, item) => sum + item.amount, 0);

        tempMonthItems[i].cumulative = (i > 1 ? : 0);
    }

    this.monthItems = structuredClone(tempMonthItems);
}


If we're at February or later, we take the remainding funds of the current month (itemsInTotal + itemsOutTotal) and add the cumulative property of the previous month, to derive the current month's cumulative value.
sortItemOrder: function() {
    let tempMonthItems = structuredClone(this.monthItemsTemplate);

    for (let i = 1; i <= 12; i++) {
        tempMonthItems[i].itemsIn = this.items.filter((item) => {return item.amount > 0 && [0, i].indexOf(item.month) != -1});
        tempMonthItems[i].itemsOut = this.items.filter((item) => {return item.amount < 0 && [0, i].indexOf(item.month) != -1});

        tempMonthItems[i].itemsInTotal = tempMonthItems[i].itemsIn.reduce((sum, item) => sum + item.amount, 0);
        tempMonthItems[i].itemsOutTotal = tempMonthItems[i].itemsOut.reduce((sum, item) => sum + item.amount, 0);

        tempMonthItems[i].cumulative = (i > 1 ? tempMonthItems[i].itemsInTotal + tempMonthItems[i].itemsOutTotal + tempMonthItems[i - 1].cumulative : 0);
    }

    this.monthItems = structuredClone(tempMonthItems);
}


With that done, we make sure to run sortItemOrder() at the end of addUpdateItem() and removeItem().
    this.setCurrentIndex(0);
    this.sortItemOrder();
},
removeItem: function(index) {
    this.items.splice(index, 1);
    this.setCurrentIndex(0);
    this.sortItemOrder();
},
setCurrentIndex: function(val) {
    this.currentIndex = val;
},


Now, for the HTML!
There, in pnlFinancialProjection, have a table. These are the headers.
<div id="pnlFinancialProjection" class="panel">
    <table>
        <tr class="header">
            <td width="20%">MONTH</td>
            <td width="20%" class="numeric">IN</td>
            <td width="20%" class="numeric">OUT</td>
            <td width="20%" class="numeric">REMAINING</td>
            <td width="20%" class="numeric">CUMULATIVE</td>
        </tr>
    </table>

</div>


And here's your table taking shape...


Then we render rows for every element in monthItems other than the one at index 0.
<table>
        <tr class="header">
            <td width="20%">MONTH</td>
            <td width="20%" class="numeric">IN</td>
            <td width="20%" class="numeric">OUT</td>
            <td width="20%" class="numeric">REMAINING</td>
            <td width="20%" class="numeric">CUMULATIVE</td>
        </tr>

        <tr v-for="(monthItem, monthItemIndex) in monthItems" v-bind:key="monthItemIndex" v-if="monthItemIndex > 0">

        </tr>

</table>


We put in the month name, itemsInTotal, itemsOutTotal, and in the case of the REMAINING column, we calculate it on the spot. If the result is positive, we style it using inText, otherwise we use outText. In both cases, we also use numeric. We do the same for cumulative.
<tr v-for="(monthItem, monthItemIndex) in monthItems" v-bind:key="monthItemIndex" v-if="monthItemIndex > 0">
    <td>{{ monthItem.monthName }}</td>
    <td class="numeric inText">{{ monthItem.itemsInTotal }}</td>
    <td class="numeric outText">{{ monthItem.itemsOutTotal }}</td>
    <td v-bind:class=" monthItem.itemsInTotal + monthItem.itemsOutTotal > 0 ? 'numeric inText' : 'numeric outText'">{{ monthItem.itemsInTotal + monthItem.itemsOutTotal }}</td>
    <td v-bind:class=" monthItem.cumulative > 0 ? 'numeric inText' : 'numeric outText'">{{ monthItem.cumulative }}</td>

</tr>



Let's test this app!
Add the item "Salary" for all months. I'm just going to put SGD 6,000 here.


Then we'll balance it out with "Expenses", which is an outgoing item for all months. We set it at SGD 1,000. Look at the financial projection now.


Then we have "Income Tax", which is to be paid in May.


Then I declare "Annual Bonus" in December, an incoming item at another SGD 6,000.

And let's say I pay "AIA Insurance" in September. Now in the financial projection, you can see that the REMAINING column is starting to show red.

Here, I add "Womb Tax" for all months, which is my quaint codeword for money I give my mother.


And "Investments" for all months. Now you can see , more of the REMAINING column has turned red. The CUMULATIVE column adjusts automatically as well.


Right on the money,
T___T

No comments:

Post a Comment