I have completed the implementation of most of the main page features. It can load recent transactions and create a pie chart based on these transactions. Furthermore, the page displays both the total earnings and expense of the day. There is one feature that I have not completed yet, and that is the ability to view transactions on a date other than the current date.

Display

New Main Page

Here’s what it looks like if there is available data: New Main Page with Data

To ensure good accessibility, I use Accessible Colors to adjust the red and green text colors. As for the chart color, Chart.js automatically handles the setup.

Implementation

Let’s take a look at its +page.server.js implementation:

+page.server.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import 'dotenv-expand/config'

export const load = async ({fetch}) => {
    const getTodayDate = async() => {
        let now = new Date(), month, day, year;
        let dateString;

        month = '' + (now.getMonth() + 1),
        day = '' + now.getDate(),
        year = now.getFullYear();

        if (month.length < 2) 
            month = '0' + month;
        if (day.length < 2) 
            day = '0' + day;

        dateString = [year, month, day].join('-');
        return dateString;
    }

    const getTransactions = async() => {
        const todayDate = await getTodayDate();
        const endpoint = `${process.env.HOST_URL}/users/${process.env.USER_ID}/transactions/get/${todayDate}`;
        const transactionRes = await fetch(endpoint)
        const transactionData = await transactionRes.json()
        return transactionData.transaction_list      
    }

    const getCategories = async() => {
        const categoryRes = await fetch(`${process.env.HOST_URL}/users/${process.env.USER_ID}/categories/list`)
        
        const categoryData = await categoryRes.json()
        return categoryData.category_list      
    }

    return {
        transactions: getTransactions(),
        categories: getCategories()
    }
}

There are 3 functions provided when the page loads: getTodayDate, getTransactions, and getCategories. getTransactions is a function to get a transaction list based on date. getCategories is a function to a user’s category. load function will return result from getTransactions and getCategories.

Now, we will look at +page.svelte implementation:

+page.svelte
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
<script>
    export let data;
    const { categories } = data;
    const { transactions } = data;

    let categoryMap = {};
    for (let i = 0 ; i < categories.length; i++) {
        categoryMap[categories[i]["category_id"]] = {
            "name": categories[i]["category_name"],
            "type": categories[i]["category_type"]
        };
    }

    let totalEarning = 0;
    let totalExpense = 0;

    let categoryAmountMap = new Map();
    for (let i = 0; i < transactions.length; i++) {
        let transaction = transactions[i];

        let categoryId = transaction['category_id'];
        let categoryData = categoryMap[categoryId];
        let categoryName = categoryData['name'];
        if(categoryData['type'] === 'EXPENSE' ) {
            totalExpense += transaction['amount'];
        } else {
            totalEarning += transaction['amount'];
        }

        if (categoryAmountMap.has(categoryName)) {
            let catAmount = categoryAmountMap.get(categoryName);
            categoryAmountMap.set(categoryName, catAmount + transaction['amount']);
        } else {
            categoryAmountMap.set(categoryName, transaction['amount']);
        }
    }

    let existingCategoryArray = Array.from(categoryAmountMap.keys());
    let perCategoryAmountArray = [];
    for(let i = 0 ; i < existingCategoryArray.length; i++) {
        let cat = existingCategoryArray[i];
        perCategoryAmountArray.push(categoryAmountMap.get(cat));
        
    }
    import Chart from 'chart.js/auto';
    import { Colors } from 'chart.js';
    import { onMount } from 'svelte';

    Chart.register(Colors);
	let now = new Date(), month, day, year;
	let dateString;
	
    let portfolio;

    const chartData = {
        labels: existingCategoryArray,
        datasets: [
            {
                label: 'Amount',
                data: perCategoryAmountArray,
                // hoverOffset: 4,
                borderWidth: 0
            }
        ]
    };
    const config = {
        type: 'pie',
        data: chartData,
        options: {
            borderRadius: '10',
            responsive: true,
            maintainAspectRatio: false,
            spacing: 0,
            plugins: {
                legend: {
                    position: 'bottom',
                    display: true,
                    labels: {
                        usePointStyle: true,
                        padding: 20,
                        font: {
                            size: 14
                        }
                    }
                },
                title: {
                    display: true,
                    text: 'Portfolio'
                }
            }
        }
    };
    onMount(()=> {
        month = '' + (now.getMonth() + 1),
        day = '' + now.getDate(),
        year = now.getFullYear();

        if (month.length < 2) 
            month = '0' + month;
        if (day.length < 2) 
            day = '0' + day;

        dateString = [year, month, day].join('-');
        totalEarning = totalEarning.toLocaleString();
        totalExpense = '(' + totalExpense.toLocaleString() + ')';

        const ctx = portfolio.getContext('2d');
        var myChart = new Chart(ctx, config);
	});
</script>

<style src="./main.scss"></style>

