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
Here’s what it looks like if there is available 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">‹</a>
<strong>{dateString}</strong>
<a href="#!" id="next_date_nav" title="Next Date">›</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:
|
|
This is the code to read result of getTransactions
and getCategories
from +page.server.js
. { categories } = data
means categories = data.categories
.
|
|
This is the code to populate category data. In this example, categoryMap
value is :
|
|
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.
|
|
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.
|
|
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]
.
|
|
We need the pie chart from chart.js library, so we import the components here. onMount
is imported here, too, for later process.
|
|
This is the variable to define the label and data of the pie chart by using existingCategoryArray
and perCategoryAmountArray
variables.
|
|
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
.
|
|
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.
|
|
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.
|
|
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.
|
|
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.