diff --git a/javascript-practice/src/js/controllers/homeController.js b/javascript-practice/src/js/controllers/homeController.js index acad97b..422ab52 100644 --- a/javascript-practice/src/js/controllers/homeController.js +++ b/javascript-practice/src/js/controllers/homeController.js @@ -28,30 +28,30 @@ export default class HomeController { return this.service.categoryService.getAllCategory(); } + handlerGetAllTransactions(idUser) { + return this.service.transactionService.getListTransactionByIdUser(idUser); + } + init() { if (this.homeView) { - this.homeView.addHandlerEventWalletForm( - this.handlerSaveWallet.bind(this), - this.handlerGetAllCategory.bind(this), - ); - - this.homeView.loadPage( + this.homeView.initFunction( this.handlerGetInfoUserLogin.bind(this), this.handlerCheckWalletValid.bind(this), this.handlerGetWalletUser.bind(this), this.handlerGetAllCategory.bind(this), - ); - - this.homeView.addHandlerEventBudgetForm( - this.handlerSaveTransaction.bind(this), + this.handlerGetAllTransactions.bind(this), this.handlerSaveWallet.bind(this), + this.handlerSaveTransaction.bind(this), ); + this.homeView.addHandlerEventWalletForm(); + + this.homeView.loadPage(); + + this.homeView.addHandlerEventBudgetForm(); this.homeView.handlerEventCategoryDialog(); - this.homeView.handlerEventTransactionDialog( - this.handlerSaveTransaction.bind(this), - ); + this.homeView.handlerEventTransactionDialog(); } } } diff --git a/javascript-practice/src/js/helpers/helpers.js b/javascript-practice/src/js/helpers/helpers.js index 10c5701..ea0b6b3 100644 --- a/javascript-practice/src/js/helpers/helpers.js +++ b/javascript-practice/src/js/helpers/helpers.js @@ -92,3 +92,64 @@ export const changeDateFormat = (oldFormatDate) => { return `${day}, ${date}, ${month}, ${year}`; }; + +export const createTransactionDetailObject = (category, transactions) => { + const totalTransaction = transactions.length; + const totalAmount = () => { + let amount = 0; + + transactions.forEach((transaction) => { + amount += transaction.amount; + }); + + return amount; + }; + const listTransaction = () => { + const results = []; + + transactions.forEach((transaction) => { + const dateParts = transaction.date.split(','); // ['Monday', '14', 'September', '2023'] + const day = dateParts[1]; + const fullDateString = `${dateParts[0]}, ${dateParts[2]} ${dateParts[3]}`; + const tempData = { + id: transaction.id, + day, + fullDateString, + note: transaction.note, + amount: transaction.amount, + }; + + results.push(tempData); + }); + + results.sort((a, b) => parseInt(b.id, 10) - parseInt(a.id, 10)); + + return results; + }; + + return { + categoryName: category.name, + url: category.url, + totalTransaction, + totalAmount: totalAmount(), + transactions: listTransaction(), + }; +}; + +export const getAllCategoryNameInTransactions = (transactions) => { + const categoryName = new Set(); + + transactions.forEach((transaction) => + categoryName.add(transaction.categoryName), + ); + + return Array.from(categoryName); +}; + +export const getAllTransactionByCategoryName = (categoryName, transactions) => { + const results = transactions.filter((transaction) => { + return transaction.categoryName === categoryName; + }); + + return results; +}; diff --git a/javascript-practice/src/js/models/transaction.js b/javascript-practice/src/js/models/transaction.js index ca598df..32065ba 100644 --- a/javascript-practice/src/js/models/transaction.js +++ b/javascript-practice/src/js/models/transaction.js @@ -3,14 +3,16 @@ export default class Transaction { id = Transaction.createIdTransactions(), categoryName = '', date = '', - note = 'None', + note = '', amount = 0, + idUser, }) { this.id = id; this.categoryName = categoryName; this.date = date; this.note = note; this.amount = amount; + this.idUser = idUser; } static createIdTransactions() { diff --git a/javascript-practice/src/js/services/categoryService.js b/javascript-practice/src/js/services/categoryService.js index 86b0db3..c0d48a3 100644 --- a/javascript-practice/src/js/services/categoryService.js +++ b/javascript-practice/src/js/services/categoryService.js @@ -22,4 +22,16 @@ export default class CategoryService extends CommonService { return null; } + + async getCategoryByName(nameCategory) { + const data = await this.getDataFromProp( + 'name', + nameCategory, + this.defaultPath, + ); + + if (data) return data; + + return null; + } } diff --git a/javascript-practice/src/js/services/commonService.js b/javascript-practice/src/js/services/commonService.js index ee96671..3591ef6 100644 --- a/javascript-practice/src/js/services/commonService.js +++ b/javascript-practice/src/js/services/commonService.js @@ -66,11 +66,39 @@ export default class CommonService { async getAllDataFromPath(path = this.defaultPath) { this.connectToDb(); - const result = await this.firebaseService.getAllDataFromPath(path); + const results = await timeOutConnect( + this.firebaseService.getAllDataFromPath(path), + ); + const listData = []; - const data = await timeOutConnect(result); + if (results) { + // Convert format object + results.forEach((data) => { + listData.push(convertDataObjectToModel(data)); + }); - if (data) return data; + return listData; + } + + return null; + } + + async getListDataFromProp(property, value, path = this.defaultPath) { + this.connectToDb(); + + const results = await timeOutConnect( + this.firebaseService.getListDataFromProp(path, property, value), + ); + const listData = []; + + if (results) { + // Convert format object + results.forEach((data) => { + listData.push(convertDataObjectToModel(data)); + }); + + return listData; + } return null; } diff --git a/javascript-practice/src/js/services/firebaseService.js b/javascript-practice/src/js/services/firebaseService.js index a5a40f7..ca7bcbf 100644 --- a/javascript-practice/src/js/services/firebaseService.js +++ b/javascript-practice/src/js/services/firebaseService.js @@ -100,7 +100,7 @@ class FirebaseService { onValue( ref(this.db, path), (snapshot) => { - const data = []; + const listData = []; // snapshot is a type of data by Firebase define snapshot.forEach((childSnapshot) => { @@ -111,11 +111,40 @@ class FirebaseService { // data = dataTemp; // } const id = childSnapshot.key; - const val = childSnapshot.val(); + const data = childSnapshot.val(); - data.push({ id, ...val }); + listData.push({ id, data }); }); - resolve(data); + resolve(listData); + }, + { + onlyOnce: true, + }, + ); + }); + } + + getListDataFromProp(path, property, value) { + return new Promise((resolve) => { + onValue( + ref(this.db, path), + (snapshot) => { + let id; + let data; + const listData = []; + + // snapshot is a type of data by Firebase define + snapshot.forEach((childSnapshot) => { + const dataTemp = childSnapshot.val(); + + if (dataTemp[property] === value) { + id = childSnapshot.key; + data = dataTemp; + + listData.push({ id, data }); + } + }); + resolve(listData); }, { onlyOnce: true, diff --git a/javascript-practice/src/js/services/transactionService.js b/javascript-practice/src/js/services/transactionService.js index 15f8210..1fd2d35 100644 --- a/javascript-practice/src/js/services/transactionService.js +++ b/javascript-practice/src/js/services/transactionService.js @@ -14,4 +14,18 @@ export default class TransactionService extends CommonService { async saveTransaction(transaction) { await this.save(transaction); } + + async getListTransactionByIdUser(idUser) { + const results = this.getListDataFromProp( + 'idUser', + idUser, + this.defaultPath, + ); + + if (results) { + return results; + } + + return null; + } } diff --git a/javascript-practice/src/js/views/homeView.js b/javascript-practice/src/js/views/homeView.js index 4f1ce6c..e5c9beb 100644 --- a/javascript-practice/src/js/views/homeView.js +++ b/javascript-practice/src/js/views/homeView.js @@ -4,7 +4,13 @@ import * as MESSAGE from '../constants/message'; import CommonView from './commonView'; import Wallet from '../models/wallet'; import Transaction from '../models/transaction'; -import { changeDateFormat, formatNumber } from '../helpers/helpers'; +import { + changeDateFormat, + createTransactionDetailObject, + formatNumber, + getAllCategoryNameInTransactions, + getAllTransactionByCategoryName, +} from '../helpers/helpers'; import defaultCategoryIcon from '../../assets/images/question-icon.svg'; export default class HomeView extends CommonView { @@ -30,21 +36,35 @@ export default class HomeView extends CommonView { this.walletDialog = document.getElementById('walletDialog'); } - async loadPage( + initFunction( getInfoUserLogin, isValidWallet, getWalletByIdUser, getAllCategory, + getAllTransactions, + saveWallet, + saveTransaction, ) { - this.getWalletByIdUser = getWalletByIdUser; // Init function + this.getInfoUserLogin = getInfoUserLogin; + this.isValidWallet = isValidWallet; + this.getWalletByIdUser = getWalletByIdUser; + this.getAllCategory = getAllCategory; + this.getAllTransactions = getAllTransactions; + this.saveWallet = saveWallet; + this.saveTransaction = saveTransaction; + } + + async loadPage() { + // Init function + this.toggleLoaderSpinner(); - const user = await getInfoUserLogin(); + const user = await this.getInfoUserLogin(); if (!user) { window.location.replace('/login'); } else { this.user = user; - const wallet = await isValidWallet(user.id); + const wallet = await this.isValidWallet(user.id); // Check user's wallet if have or not if (!wallet) { @@ -55,18 +75,22 @@ export default class HomeView extends CommonView { this.loadEvent(); // Init data - this.loadData(getAllCategory); + this.loadData(); } } this.toggleLoaderSpinner(); } - async loadData(getAllCategory) { + async loadData() { await this.loadWalletUser(); - await this.loadCategory(getAllCategory); + await this.loadTransactionData(); + await this.loadCategory(this.getAllCategory); + this.loadSummaryTab(); + this.loadTransactionTab(); } + // ---------------------LOAD DATA---------------------// /** * Load wallet user */ @@ -86,16 +110,173 @@ export default class HomeView extends CommonView { } async updateAmountWallet(amount, saveWallet) { - this.wallet.amount += +amount; + this.wallet.amount += amount; + + if (amount > 0) { + this.wallet.inflow += amount; + } else { + this.wallet.outflow -= amount; + } await saveWallet(this.wallet); } - // ---------------------TRANSACTIONS DIALOG---------------------// - handlerEventTransactionDialog(saveTransaction) { + /** + * Load category data + * @param {function} getAllCategory Get all category function + */ + async loadCategory(getAllCategory) { + this.listCategory = await getAllCategory(); + + if (this.listCategory) { + this.renderCategoryItem(); + } + } + + loadSummaryTab() { + const inflowValue = document.querySelector('.inflow__text--income'); + const outflowValue = document.querySelector('.outflow__text--outcome'); + const totalValue = document.querySelector('.summary__total'); + + const { inflow } = this.wallet; + const { outflow } = this.wallet; + const total = inflow - outflow; + + inflowValue.textContent = `+$ ${formatNumber(inflow)}`; + outflowValue.textContent = `-$ ${formatNumber(outflow)}`; + totalValue.textContent = `${total >= 0 ? '+' : '-'}$ ${formatNumber( + Math.abs(total), + )}`; + } + + async loadTransactionData() { + this.listTransactions = await this.getAllTransactions(this.wallet.idUser); + } + + async loadTransactionTab() { + // Init data first + const transactionDetails = this.loadTransactionDetailsData(); + const transactionEl = document.querySelector('.transaction'); + const listTransactionDetailEl = + transactionEl.querySelector('.transaction__list'); + + const markup = []; + + if (transactionDetails) { + transactionDetails.forEach((transactionDetail) => { + markup.push(this.transactionDetailMarkup(transactionDetail)); + }); + } + + listTransactionDetailEl.innerHTML = ''; + listTransactionDetailEl.insertAdjacentHTML('afterbegin', markup.join('\n')); + } + + loadTransactionDetailsData() { + // Get all category user have in transactions + const listCategoryInTransaction = getAllCategoryNameInTransactions( + this.listTransactions, + ); + + // Create transactions details object + const tempList = []; + + listCategoryInTransaction.forEach((categoryName) => { + // Get category object from list category has been loaded. + const category = this.listCategory.filter( + (item) => item.name === categoryName, + ); + + const transactions = getAllTransactionByCategoryName( + categoryName, + this.listTransactions, + ); + + tempList.push( + createTransactionDetailObject( + Object.assign({}, ...category), + transactions, + ), + ); + }); + + tempList.sort( + (a, b) => + parseInt(b.transactions[0].id, 10) - parseInt(a.transactions[0].id, 10), + ); + + return tempList; + } + + transactionDetailMarkup(transactionDetail) { + const transactionTimeMarkup = () => { + const listMarkup = []; + transactionDetail.transactions.forEach((transaction) => { + const markup = ` +
+
+

${transaction.day}

+
+

+ ${transaction.fullDateString} +

+

${ + transaction.note === '' ? 'None' : transaction.note + }

+
+
+

${transaction.amount >= 0 ? '+' : '-'}$ ${formatNumber( + Math.abs(transaction.amount), + )}

+
+ `; + + listMarkup.push(markup); + }); + + return listMarkup.join('\n'); + }; + + return ` +
+
+
+
+ Transportation icon category +
+
+

+ ${transactionDetail.categoryName} +

+

${ + transactionDetail.totalTransaction + } Transactions

+
+
+

${ + transactionDetail.totalAmount >= 0 ? '+' : '-' + }$ ${formatNumber(Math.abs(transactionDetail.totalAmount))}

+
+
+ + ${transactionTimeMarkup()} +
+ `; + } + + // ---------------------END---------------------// + + // ---------------------TRANSACTION DIALOG---------------------// + handlerEventTransactionDialog() { this.transactionDialog.addEventListener('submit', (e) => { e.preventDefault(); - this.submitTransactionDialog(saveTransaction); + this.submitTransactionDialog(); }); this.transactionDialog.addEventListener('input', () => { @@ -116,26 +297,30 @@ export default class HomeView extends CommonView { }); } - async submitTransactionDialog(saveTransaction) { + async submitTransactionDialog() { this.toggleLoaderSpinner(); this.transactionDialog.close(); try { const transactionForm = document.getElementById('formAddTransaction'); - const dateEl = transactionForm.querySelector("[name='selected_date']"); - const categoryNameEl = transactionForm.querySelector( + const date = transactionForm.querySelector("[name='selected_date']"); + const categoryName = transactionForm.querySelector( "[name='category_name']", ); - const amountEl = transactionForm.querySelector("[name='amount']"); - const noteEl = transactionForm.querySelector("[name='note']"); + const amount = transactionForm.querySelector("[name='amount']"); + const note = transactionForm.querySelector("[name='note']"); const transaction = new Transaction({ - categoryName: categoryNameEl.value, - date: dateEl.value, - amount: +amountEl.value, - note: noteEl.value, + categoryName: categoryName.value, + date: changeDateFormat(date.value), + amount: -+amount.value, + note: note.value, + idUser: this.wallet.idUser, }); - await saveTransaction(transaction); + await this.saveTransaction(transaction); + + // Update wallet info + this.updateAmountWallet(-+amount.value, this.saveWallet); this.showSuccessToast( MESSAGE.ADD_TRANSACTION_SUCCESS, @@ -143,6 +328,9 @@ export default class HomeView extends CommonView { ); this.clearInputTransactionForm(transactionForm); + + // Reload data + this.loadData(); } catch (error) { this.showErrorToast(error); } @@ -164,18 +352,6 @@ export default class HomeView extends CommonView { // ---------------------END DIALOG---------------------// // ---------------------SELECTED CATEGORY DIALOG---------------------// - /** - * Load category data - * @param {function} getAllCategory Get all category function - */ - async loadCategory(getAllCategory) { - this.listCategory = await getAllCategory(); - - if (this.listCategory) { - this.renderCategoryItem(); - } - } - handlerEventCategoryDialog() { this.categoryDialog.addEventListener('input', () => { setTimeout(() => { @@ -232,11 +408,11 @@ export default class HomeView extends CommonView { // ---------------------END DIALOG---------------------// // ---------------------ADD BUDGET DIALOG---------------------// - addHandlerEventBudgetForm(saveTransaction, saveWallet) { + addHandlerEventBudgetForm() { this.budgetDialog.addEventListener('submit', (e) => { e.preventDefault(); - this.submitBudgetForm(saveTransaction, saveWallet); + this.submitBudgetForm(); }); this.budgetDialog.addEventListener('input', (e) => { @@ -245,7 +421,7 @@ export default class HomeView extends CommonView { }); } - async submitBudgetForm(saveTransaction, saveWallet) { + async submitBudgetForm() { try { this.toggleLoaderSpinner(); // Enable loader spinner this.budgetDialog.close(); // Close dialog @@ -261,13 +437,14 @@ export default class HomeView extends CommonView { date: changeDateFormat(date), amount: +amount, note, + idUser: this.wallet.idUser, }); - await saveTransaction(transaction); + await this.saveTransaction(transaction); - await this.updateAmountWallet(+amount, saveWallet); // Update wallet + await this.updateAmountWallet(+amount, this.saveWallet); // Update wallet - this.loadWalletUser(); + this.loadData(); // Hide loader spinner this.toggleLoaderSpinner(); @@ -298,11 +475,11 @@ export default class HomeView extends CommonView { // ---------------------END DIALOG---------------------// // ---------------------WALLET DIALOG--------------------- // - addHandlerEventWalletForm(saveWallet, getAllCategory) { + addHandlerEventWalletForm() { this.walletDialog.addEventListener('submit', (e) => { e.preventDefault(); - this.submitWalletForm(saveWallet, getAllCategory); + this.submitWalletForm(); }); this.walletDialog.addEventListener('input', (e) => { @@ -311,7 +488,7 @@ export default class HomeView extends CommonView { }); } - async submitWalletForm(saveWallet, getAllCategory) { + async submitWalletForm() { this.walletDialog.close(); this.toggleLoaderSpinner(); @@ -325,9 +502,10 @@ export default class HomeView extends CommonView { walletName, amount: +amount, idUser: this.user.id, + inflow: +amount, }); - await saveWallet(wallet); + await this.saveWallet(wallet); this.showSuccessToast( MESSAGE.ADD_WALLET_SUCCESS, @@ -335,7 +513,7 @@ export default class HomeView extends CommonView { ); this.loadEvent(); // Load event page - this.loadData(getAllCategory); // Load data from database into page + this.loadData(); // Load data from database into page } catch (error) { // Show toast error this.showErrorToast(error); diff --git a/javascript-practice/src/pages/index.html b/javascript-practice/src/pages/index.html index 52becf1..640b15c 100644 --- a/javascript-practice/src/pages/index.html +++ b/javascript-practice/src/pages/index.html @@ -251,93 +251,21 @@

Inflow

-

+$ 500,000,000.00

+

+$ 0.00

Outflow

-

-$ 100,000.00

+

$ 0.00

-

+$ 499,900,000.00

+

+$ 0.00

-
- - -
-
-
-
- Transportation icon category -
-
-

- Transportation -

-

1 Transactions

-
-
-

-$ 100,000.00

-
-
- -
-
-

18

-
-

- Friday, August 2023 -

-

Test Note

-
-
-

-$ 100,000.00

-
-
- -
-
-
-
- Transportation icon category -
-
-

- Transportation -

-

1 Transactions

-
-
-

- +$ 500,000,000.00 -

-
-
- -
-
-

18

-
-

- Friday, August 2023 -

-

Initial Balance

-
-
-

+$ 500,000,000.00

-
-
-
+
diff --git a/javascript-practice/src/styles/pages/_index.scss b/javascript-practice/src/styles/pages/_index.scss index 33fcb15..83fcd33 100644 --- a/javascript-practice/src/styles/pages/_index.scss +++ b/javascript-practice/src/styles/pages/_index.scss @@ -2,6 +2,7 @@ .background { height: 100vh; background: $gray; + overflow: auto; } .wrapper {