<main class="admin__main">
    <div class="dashboard">
        <div class="dashboard__item bigvalue">
            <div class="card">
                <strong class=" bigvalue__earning" title="Earning">{totalEarning}</strong>
            </div>
        </div>
        <div class="dashboard__item">
            <div class="card bigvalue">
                <a href="#!" id="prev_date_nav" title="Previous Date">&#8249;</a>
                <strong>{dateString}</strong>
                <a href="#!" id="next_date_nav" title="Next Date">&#8250;</a>
            </div>
        </div>
        <div class="dashboard__item">
            <div class="card bigvalue">
                <strong class="bigvalue__expense" title="Expense">{totalExpense}</strong>
            </div>
        </div>
        <div class="dashboard__item dashboard__item--full">
            <div class="card">
                <div class="chart">
                    <canvas bind:this={portfolio} width={500} height={300} >
                        Your browser does not support the canvas element.
                    </canvas>
                </div>
            </div>
        </div>
        <div class="dashboard__item dashboard__item--col">
            <div class="card">
                {#if transactions.length > 0}
                <div class="dashboard__table">
                    <table>
                        <caption>Transaction</caption>
                        <thead>
                            <tr>
                                <th class="table__default">Category</th>
                                <th class="table__number">Amount</th>
                                <th class="table__default">Notes</th>
                            </tr>
                        </thead>
                        <tbody>
                            {#each transactions as transaction}
                                <tr>
                                    <td>{categoryMap[transaction['category_id']]['name']}</td>
                                    <td>
                                        {#if categoryMap[transaction['category_id']]['type'] === 'EARNING'}
                                            <span class="transaction__amount transaction__amount__positive">
                                                {transaction.amount.toLocaleString()}
                                            </span>
                                        {:else}
                                            <span class="transaction__amount transaction__amount__negative">
                                                {transaction.amount.toLocaleString()}
                                            </span>
                                        {/if}
                                    </td>
                                    <td>
                                        <div class="content">
                                            {#if transaction.note != null}
                                                {transaction.note}
                                            {/if}
                                        </div>
                                    </td>
                                </tr>
                            {/each}    
                        </tbody>
                    </table>
                </div>
                {/if}
            </div>
        </div>
    </div>
</main>

Let’s break down the script first:

1
2
3
export let data;
const { categories } = data;
const { transactions } = data;

This is the code to read result of getTransactions and getCategories from +page.server.js. { categories } = data means categories = data.categories.

1
2
3
4
5
6
7
let categoryMap = {};
for (let i = 0 ; i < categories.length; i++) {
    categoryMap[categories[i]["category_id"]] = {
        "name": categories[i]["category_name"],
        "type": categories[i]["category_type"]
    };
}

This is the code to populate category data. In this example, categoryMap value is :

1
2
3
4
5
6
{
  '200000000': { name: 'Test', type: 'EARNING' },
  '200000001': { name: 'Hello', type: 'EXPENSE' },
  '200000002': { name: 'Hi', type: 'EXPENSE' },
  '200000003': { name: 'Blah', type: 'EARNING' }
}

with name as category name and type is the category type. categoryMap is used to display the category name and to determine the color in the transaction table.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
let categoryAmountMap = new Map();
for (let i = 0; i < transactions.length; i++) {
    let transaction = transactions[i];

    let categoryId = transaction['category_id'];
    let categoryData = categoryMap[categoryId];
    let categoryName = categoryData['name'];
    if(categoryData['type'] === 'EXPENSE' ) {
        totalExpense += transaction['amount'];
    } else {
        totalEarning += transaction['amount'];
    }

    if (categoryAmountMap.has(categoryName)) {
        let catAmount = categoryAmountMap.get(categoryName);
        categoryAmountMap.set(categoryName, catAmount + transaction['amount']);
    } else {
        categoryAmountMap.set(categoryName, transaction['amount']);
    }
}

In this code snippet, it creates a map named categoryAmountMap containing category name as the key and the total amount as its value. In this example, there is only one data in categoryAmountMap: { 'Test' => 100000 }. totalEarning and totalExpense calculation process also happens here.

1
2
3
4
5
6
let existingCategoryArray = Array.from(categoryAmountMap.keys());
let perCategoryAmountArray = [];
for(let i = 0 ; i < existingCategoryArray.length; i++) {
    let cat = existingCategoryArray[i];
    perCategoryAmountArray.push(categoryAmountMap.get(cat));
}

existingCategoryArray stores keys (categories) from categoryAmountMap, and perCategoryAmountArray stores the total of each category. Both variables play a role in supplying data for the pie chart. In this example, the value inside existingCategoryArray is ['Test'] and the value inside perCategoryAmountArray is [100000].

1
2
3
4
5
import Chart from 'chart.js/auto';
import { Colors } from 'chart.js';
import { onMount } from 'svelte';

Chart.register(Colors);

We need the pie chart from chart.js library, so we import the components here. onMount is imported here, too, for later process.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const chartData = {
    labels: existingCategoryArray,
    datasets: [
        {
            label: 'Amount',
            data: perCategoryAmountArray,
            // hoverOffset: 4,
            borderWidth: 0
        }
    ]
};

This is the variable to define the label and data of the pie chart by using existingCategoryArray and perCategoryAmountArray variables.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const config = {
    type: 'pie',
    data: chartData,
    options: {
        borderRadius: '10',
        responsive: true,
        maintainAspectRatio: false,
        spacing: 0,
        plugins: {
            legend: {
                position: 'bottom',
                display: true,
                labels: {
                    usePointStyle: true,
                    padding: 20,
                    font: {
                        size: 14
                    }
                }
            },
            title: {
                display: true,
                text: 'Portfolio'
            }
        }
    }
};

This is the configuration for the pie chart itself. In this configuration, the pie chart is made to be responsive. The legend is placed at the bottom of the chart, and has font size of 14. The chart title is Portfolio.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
onMount(()=> {
    month = '' + (now.getMonth() + 1),
    day = '' + now.getDate(),
    year = now.getFullYear();

    if (month.length < 2) 
        month = '0' + month;
    if (day.length < 2) 
        day = '0' + day;

    dateString = [year, month, day].join('-');
    totalEarning = totalEarning.toLocaleString();
    totalExpense = '(' + totalExpense.toLocaleString() + ')';

    const ctx = portfolio.getContext('2d');
    var myChart = new Chart(ctx, config);
});

onMount is a function that is called when a component is initialized. In this case, when the page loads all the HTML component, it will provide dateString, totalEarning, totalExpense, and initialize myChart variable.

Now, let’s look at the page implementation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<div class="dashboard__item">
    <div class="card bigvalue">
        <strong class=" bigvalue__earning" title="Earning">{totalEarning}</strong>
    </div>
</div>
<div class="dashboard__item">
    <div class="card bigvalue">
        <a href="#!" id="prev_date_nav" title="Previous Date">&#8249;</a>
        <strong>{dateString}</strong>
        <a href="#!" id="next_date_nav" title="Next Date">&#8250;</a>
    </div>
</div>
<div class="dashboard__item">
    <div class="card bigvalue">
        <strong class="bigvalue__expense" title="Expense">{totalExpense}</strong>
    </div>
</div>

This is the code to display top part of the page. It displays the total earning, current date string, and total expense value. All the values are sourced from their respective variables in the script before. There are previous date and next date buttons, but they are not implemented yet.

1
2
3
4
5
6
7
8
9
<div class="dashboard__item dashboard__item--full">
    <div class="card">
        <div class="chart">
            <canvas bind:this={portfolio} width={500} height={300} >
                Your browser does not support the canvas element.
            </canvas>
        </div>
    </div>
</div>

This is where the pie chart is located, using the portfolio variable set up in the script. The initial width is 500, and height of 300. There is a fallback text if a browser does not support canvas element.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<div class="dashboard__item dashboard__item--col">
    <div class="card">
        {#if transactions.length > 0}
        <div class="dashboard__table">
            <table>
                <caption>Transaction</caption>
                <thead>
                    <tr>
                        <th class="table__default">Category</th>
                        <th class="table__number">Amount</th>
                        <th class="table__default">Notes</th>
                    </tr>
                </thead>
                <tbody>
                    {#each transactions as transaction}
                        <tr>
                            <td>{categoryMap[transaction['category_id']]['name']}</td>
                            <td>
                                {#if categoryMap[transaction['category_id']]['type'] === 'EARNING'}
                                    <span class="transaction__amount transaction__amount__positive">
                                        {transaction.amount.toLocaleString()}
                                    </span>
                                {:else}
                                    <span class="transaction__amount transaction__amount__negative">
                                        {transaction.amount.toLocaleString()}
                                    </span>
                                {/if}
                            </td>
                            <td>
                                <div class="content">
                                    {#if transaction.note != null}
                                        {transaction.note}
                                    {/if}
                                </div>
                            </td>
                        </tr>
                    {/each}    
                </tbody>
            </table>
        </div>
        {/if}
    </div>
</div>

This is the HTML code to display the transaction table. If there is no transaction, the table will not be displayed. In tbody, iterate through the transactions data, utilizing categoryMap to determine both the category name and the representation of amount.

Improvement

After completing the basic feature, there are several things that I believe can be improved:

  • The ability to view past transactions is crucial for effectively analyzing spending trends, but it hasn’t been implemented yet. I will try to implement this feature soon. I was busy improving the table layout, but the result was disappointing.

  • Fixing the table layout. I have to find a way to make the table layout consistent with the overall design.

  • Updating the backend to return the result in a specific format. Currently, the frontend calls backend twice to get transactions and categories. By processing the data in the backend instead, we can reduce the API call to 1.

  • I think some of the implementations in +page.server.js can be moved to +page.js so it doesn’t run on the server.

  • The pie chart can be turned into a reusable component.

  • Support for another currency, because currently Tabunganku only has support for IDR currency.

  • The sidebar menu can be improved, but it’s a concern for the future.

P.S. I can’t guarantee I will do all of these!

I will post about the backend implementation next. I have finished the backend before creating the frontend part.

Resources

  • Accessible Colors: link
  • Chart.js: link
  • Tabunganku-fe repository: link