diff --git a/javascript-practice/src/assets/images/ic_category_foodndrink.png.png b/javascript-practice/src/assets/images/ic_category_foodndrink.png.png
deleted file mode 100644
index 8d61471..0000000
Binary files a/javascript-practice/src/assets/images/ic_category_foodndrink.png.png and /dev/null differ
diff --git a/javascript-practice/src/assets/images/ic_category_other_income.png.png b/javascript-practice/src/assets/images/ic_category_other_income.png.png
deleted file mode 100644
index 24d0828..0000000
Binary files a/javascript-practice/src/assets/images/ic_category_other_income.png.png and /dev/null differ
diff --git a/javascript-practice/src/assets/images/ic_category_transport.png.png b/javascript-practice/src/assets/images/ic_category_transport.png.png
deleted file mode 100644
index a7eaf5b..0000000
Binary files a/javascript-practice/src/assets/images/ic_category_transport.png.png and /dev/null differ
diff --git a/javascript-practice/src/assets/images/icon.png b/javascript-practice/src/assets/images/icon.png
deleted file mode 100644
index 519e341..0000000
Binary files a/javascript-practice/src/assets/images/icon.png and /dev/null differ
diff --git a/javascript-practice/src/assets/images/icon.webp b/javascript-practice/src/assets/images/icon.webp
new file mode 100644
index 0000000..a9d8434
Binary files /dev/null and b/javascript-practice/src/assets/images/icon.webp differ
diff --git a/javascript-practice/src/js/controllers/homeController.js b/javascript-practice/src/js/controllers/homeController.js
index e08905c..0740429 100644
--- a/javascript-practice/src/js/controllers/homeController.js
+++ b/javascript-practice/src/js/controllers/homeController.js
@@ -1,3 +1,4 @@
+import Transform from '../helpers/transform';
import UserService from '../services/userService';
export default class HomeController {
@@ -49,16 +50,13 @@ export default class HomeController {
this.handlerSaveTransaction.bind(this),
HomeController.handlerClearAccessToken.bind(this),
this.handlerDeleteTransaction.bind(this),
+ new Transform(),
);
- this.homeView.addHandlerEventWalletForm();
this.homeView.loadPage();
- this.homeView.addHandlerEventBudgetForm();
-
- this.homeView.handlerEventCategoryDialog();
-
- this.homeView.handlerEventTransactionDialog();
+ // Subscribe listener data update
+ this.homeView.subscribeListenerData();
}
}
}
diff --git a/javascript-practice/src/js/helpers/dataProcess.js b/javascript-practice/src/js/helpers/dataProcess.js
new file mode 100644
index 0000000..a276218
--- /dev/null
+++ b/javascript-practice/src/js/helpers/dataProcess.js
@@ -0,0 +1,76 @@
+import { changeDateFormat } from './helpers';
+
+export const createId = () => {
+ return new Date().getTime();
+};
+
+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.map((transaction) => {
+ const dateParts = changeDateFormat(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,
+ };
+
+ return 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;
+};
+
+export const convertModelToDataObject = (model) => {
+ const { id, ...data } = model;
+
+ return { id, data };
+};
+
+export const convertDataObjectToModel = (data) => {
+ const { id, ...object } = data;
+
+ return { id, ...object.data };
+};
diff --git a/javascript-practice/src/js/helpers/helpers.js b/javascript-practice/src/js/helpers/helpers.js
index 7027169..edc2eed 100644
--- a/javascript-practice/src/js/helpers/helpers.js
+++ b/javascript-practice/src/js/helpers/helpers.js
@@ -1,24 +1,7 @@
import * as MESSAGE from '../constants/message';
-import { TIME_OUT_SEC, REGEX, DAY, MONTH } from '../constants/config';
+import { TIME_OUT_SEC, DAY, MONTH } from '../constants/config';
import FirebaseService from '../services/firebaseService';
-/**
- * Validate password
- * @param {string} password Password input
- * @returns {boolean} Return true if validate password success, otherwise return false
- */
-export const isValidPassword = (password) => {
- return REGEX.PASSWORD.test(password);
-};
-
-export const isValidateEmail = (email) => {
- return String(email).toLowerCase().match(REGEX.EMAIL);
-};
-
-export const compare2Password = (password, passwordConfirm) => {
- return password === passwordConfirm;
-};
-
/**
* A waiting function with s second
* @param {number} s The time will be waiting
@@ -59,18 +42,6 @@ export const createToken = () => {
return token;
};
-export const convertModelToDataObject = (model) => {
- const { id, ...data } = model;
-
- return { id, data };
-};
-
-export const convertDataObjectToModel = (data) => {
- const { id, ...object } = data;
-
- return { id, ...object.data };
-};
-
export const getSubdirectoryURL = () => {
const url = window.location.href;
const parts = url.split('/'); // Results: ['http:', '', 'example.com', '']
@@ -101,73 +72,6 @@ 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.map((transaction) => {
- const dateParts = changeDateFormat(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,
- };
-
- return 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;
-};
-
-export const renderRequiredText = (field, element) => {
- const markup = `
-
${MESSAGE.REQUIRED_MESSAGE(field)}
- `;
-
- element.insertAdjacentHTML('afterend', markup);
-};
-
export const redirectToLoginPage = () => {
window.location.replace('/login');
};
diff --git a/javascript-practice/src/js/helpers/transform.js b/javascript-practice/src/js/helpers/transform.js
new file mode 100644
index 0000000..f989c48
--- /dev/null
+++ b/javascript-practice/src/js/helpers/transform.js
@@ -0,0 +1,26 @@
+export default class Transform {
+ constructor() {
+ this.signal = {};
+ }
+
+ onSendSignal(fromClass, value) {
+ Object.keys(this.signal).forEach((key) => {
+ const item = this.signal[key];
+
+ // Same receiver
+ if (item.name === fromClass) {
+ return;
+ }
+
+ // Send signal
+ const { handler } = this.signal[key];
+ if (handler) {
+ handler(value);
+ }
+ });
+ }
+
+ create(className, handler = null) {
+ this.signal[className] = { name: className, handler };
+ }
+}
diff --git a/javascript-practice/src/js/helpers/validateForm.js b/javascript-practice/src/js/helpers/validateForm.js
new file mode 100644
index 0000000..bf1a4f4
--- /dev/null
+++ b/javascript-practice/src/js/helpers/validateForm.js
@@ -0,0 +1,27 @@
+import { REGEX } from '../constants/config';
+import * as MESSAGE from '../constants/message';
+
+/**
+ * Validate password
+ * @param {string} password Password input
+ * @returns {boolean} Return true if validate password success, otherwise return false
+ */
+export const isValidPassword = (password) => {
+ return REGEX.PASSWORD.test(password);
+};
+
+export const isValidateEmail = (email) => {
+ return String(email).toLowerCase().match(REGEX.EMAIL);
+};
+
+export const compare2Password = (password, passwordConfirm) => {
+ return password === passwordConfirm;
+};
+
+export const renderRequiredText = (field, element) => {
+ const markup = `
+ ${MESSAGE.REQUIRED_MESSAGE(field)}
+ `;
+
+ element.insertAdjacentHTML('afterend', markup);
+};
diff --git a/javascript-practice/src/js/models/transaction.js b/javascript-practice/src/js/models/transaction.js
index c6ff86f..9da4900 100644
--- a/javascript-practice/src/js/models/transaction.js
+++ b/javascript-practice/src/js/models/transaction.js
@@ -1,21 +1,12 @@
+import { createId } from '../helpers/dataProcess';
+
export default class Transaction {
- constructor({
- id = '',
- categoryName = '',
- date = '',
- note = '',
- amount = 0,
- idUser,
- }) {
- this.id = id === '' ? Transaction.createIdTransactions() : id;
+ constructor({ id, categoryName, date, note, amount = 0, idUser }) {
+ this.id = !id ? createId() : id;
this.categoryName = categoryName;
this.date = date;
this.note = note;
this.amount = amount;
this.idUser = idUser;
}
-
- static createIdTransactions() {
- return new Date().getTime();
- }
}
diff --git a/javascript-practice/src/js/models/user.js b/javascript-practice/src/js/models/user.js
index b3ed5e7..f8005c6 100644
--- a/javascript-practice/src/js/models/user.js
+++ b/javascript-practice/src/js/models/user.js
@@ -1,12 +1,10 @@
+import { createId } from '../helpers/dataProcess';
+
export default class User {
constructor({ email, password, accessToken = '' }) {
- this.id = User.createIdUser();
+ this.id = createId();
this.email = email;
this.password = password;
this.accessToken = accessToken;
}
-
- static createIdUser() {
- return new Date().getTime();
- }
}
diff --git a/javascript-practice/src/js/models/wallet.js b/javascript-practice/src/js/models/wallet.js
index 44d5a6e..3ab711d 100644
--- a/javascript-practice/src/js/models/wallet.js
+++ b/javascript-practice/src/js/models/wallet.js
@@ -1,13 +1,15 @@
+import { createId } from '../helpers/dataProcess';
+
export default class Wallet {
- constructor({ walletName = '', inflow = 0, outflow = 0, idUser }) {
- this.id = Wallet.createIdWallet();
+ constructor({ walletName, inflow = 0, outflow = 0, idUser }) {
+ this.id = createId();
this.walletName = walletName;
this.inflow = inflow;
this.outflow = outflow;
this.idUser = idUser;
}
- static createIdWallet() {
- return new Date().getTime();
+ amountWallet() {
+ return +this.inflow + +this.outflow;
}
}
diff --git a/javascript-practice/src/js/services/commonService.js b/javascript-practice/src/js/services/commonService.js
index eb3d611..bb9648d 100644
--- a/javascript-practice/src/js/services/commonService.js
+++ b/javascript-practice/src/js/services/commonService.js
@@ -1,8 +1,8 @@
+import { timeOutConnect } from '../helpers/helpers';
import {
convertDataObjectToModel,
convertModelToDataObject,
- timeOutConnect,
-} from '../helpers/helpers';
+} from '../helpers/dataProcess';
import FirebaseService from './firebaseService';
export default class CommonService {
diff --git a/javascript-practice/src/js/services/walletService.js b/javascript-practice/src/js/services/walletService.js
index 7092e11..9e19b7a 100644
--- a/javascript-practice/src/js/services/walletService.js
+++ b/javascript-practice/src/js/services/walletService.js
@@ -1,3 +1,4 @@
+import Wallet from '../models/wallet';
import CommonService from './commonService';
export default class WalletService extends CommonService {
@@ -34,6 +35,10 @@ export default class WalletService extends CommonService {
async getWalletByIdUser(idUser) {
const result = await this.getDataFromProp('idUser', idUser);
- return result || null;
+ if (result) {
+ return new Wallet(result);
+ }
+
+ return null;
}
}
diff --git a/javascript-practice/src/js/views/authenticationView.js b/javascript-practice/src/js/views/authenticationView.js
index 29a3fbc..105a690 100644
--- a/javascript-practice/src/js/views/authenticationView.js
+++ b/javascript-practice/src/js/views/authenticationView.js
@@ -5,7 +5,7 @@ import {
isValidPassword,
isValidateEmail,
renderRequiredText,
-} from '../helpers/helpers';
+} from '../helpers/validateForm';
export default class AuthenticationView extends CommonView {
constructor() {
diff --git a/javascript-practice/src/js/views/budgetView.js b/javascript-practice/src/js/views/budgetView.js
new file mode 100644
index 0000000..20140fe
--- /dev/null
+++ b/javascript-practice/src/js/views/budgetView.js
@@ -0,0 +1,174 @@
+import { DEFAULT_CATEGORY } from '../constants/config';
+import Transaction from '../models/transaction';
+import * as MESSAGE from '../constants/message';
+import { renderRequiredText } from '../helpers/validateForm';
+
+class BudgetView {
+ constructor() {
+ this.budgetDialog = document.getElementById('budgetDialog');
+ this.addBudgetBtn = document.getElementById('addBudget');
+
+ this.handlerEventBudgetView();
+ }
+
+ initFunction(
+ showErrorToast,
+ showSuccessToast,
+ toggleLoaderSpinner,
+ saveTransaction,
+ loadTransactionData,
+ updateAmountWallet,
+ loadData,
+ transform,
+ ) {
+ this.showErrorToast = showErrorToast;
+ this.showSuccessToast = showSuccessToast;
+ this.toggleLoaderSpinner = toggleLoaderSpinner;
+ this.saveTransaction = saveTransaction;
+ this.loadTransactionData = loadTransactionData;
+ this.updateAmountWallet = updateAmountWallet;
+ this.loadData = loadData;
+ this.transform = transform;
+ }
+
+ subscribe() {
+ this.transform.create('budgetView', this.updateData.bind(this));
+ }
+
+ sendData() {
+ const data = { wallet: this.wallet, listTransaction: this.listTransaction };
+
+ this.transform.onSendSignal('budgetView', data);
+ }
+
+ updateData(data) {
+ if (data.wallet) this.wallet = data.wallet;
+
+ if (data.listTransactions) this.listTransaction = data.listTransactions;
+
+ if (data.listCategory) this.listCategory = data.listCategory;
+ }
+
+ addHandlerEventBudgetForm() {
+ this.budgetDialog.addEventListener('submit', (e) => {
+ e.preventDefault();
+
+ this.clearErrorStyleBudgetForm();
+ this.submitBudgetForm();
+ });
+
+ this.budgetDialog.addEventListener('input', () => {
+ this.changeBtnStyle();
+ this.clearErrorStyleBudgetForm();
+ });
+ }
+
+ clearErrorStyleBudgetForm() {
+ // Clear error style
+ const inputFieldEls =
+ this.budgetDialog.querySelectorAll('.form__input-field');
+ const errorTexts = this.budgetDialog.querySelectorAll('.error-text');
+
+ inputFieldEls.forEach((item) => {
+ if (item.classList.contains('error-input'))
+ item.classList.remove('error-input');
+ });
+
+ errorTexts.forEach((item) => {
+ if (item) item.remove();
+ });
+ }
+
+ async submitBudgetForm() {
+ try {
+ const { formAddBudget } = document.forms;
+ const form = new FormData(formAddBudget);
+ const date = form.get('selected_date');
+ const amount = form.get('amount');
+ const note = form.get('note');
+
+ // Validate data user input
+
+ if (this.validateBudgetForm(date, amount)) {
+ this.toggleLoaderSpinner(); // Enable loader spinner
+ this.budgetDialog.close(); // Close dialog
+
+ const transaction = new Transaction({
+ categoryName: DEFAULT_CATEGORY.INCOME,
+ date,
+ amount: +amount,
+ note,
+ idUser: this.wallet.idUser,
+ });
+
+ await this.saveTransaction(transaction);
+
+ // Reload data
+ await this.loadTransactionData();
+ await this.updateAmountWallet();
+ await this.loadData();
+
+ // Hide loader spinner
+ this.toggleLoaderSpinner();
+
+ // Show success message
+ this.showSuccessToast(
+ MESSAGE.ADD_TRANSACTION_SUCCESS,
+ MESSAGE.DEFAULT_MESSAGE,
+ );
+
+ document.getElementById('formAddBudget').reset();
+ }
+ } catch (error) {
+ this.showErrorToast(error);
+ }
+ }
+
+ validateBudgetForm(date, amount) {
+ const inputFieldEl =
+ this.budgetDialog.querySelectorAll('.form__input-field');
+
+ if (!date || !amount) {
+ if (!date) {
+ renderRequiredText('date', inputFieldEl[0]);
+ inputFieldEl[0].classList.add('error-input');
+ }
+
+ if (!amount) {
+ renderRequiredText('amount', inputFieldEl[1]);
+ inputFieldEl[1].classList.add('error-input');
+ }
+
+ return false;
+ }
+
+ return true;
+ }
+
+ changeBtnStyle() {
+ const date = this.budgetDialog.querySelector('.input-date').value;
+ const amount = +this.budgetDialog.querySelector('.form__input-balance')
+ .value;
+ const saveBtn = this.budgetDialog.querySelector('.form__save-btn');
+
+ if (date.trim() && amount >= 1) {
+ saveBtn.classList.add('active');
+ } else {
+ saveBtn.classList.remove('active');
+ }
+ }
+
+ handlerEventBudgetView() {
+ this.addBudgetBtn.addEventListener('click', () => {
+ // Set default value for date input
+ this.budgetDialog.querySelector("[name='selected_date']").valueAsDate =
+ new Date();
+
+ this.budgetDialog.showModal();
+ });
+
+ this.addHandlerEventBudgetForm();
+ }
+}
+
+export default new BudgetView();
diff --git a/javascript-practice/src/js/views/categoryView.js b/javascript-practice/src/js/views/categoryView.js
new file mode 100644
index 0000000..b0e7161
--- /dev/null
+++ b/javascript-practice/src/js/views/categoryView.js
@@ -0,0 +1,136 @@
+import { REMOVE_CATEGORY } from '../constants/config';
+
+class CategoryView {
+ constructor() {
+ this.categoryDialog = document.getElementById('categoryDialog');
+ this.categoryField = document.getElementById('selectCategory');
+ this.closeIcon = document.querySelector('.close-icon');
+
+ this.handlerEventCategoryDialog();
+ this.addEventSelectCategoryDialog();
+ }
+
+ initFunction(getAllCategory, transform) {
+ this.getAllCategory = getAllCategory;
+ this.transform = transform;
+ }
+
+ sendData() {
+ const data = { listCategory: this.listCategory };
+
+ this.transform.onSendSignal('categoryView', data);
+ }
+
+ handlerEventCategoryDialog() {
+ this.categoryDialog.addEventListener('input', () => {
+ setTimeout(() => {
+ this.searchCategory();
+ }, 300);
+ });
+ }
+
+ /**
+ * Load category data
+ * @param {function} getAllCategory Get all category function
+ */
+ async loadCategory() {
+ if (!this.listCategory) {
+ this.listCategory = await this.getAllCategory();
+
+ this.sendData();
+ }
+
+ if (this.listCategory) {
+ this.renderCategoryList();
+ }
+ }
+
+ searchCategory() {
+ const searchCategoryEl =
+ this.categoryDialog.querySelector("[name='category']");
+ const searchValue = searchCategoryEl.value.trim().toLowerCase();
+ let newListCategory = [];
+
+ if (searchValue) {
+ this.listCategory.forEach((category) => {
+ const categoryName = category.name.trim().toLowerCase();
+
+ if (categoryName.includes(searchValue)) {
+ newListCategory.unshift(category);
+ }
+ });
+ } else {
+ newListCategory = this.listCategory;
+ }
+
+ // Render category item
+ this.renderCategoryList(this.keySearchCategory, newListCategory);
+ }
+
+ renderCategoryList(categorySelected, listCategory = this.listCategory) {
+ // Remove category name unnecessary
+ const newListCategory = listCategory.filter(
+ (item) => !REMOVE_CATEGORY.includes(item.name),
+ );
+
+ const listCategoryEl = document.querySelector('.list-category');
+
+ listCategoryEl.innerHTML = ''; // Remove old category item
+
+ newListCategory.forEach((category) => {
+ const markup = `
+
+

+
${category.name}
+
+ `;
+
+ listCategoryEl.insertAdjacentHTML('afterbegin', markup);
+ });
+ }
+
+ addEventSelectCategoryDialog() {
+ const categoryListEl = document.querySelector('.list-category');
+ const categoryIconEl = this.categoryField.querySelector('.category-icon');
+ const categoryNameEl = this.categoryField.querySelector('.category-name');
+
+ categoryListEl.addEventListener('click', (e) => {
+ const categoryItem = e.target.closest('.category-item');
+
+ if (categoryItem) {
+ const { url } = categoryItem.dataset;
+ const { value } = categoryItem.dataset;
+
+ // Set url and value into category field in transaction dialog
+ categoryIconEl.src = url;
+ categoryNameEl.value = value;
+
+ // Close select category dialog
+ this.categoryDialog.close();
+ }
+ });
+
+ // Pass value selected to category dialog
+ categoryNameEl.addEventListener('click', () => {
+ if (categoryNameEl.value) {
+ // Make the keyword search category name into global
+ this.keySearchCategory = categoryNameEl.value;
+
+ this.renderCategoryList(this.keySearchCategory);
+ }
+ this.categoryDialog.showModal();
+ });
+
+ this.closeIcon.addEventListener('click', () => {
+ this.categoryDialog.close();
+ });
+ }
+}
+
+export default new CategoryView();
diff --git a/javascript-practice/src/js/views/homeView.js b/javascript-practice/src/js/views/homeView.js
index 618c17e..450b8d9 100644
--- a/javascript-practice/src/js/views/homeView.js
+++ b/javascript-practice/src/js/views/homeView.js
@@ -1,23 +1,15 @@
/* eslint-disable class-methods-use-this */
-import {
- TYPE_TOAST,
- BTN_CONTENT,
- DEFAULT_CATEGORY,
- FIRST_ADD_WALLET_NOTE,
- REMOVE_CATEGORY,
-} from '../constants/config';
+import { TYPE_TOAST, BTN_CONTENT } from '../constants/config';
import * as MESSAGE from '../constants/message';
import CommonView from './commonView';
-import Wallet from '../models/wallet';
-import Transaction from '../models/transaction';
-import {
- createTransactionDetailObject,
- formatNumber,
- getAllCategoryNameInTransactions,
- getAllTransactionByCategoryName,
- renderRequiredText,
-} from '../helpers/helpers';
-import defaultCategoryIcon from '../../assets/images/question-icon.svg';
+import { formatNumber } from '../helpers/helpers';
+
+import WalletView from './walletView';
+import TransactionView from './transactionView';
+import CategoryView from './categoryView';
+import BudgetView from './budgetView';
+import SummaryTabView from './summaryTabView';
+import TransactionTabView from './transactionTabView';
export default class HomeView extends CommonView {
constructor() {
@@ -29,19 +21,9 @@ export default class HomeView extends CommonView {
this.saveBtns = document.querySelectorAll('.form__save-btn');
this.dialogs = document.querySelectorAll('.dialog');
- this.closeIcon = document.querySelector('.close-icon');
-
- this.categoryField = document.getElementById('selectCategory');
- this.addBudgetBtn = document.getElementById('addBudget');
- this.addTransactionBtn = document.getElementById('addTransaction');
this.amountInputs = document.querySelectorAll('.form__input-balance');
- this.budgetDialog = document.getElementById('budgetDialog');
this.transactionDialog = document.getElementById('transactionDialog');
- this.categoryDialog = document.getElementById('categoryDialog');
- this.walletDialog = document.getElementById('walletDialog');
-
- this.transactionForm = document.getElementById('formAddTransaction');
}
initFunction(
@@ -53,6 +35,7 @@ export default class HomeView extends CommonView {
saveTransaction,
clearAccessToken,
deleteTransaction,
+ transform,
) {
this.getInfoUserLogin = getInfoUserLogin;
this.getWalletByIdUser = getWalletByIdUser;
@@ -62,11 +45,97 @@ export default class HomeView extends CommonView {
this.saveTransaction = saveTransaction;
this.clearAccessToken = clearAccessToken;
this.deleteTransaction = deleteTransaction;
+
+ this.transform = transform;
+
+ this.initTransactionViewFunction();
+ this.initCategoryViewFunction();
+ this.initBudgetViewFunction();
+ this.initSummaryTabViewFunction();
+ this.initTransactionTabViewFunction();
+ this.initWalletViewFunction();
+ }
+
+ initCategoryViewFunction() {
+ CategoryView.initFunction(this.getAllCategory, this.transform);
+ }
+
+ initTransactionViewFunction() {
+ TransactionView.initFunction(
+ this.toggleLoaderSpinner.bind(this),
+ this.deleteTransaction,
+ this.loadTransactionData.bind(this),
+ this.updateAmountWallet.bind(this),
+ this.loadData.bind(this),
+ this.showSuccessToast.bind(this),
+ this.showErrorToast.bind(this),
+ this.saveTransaction,
+ this.transform,
+ );
+ }
+
+ initBudgetViewFunction() {
+ BudgetView.initFunction(
+ this.showErrorToast.bind(this),
+ this.showSuccessToast.bind(this),
+ this.toggleLoaderSpinner.bind(this),
+ this.saveTransaction.bind(this),
+ this.loadTransactionData.bind(this),
+ this.updateAmountWallet.bind(this),
+ this.loadData.bind(this),
+ this.transform,
+ );
+ }
+
+ initFunctionCategoryView() {
+ CategoryView.initFunction(this.getAllCategory, this.transform);
+ }
+
+ initSummaryTabViewFunction() {
+ SummaryTabView.initFunction(this.transform);
+ }
+
+ initTransactionTabViewFunction() {
+ TransactionTabView.initFunction(this.transform);
+ }
+
+ initWalletViewFunction() {
+ WalletView.initFunction(
+ this.transform,
+ this.toggleLoaderSpinner.bind(this),
+ this.saveWallet,
+ this.saveTransaction,
+ this.loadTransactionData.bind(this),
+ this.loadData.bind(this),
+ this.loadEvent.bind(this),
+ this.showSuccessToast.bind(this),
+ this.showErrorToast.bind(this),
+ );
+ }
+
+ subscribeListenerData() {
+ this.subscribe();
+ TransactionView.subscribe();
+ BudgetView.subscribe();
+ SummaryTabView.subscribe();
+ TransactionTabView.subscribe();
+ WalletView.subscribe();
+ }
+
+ async loadData() {
+ // Send data to other class
+ this.sendData();
+
+ await CategoryView.loadCategory();
+
+ await this.loadWalletUser();
+
+ SummaryTabView.load();
+
+ TransactionTabView.loadTransactionTab();
}
async loadPage() {
- // Init function
-
this.toggleLoaderSpinner();
const user = await this.getInfoUserLogin();
@@ -77,10 +146,11 @@ export default class HomeView extends CommonView {
this.user = user;
this.wallet = await this.getWalletByIdUser(user.id);
+ this.sendData();
// Check user's wallet if have or not
if (!this.wallet) {
// Show add wallet dialog
- this.walletDialog.showModal();
+ WalletView.showDialog();
} else {
// Init data
await this.loadTransactionData();
@@ -94,13 +164,30 @@ export default class HomeView extends CommonView {
this.toggleLoaderSpinner();
}
- async loadData() {
- await this.loadWalletUser();
- this.loadSummaryTab();
- await this.loadTransactionTab();
+ // ---------------------LOAD DATA---------------------//
+
+ subscribe() {
+ this.transform.create('homeView', this.updateData.bind(this));
+ }
+
+ sendData() {
+ const data = {
+ wallet: this.wallet,
+ listTransactions: this.listTransactions,
+ user: this.user,
+ };
+
+ this.transform.onSendSignal('homeView', data);
+ }
+
+ updateData(data) {
+ if (data.wallet) this.wallet = data.wallet;
+
+ if (data.listTransactions) this.listTransaction = data.listTransactions;
+
+ if (data.listCategory) this.listCategory = data.listCategory;
}
- // ---------------------LOAD DATA---------------------//
/**
* Load wallet user
*/
@@ -111,7 +198,7 @@ export default class HomeView extends CommonView {
const walletName = document.querySelector('.wallet__name');
const walletPrice = document.querySelector('.wallet__price');
const walletNameValue = wallet.walletName;
- const walletAmountValue = wallet.inflow + wallet.outflow;
+ const walletAmountValue = wallet.amountWallet();
const sign = walletAmountValue >= 0 ? '+' : '-';
@@ -131,7 +218,7 @@ export default class HomeView extends CommonView {
let outflow = 0;
// Init data first
- this.transactionDetails = this.loadTransactionDetailsData();
+ this.transactionDetails = TransactionTabView.loadTransactionDetailsData();
this.transactionDetails.forEach((transaction) => {
if (transaction.totalAmount >= 0) {
@@ -148,679 +235,14 @@ export default class HomeView extends CommonView {
await this.saveWallet(this.wallet);
}
- /**
- * Load category data
- * @param {function} getAllCategory Get all category function
- */
- async loadCategory() {
- if (!this.listCategory) {
- this.listCategory = await this.getAllCategory();
- }
-
- if (this.listCategory) {
- this.renderCategoryList();
- }
- }
-
- 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(Math.abs(outflow))}`;
- totalValue.textContent = `${total >= 0 ? '+' : '-'}$ ${formatNumber(
- Math.abs(total),
- )}`;
- }
-
async loadTransactionData() {
this.listTransactions = await this.getAllTransactions(this.wallet.idUser);
- }
- async loadTransactionTab() {
- // Load category
- await this.loadCategory();
-
- // Init data first
- this.transactionDetails = this.loadTransactionDetailsData();
-
- const transactionEl = document.querySelector('.transaction');
- const listTransactionDetailEl =
- transactionEl.querySelector('.transaction__list');
-
- const markup = [];
-
- if (this.transactionDetails) {
- this.transactionDetails.forEach((transactionDetail) => {
- markup.push(this.transactionDetailMarkup(transactionDetail));
- });
- }
-
- listTransactionDetailEl.innerHTML = '';
- listTransactionDetailEl.insertAdjacentHTML('afterbegin', markup.join('\n'));
-
- // Load event
- this.addEventTransactionItem();
- }
-
- loadTransactionDetailsData() {
- // Get all category user have in transactions
- const listCategoryInTransaction = getAllCategoryNameInTransactions(
- this.listTransactions,
- );
-
- // Create transactions details object
- const tempList = listCategoryInTransaction.map((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,
- );
-
- return 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 `
-
-
-
-
-

-
-
-
- ${transactionDetail.categoryName}
-
-
${
- transactionDetail.totalTransaction
- } Transactions
-
-
-
${
- transactionDetail.totalAmount >= 0 ? '+' : '-'
- }$ ${formatNumber(Math.abs(transactionDetail.totalAmount))}
-
-
-
- ${transactionTimeMarkup()}
-
- `;
+ this.sendData();
}
// ---------------------END---------------------//
- // ---------------------TRANSACTION DIALOG---------------------//
- handlerEventTransactionDialog() {
- this.transactionDialog.addEventListener('submit', (e) => {
- e.preventDefault();
- this.clearErrorTransactionDialog();
- this.submitTransactionDialog();
- });
-
- this.transactionDialog.addEventListener('input', () => {
- this.changeBtnStyleTransactionDialog();
- this.clearErrorTransactionDialog();
- });
-
- // Add delete transaction event
- this.deleteTransactionEvent();
- }
-
- clearErrorTransactionDialog() {
- // Clear error style
- const inputFieldEls =
- this.transactionDialog.querySelectorAll('.form__input-field');
- const errorTextEls = this.transactionDialog.querySelectorAll('.error-text');
-
- inputFieldEls.forEach((item) => {
- if (item.classList.contains('error-input'))
- item.classList.remove('error-input');
- });
- errorTextEls.forEach((item) => {
- if (item) item.remove();
- });
- }
-
- deleteTransactionEvent() {
- const deleteTransactionBtn =
- this.transactionDialog.querySelector('.form__delete-btn');
-
- deleteTransactionBtn.addEventListener('click', async () => {
- const idEl = this.transactionDialog.querySelector(
- "[name='id_transaction']",
- );
-
- if (idEl.value) {
- try {
- this.transactionDialog.close();
- this.toggleLoaderSpinner();
-
- await this.deleteTransaction(idEl.value);
-
- // Reload data
- await this.loadTransactionData();
- await this.updateAmountWallet();
- await this.loadData();
-
- this.showSuccessToast('Delete success!', MESSAGE.DEFAULT_MESSAGE);
- } catch (error) {
- this.showErrorToast(error);
- }
- this.toggleLoaderSpinner();
- }
- });
- }
-
- changeBtnStyleTransactionDialog() {
- const dateInput = this.transactionDialog.querySelector(
- "[name='selected_date']",
- ).value;
- const categoryName = this.transactionDialog.querySelector(
- "[name='category_name']",
- ).value;
- const amountInput =
- this.transactionDialog.querySelector("[name='amount']").value;
- const saveBtn = this.transactionDialog.querySelector('.form__save-btn');
-
- saveBtn.classList.toggle(
- 'active',
- dateInput && categoryName && amountInput >= 1,
- );
- }
-
- initValueTransactionDialog(idTransaction) {
- let categoryName;
-
- if (idTransaction) {
- // Get transaction object
- const transactionArr = this.listTransactions.filter(
- (obj) => obj.id === idTransaction,
- );
- const transaction = Object.assign({}, ...transactionArr);
- // Get category object
- const categoryArr = this.listCategory.filter(
- (obj) => obj.name === transaction.categoryName,
- );
- const category = Object.assign({}, ...categoryArr);
-
- const idEl = this.transactionDialog.querySelector(
- "[name='id_transaction']",
- );
- const dateEl = this.transactionDialog.querySelector(
- "[name='selected_date']",
- );
- const categoryEl = this.transactionDialog.querySelector(
- "[name='category_name']",
- );
- const amountEl = this.transactionDialog.querySelector("[name='amount']");
- const noteEl = this.transactionDialog.querySelector("[name='note']");
- const iconEl = this.transactionDialog.querySelector('.category-icon');
-
- idEl.value = transaction.id;
- dateEl.value = transaction.date;
- categoryEl.value = transaction.categoryName;
- amountEl.value = Math.abs(transaction.amount);
- noteEl.value = transaction.note;
- iconEl.src = category.url;
-
- categoryName = categoryEl.value;
- }
-
- // Show delete button only if it is a edit form and not a income transaction
- const deleteBtn = this.transactionDialog.querySelector('.form__delete-btn');
- const showDeleteBtn = () => {
- return idTransaction && categoryName !== 'Income';
- };
-
- deleteBtn.classList.toggle('hide', !showDeleteBtn());
- }
-
- async submitTransactionDialog() {
- try {
- const dateEl = this.transactionForm.querySelector(
- "[name='selected_date']",
- );
- const categoryNameEl = this.transactionForm.querySelector(
- "[name='category_name']",
- );
- const amountEl = this.transactionForm.querySelector("[name='amount']");
- const noteEl = this.transactionForm.querySelector("[name='note']");
- const idEl = this.transactionForm.querySelector(
- "[name='id_transaction']",
- );
- const amount =
- categoryNameEl.value === 'Income' ? +amountEl.value : -+amountEl.value; // Check if this is a income transaction, amount must plus
-
- // Validate data user input
- if (
- this.validateTransactionForm(dateEl.value, categoryNameEl.value, amount)
- ) {
- this.toggleLoaderSpinner();
- this.transactionDialog.close();
-
- const transaction = new Transaction({
- id: idEl.value,
- categoryName: categoryNameEl.value,
- date: dateEl.value,
- amount,
- note: noteEl.value,
- idUser: this.wallet.idUser,
- });
-
- await this.saveTransaction(transaction);
-
- // Reload data
- await this.loadTransactionData();
- this.updateAmountWallet();
- await this.loadData();
-
- if (!idEl.value) {
- // Add success
- this.showSuccessToast(
- MESSAGE.ADD_TRANSACTION_SUCCESS,
- MESSAGE.DEFAULT_MESSAGE,
- );
- } else {
- // Update success
- this.showSuccessToast(
- MESSAGE.UPDATE_TRANSACTION_SUCCESS,
- MESSAGE.DEFAULT_MESSAGE,
- );
- }
-
- this.toggleLoaderSpinner();
- this.clearInputTransactionForm(this.transactionForm);
- }
- } catch (error) {
- this.showErrorToast(error);
- this.toggleLoaderSpinner();
- }
- }
-
- validateTransactionForm(date, categoryName, amount) {
- const inputFieldEls =
- this.transactionDialog.querySelectorAll('.form__input-field');
-
- if (!date || !categoryName || !amount) {
- if (!date) {
- renderRequiredText('date', inputFieldEls[0]);
- inputFieldEls[0].classList.add('error-input');
- }
-
- if (!categoryName) {
- renderRequiredText('category', inputFieldEls[1]);
- inputFieldEls[1].classList.add('error-input');
- }
-
- if (!amount) {
- renderRequiredText('amount', inputFieldEls[2]);
- inputFieldEls[2].classList.add('error-input');
- }
-
- return false;
- }
-
- return true;
- }
-
- clearInputTransactionForm() {
- const categoryIcon = this.transactionForm.querySelector('.category-icon');
-
- categoryIcon.src = defaultCategoryIcon;
- this.keySearchCategory = null; // Delete keyword search
- this.renderCategoryList(this.keySearchCategory);
- this.transactionForm.reset();
- }
- // ---------------------END DIALOG---------------------//
-
- // ---------------------SELECTED CATEGORY DIALOG---------------------//
- handlerEventCategoryDialog() {
- this.categoryDialog.addEventListener('input', () => {
- setTimeout(() => {
- this.searchCategory();
- }, 300);
- });
- }
-
- searchCategory() {
- const searchCategoryEl =
- this.categoryDialog.querySelector("[name='category']");
- const searchValue = searchCategoryEl.value.trim().toLowerCase();
- let newListCategory = [];
-
- if (searchValue) {
- this.listCategory.forEach((category) => {
- const categoryName = category.name.trim().toLowerCase();
-
- if (categoryName.includes(searchValue)) {
- newListCategory.unshift(category);
- }
- });
- } else {
- newListCategory = this.listCategory;
- }
-
- // Render category item
- this.renderCategoryList(this.keySearchCategory, newListCategory);
- }
-
- renderCategoryList(categorySelected, listCategory = this.listCategory) {
- // Remove category name unnecessary
- const newListCategory = listCategory.filter(
- (item) => !REMOVE_CATEGORY.includes(item.name),
- );
-
- const listCategoryEl = document.querySelector('.list-category');
-
- listCategoryEl.innerHTML = ''; // Remove old category item
-
- newListCategory.forEach((category) => {
- const markup = `
-
-

-
${category.name}
-
- `;
-
- listCategoryEl.insertAdjacentHTML('afterbegin', markup);
- });
- }
-
- // ---------------------END DIALOG---------------------//
-
- // ---------------------ADD BUDGET DIALOG---------------------//
- addHandlerEventBudgetForm() {
- this.budgetDialog.addEventListener('submit', (e) => {
- e.preventDefault();
-
- this.clearErrorStyleBudgetForm();
- this.submitBudgetForm();
- });
-
- this.budgetDialog.addEventListener('input', (e) => {
- const bodyDialog = e.target.closest('.dialog__body');
-
- this.changeBtnStyle(bodyDialog);
- this.clearErrorStyleBudgetForm();
- });
- }
-
- clearErrorStyleBudgetForm() {
- // Clear error style
- const inputFieldEls =
- this.budgetDialog.querySelectorAll('.form__input-field');
- const errorTexts = this.budgetDialog.querySelectorAll('.error-text');
-
- inputFieldEls.forEach((item) => {
- if (item.classList.contains('error-input'))
- item.classList.remove('error-input');
- });
-
- errorTexts.forEach((item) => {
- if (item) item.remove();
- });
- }
-
- async submitBudgetForm() {
- try {
- const { formAddBudget } = document.forms;
- const form = new FormData(formAddBudget);
- const date = form.get('selected_date');
- const amount = form.get('amount');
- const note = form.get('note');
-
- // Validate data user input
-
- if (this.validateBudgetForm(date, amount)) {
- this.toggleLoaderSpinner(); // Enable loader spinner
- this.budgetDialog.close(); // Close dialog
-
- const transaction = new Transaction({
- categoryName: DEFAULT_CATEGORY.INCOME,
- date,
- amount: +amount,
- note,
- idUser: this.wallet.idUser,
- });
-
- await this.saveTransaction(transaction);
-
- // Reload data
- await this.loadTransactionData();
- await this.updateAmountWallet();
- await this.loadData();
-
- // Hide loader spinner
- this.toggleLoaderSpinner();
-
- // Show success message
- this.showSuccessToast(
- MESSAGE.ADD_TRANSACTION_SUCCESS,
- MESSAGE.DEFAULT_MESSAGE,
- );
-
- document.getElementById('formAddBudget').reset();
- }
- } catch (error) {
- this.showErrorToast(error);
- }
- }
-
- validateBudgetForm(date, amount) {
- const inputFieldEl =
- this.budgetDialog.querySelectorAll('.form__input-field');
-
- if (!date || !amount) {
- if (!date) {
- renderRequiredText('date', inputFieldEl[0]);
- inputFieldEl[0].classList.add('error-input');
- }
-
- if (!amount) {
- renderRequiredText('amount', inputFieldEl[1]);
- inputFieldEl[1].classList.add('error-input');
- }
-
- return false;
- }
-
- return true;
- }
-
- changeBtnStyle(bodyDialog) {
- const date = bodyDialog.querySelector('.input-date').value;
- const amount = +bodyDialog.querySelector('.form__input-balance').value;
- const saveBtn = bodyDialog.querySelector('.form__save-btn');
-
- if (date.trim() && amount >= 1) {
- saveBtn.classList.add('active');
- } else {
- saveBtn.classList.remove('active');
- }
- }
- // ---------------------END DIALOG---------------------//
-
- // ---------------------WALLET DIALOG--------------------- //
- addHandlerEventWalletForm() {
- this.walletDialog.addEventListener('submit', (e) => {
- e.preventDefault();
-
- this.clearErrorStyleWalletDialog();
- this.submitWalletForm();
- });
-
- this.walletDialog.addEventListener('input', (e) => {
- const bodyDialog = e.target.closest('.dialog__body');
-
- this.changeBtnStyleWalletDialog(bodyDialog);
- this.clearErrorStyleWalletDialog();
- });
- }
-
- clearErrorStyleWalletDialog() {
- const inputFieldEls =
- this.walletDialog.querySelectorAll('.form__input-field');
- const errorTextEls = this.walletDialog.querySelectorAll('.error-text');
-
- inputFieldEls.forEach((item) => {
- if (item.classList.contains('error-input'))
- item.classList.remove('error-input');
- });
-
- errorTextEls.forEach((item) => {
- if (item) item.remove();
- });
- }
-
- async submitWalletForm() {
- try {
- // Wallet info
- const { walletForm } = document.forms;
- const form = new FormData(walletForm);
- const walletName = form.get('walletName');
- const amount = form.get('amount');
-
- if (this.validateWalletDialog(walletName, amount)) {
- this.walletDialog.close();
- this.toggleLoaderSpinner();
-
- this.wallet = new Wallet({
- walletName,
- amount: +amount,
- idUser: this.user.id,
- inflow: +amount,
- });
-
- await this.saveWallet(this.wallet);
-
- // Transaction info
- const transaction = new Transaction({
- categoryName: 'Income',
- date: new Date().toISOString().slice(0, 10),
- note: FIRST_ADD_WALLET_NOTE,
- amount: +amount,
- idUser: this.user.id,
- });
-
- await this.saveTransaction(transaction);
-
- // Load data and event
- await this.loadTransactionData();
- await this.loadData();
- this.loadEvent();
-
- this.showSuccessToast(
- MESSAGE.ADD_WALLET_SUCCESS,
- MESSAGE.DEFAULT_MESSAGE,
- );
-
- await this.loadData(); // Load data from database into page
-
- this.toggleLoaderSpinner();
- }
- } catch (error) {
- // Show toast error
- this.showErrorToast(error);
- this.toggleLoaderSpinner();
- }
- }
-
- validateWalletDialog(walletName, amount) {
- const inputFieldEls =
- this.walletDialog.querySelectorAll('.form__input-field');
-
- if (!walletName || !amount) {
- if (!walletName) {
- renderRequiredText('wallet name', inputFieldEls[0]);
- inputFieldEls[0].classList.add('error-input');
- }
-
- if (!amount) {
- renderRequiredText('amount', inputFieldEls[2]);
- inputFieldEls[2].classList.add('error-input');
- }
-
- return false;
- }
-
- return true;
- }
-
- changeBtnStyleWalletDialog(bodyDialog) {
- const walletName = bodyDialog.querySelector('.form__input-text').value;
- const amount = bodyDialog.querySelector('.form__input-balance').value;
- const saveBtn = bodyDialog.querySelector('.form__save-btn');
-
- if (walletName.length >= 3 && amount >= 1) {
- saveBtn.classList.add('active');
- } else {
- saveBtn.classList.remove('active');
- }
- }
-
- // ---------------------END DIALOG--------------------- //
-
showSuccessToast(title, message) {
const typeToast = TYPE_TOAST.success;
const btnContent = BTN_CONTENT.OK;
@@ -849,8 +271,6 @@ export default class HomeView extends CommonView {
loadEvent() {
this.addCommonEventPage();
this.handlerTabsTransfer();
- this.addEventSelectCategoryDialog();
- this.addEventTransactionItem();
}
/**
@@ -891,18 +311,6 @@ export default class HomeView extends CommonView {
});
});
- this.addTransactionBtn.addEventListener('click', () => {
- this.showTransactionDialog();
- });
-
- this.addBudgetBtn.addEventListener('click', () => {
- // Set default value for date input
- this.budgetDialog.querySelector("[name='selected_date']").valueAsDate =
- new Date();
-
- this.budgetDialog.showModal();
- });
-
this.cancelBtns.forEach((btn) => {
btn.addEventListener('click', () => {
this.closeAllDialog();
@@ -931,85 +339,6 @@ export default class HomeView extends CommonView {
});
}
- showTransactionDialog(idTransaction = null) {
- this.clearInputTransactionForm();
-
- // Init data transaction to dialog
- this.initValueTransactionDialog(idTransaction);
-
- if (!idTransaction)
- this.transactionDialog.querySelector(
- "[name='selected_date']",
- ).valueAsDate = new Date(); // Set default value for date input
-
- // Change style submit btn
- this.changeBtnStyleTransactionDialog();
- this.transactionDialog.showModal();
- }
-
- addEventTransactionItem() {
- const transactionItemEl = document.querySelectorAll('.transaction__item');
-
- if (transactionItemEl) {
- transactionItemEl.forEach((item) => {
- item.addEventListener('click', (e) => {
- const transactionTime = e.target.closest('.transaction__time');
- const categoryNameEl = item.querySelector(
- '.transaction__category-name',
- );
-
- if (transactionTime) {
- const idTransaction = transactionTime.dataset.id;
-
- // If it is income transaction, don't show dialog
- if (categoryNameEl.textContent.trim() !== 'Income')
- this.showTransactionDialog(idTransaction);
- }
- });
- });
- }
- }
-
- addEventSelectCategoryDialog() {
- const categoryListEl = document.querySelector('.list-category');
- const categoryIconEl = this.categoryField.querySelector('.category-icon');
- const categoryNameEl = this.categoryField.querySelector('.category-name');
-
- categoryListEl.addEventListener('click', (e) => {
- const categoryItem = e.target.closest('.category-item');
-
- if (categoryItem) {
- const { url } = categoryItem.dataset;
- const { value } = categoryItem.dataset;
-
- // Set url and value into category field in transaction dialog
- categoryIconEl.src = url;
- categoryNameEl.value = value;
-
- // Close select category dialog
- this.categoryDialog.close();
-
- // Clear error style for transaction dialog
- this.clearErrorTransactionDialog();
- }
- });
-
- // Pass value selected to category dialog
- categoryNameEl.addEventListener('click', () => {
- if (categoryNameEl.value) {
- // Make the keyword search category name into global
- this.keySearchCategory = categoryNameEl.value;
-
- this.renderCategoryList(this.keySearchCategory);
- }
- this.categoryDialog.showModal();
- });
-
- this.closeIcon.addEventListener('click', () => {
- this.categoryDialog.close();
- });
- }
-
closeAllDialog() {
this.dialogs.forEach((dialog) => {
dialog.close();
diff --git a/javascript-practice/src/js/views/summaryTabView.js b/javascript-practice/src/js/views/summaryTabView.js
new file mode 100644
index 0000000..7cc1022
--- /dev/null
+++ b/javascript-practice/src/js/views/summaryTabView.js
@@ -0,0 +1,37 @@
+import { formatNumber } from '../helpers/helpers';
+
+class SummaryTabView {
+ initFunction(transform) {
+ this.transform = transform;
+ }
+
+ subscribe() {
+ this.transform.create('summaryTabView', this.updateData.bind(this));
+ }
+
+ updateData(data) {
+ if (data.wallet) this.wallet = data.wallet;
+
+ if (data.listTransactions) this.listTransaction = data.listTransactions;
+
+ if (data.listCategory) this.listCategory = data.listCategory;
+ }
+
+ load() {
+ 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(Math.abs(outflow))}`;
+ totalValue.textContent = `${total >= 0 ? '+' : '-'}$ ${formatNumber(
+ Math.abs(total),
+ )}`;
+ }
+}
+
+export default new SummaryTabView();
diff --git a/javascript-practice/src/js/views/transactionTabView.js b/javascript-practice/src/js/views/transactionTabView.js
new file mode 100644
index 0000000..566b7b9
--- /dev/null
+++ b/javascript-practice/src/js/views/transactionTabView.js
@@ -0,0 +1,169 @@
+import {
+ createTransactionDetailObject,
+ getAllCategoryNameInTransactions,
+ getAllTransactionByCategoryName,
+} from '../helpers/dataProcess';
+import { formatNumber } from '../helpers/helpers';
+import TransactionView from './transactionView';
+
+class TransactionTabView {
+ constructor() {
+ this.addEventTransactionItem();
+ }
+
+ initFunction(transform) {
+ this.transform = transform;
+ }
+
+ subscribe() {
+ this.transform.create('transactionTabView', this.updateData.bind(this));
+ }
+
+ updateData(data) {
+ if (data.wallet) this.wallet = data.wallet;
+
+ if (data.listTransactions) this.listTransactions = data.listTransactions;
+
+ if (data.listCategory) this.listCategory = data.listCategory;
+ }
+
+ async loadTransactionTab() {
+ // Load category
+ // Init data first
+ this.transactionDetails = this.loadTransactionDetailsData();
+ const transactionEl = document.querySelector('.transaction');
+ const listTransactionDetailEl =
+ transactionEl.querySelector('.transaction__list');
+ const markup = [];
+ if (this.transactionDetails) {
+ this.transactionDetails.forEach((transactionDetail) => {
+ markup.push(this.transactionDetailMarkup(transactionDetail));
+ });
+ }
+ listTransactionDetailEl.innerHTML = '';
+ listTransactionDetailEl.insertAdjacentHTML('afterbegin', markup.join('\n'));
+ // Load event
+ this.addEventTransactionItem();
+ }
+
+ loadTransactionDetailsData() {
+ // Get all category user have in transactions
+ const listCategoryInTransaction = getAllCategoryNameInTransactions(
+ this.listTransactions,
+ );
+
+ // Create transactions details object
+ const tempList = listCategoryInTransaction.map((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,
+ );
+
+ return createTransactionDetailObject(
+ Object.assign({}, ...category),
+ transactions,
+ );
+ });
+
+ tempList.sort(
+ (a, b) =>
+ parseInt(b.transactions[0].id, 10) - parseInt(a.transactions[0].id, 10),
+ );
+
+ return tempList;
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ 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 `
+
+
+
+
+

+
+
+
+ ${transactionDetail.categoryName}
+
+
${
+ transactionDetail.totalTransaction
+ } Transactions
+
+
+
${
+ transactionDetail.totalAmount >= 0 ? '+' : '-'
+ }$ ${formatNumber(Math.abs(transactionDetail.totalAmount))}
+
+
+
+ ${transactionTimeMarkup()}
+
+ `;
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ addEventTransactionItem() {
+ const transactionItemEl = document.querySelectorAll('.transaction__item');
+
+ if (transactionItemEl) {
+ transactionItemEl.forEach((item) => {
+ item.addEventListener('click', (e) => {
+ const transactionTime = e.target.closest('.transaction__time');
+ const categoryNameEl = item.querySelector(
+ '.transaction__category-name',
+ );
+
+ if (transactionTime) {
+ const idTransaction = transactionTime.dataset.id;
+
+ // If it is income transaction, don't show dialog
+ if (categoryNameEl.textContent.trim() !== 'Income')
+ TransactionView.showTransactionDialog(idTransaction);
+ }
+ });
+ });
+ }
+ }
+}
+
+export default new TransactionTabView();
diff --git a/javascript-practice/src/js/views/transactionView.js b/javascript-practice/src/js/views/transactionView.js
new file mode 100644
index 0000000..94bc61b
--- /dev/null
+++ b/javascript-practice/src/js/views/transactionView.js
@@ -0,0 +1,312 @@
+import * as MESSAGE from '../constants/message';
+import { renderRequiredText } from '../helpers/validateForm';
+import Transaction from '../models/transaction';
+import CategoryView from './categoryView';
+
+import defaultCategoryIcon from '../../assets/images/question-icon.svg';
+
+class TransactionView {
+ constructor() {
+ this.addTransactionBtn = document.getElementById('addTransaction');
+ this.transactionDialog = document.getElementById('transactionDialog');
+ this.transactionForm = document.getElementById('formAddTransaction');
+
+ this.handlerEventTransactionDialog();
+ }
+
+ initFunction(
+ toggleLoaderSpinner,
+ deleteTransaction,
+ loadTransactionData,
+ updateAmountWallet,
+ loadData,
+ showSuccessToast,
+ showErrorToast,
+ saveTransaction,
+ transform,
+ ) {
+ this.toggleLoaderSpinner = toggleLoaderSpinner;
+ this.deleteTransaction = deleteTransaction;
+ this.loadTransactionData = loadTransactionData;
+ this.updateAmountWallet = updateAmountWallet;
+ this.loadData = loadData;
+ this.showSuccessToast = showSuccessToast;
+ this.showErrorToast = showErrorToast;
+ this.saveTransaction = saveTransaction;
+ this.transform = transform;
+ }
+
+ subscribe() {
+ this.transform.create('transactionView', this.updateData.bind(this));
+ }
+
+ sendData() {
+ const data = { wallet: this.wallet, listTransaction: this.listTransaction };
+
+ this.transform.onSendSignal('transactionView', data);
+ }
+
+ updateData(data) {
+ if (data.wallet) this.wallet = data.wallet;
+
+ if (data.listTransactions) this.listTransactions = data.listTransactions;
+
+ if (data.listCategory) this.listCategory = data.listCategory;
+ }
+
+ handlerEventTransactionDialog() {
+ this.transactionDialog.addEventListener('submit', (e) => {
+ e.preventDefault();
+ this.clearErrorTransactionDialog();
+ this.submitTransactionDialog();
+ });
+
+ this.transactionDialog.addEventListener('input', () => {
+ this.changeBtnStyleTransactionDialog();
+ this.clearErrorTransactionDialog();
+ });
+
+ // Add delete transaction event
+ this.deleteTransactionEvent();
+
+ this.addTransactionBtn.addEventListener('click', () => {
+ this.showTransactionDialog();
+ });
+
+ this.handlerCategoryFieldEvent();
+ }
+
+ handlerCategoryFieldEvent() {
+ const categoryField = this.transactionForm.querySelector(
+ "[name='category_name']",
+ );
+
+ categoryField.addEventListener('click', () => {
+ this.clearErrorTransactionDialog();
+ });
+ }
+
+ clearErrorTransactionDialog() {
+ // Clear error style
+ const inputFieldEls =
+ this.transactionDialog.querySelectorAll('.form__input-field');
+ const errorTextEls = this.transactionDialog.querySelectorAll('.error-text');
+
+ inputFieldEls.forEach((item) => {
+ if (item.classList.contains('error-input'))
+ item.classList.remove('error-input');
+ });
+ errorTextEls.forEach((item) => {
+ if (item) item.remove();
+ });
+ }
+
+ deleteTransactionEvent() {
+ const deleteTransactionBtn =
+ this.transactionDialog.querySelector('.form__delete-btn');
+
+ deleteTransactionBtn.addEventListener('click', async () => {
+ const idEl = this.transactionDialog.querySelector(
+ "[name='id_transaction']",
+ );
+
+ if (idEl.value) {
+ try {
+ this.transactionDialog.close();
+ this.toggleLoaderSpinner();
+
+ await this.deleteTransaction(idEl.value);
+
+ // Reload data
+ await this.loadTransactionData();
+ await this.updateAmountWallet();
+ await this.loadData();
+
+ this.showSuccessToast('Delete success!', MESSAGE.DEFAULT_MESSAGE);
+ } catch (error) {
+ this.showErrorToast(error);
+ }
+ this.toggleLoaderSpinner();
+ }
+ });
+ }
+
+ changeBtnStyleTransactionDialog() {
+ const dateInput = this.transactionDialog.querySelector(
+ "[name='selected_date']",
+ ).value;
+ const categoryName = this.transactionDialog.querySelector(
+ "[name='category_name']",
+ ).value;
+ const amountInput =
+ this.transactionDialog.querySelector("[name='amount']").value;
+ const saveBtn = this.transactionDialog.querySelector('.form__save-btn');
+
+ saveBtn.classList.toggle(
+ 'active',
+ dateInput && categoryName && amountInput >= 1,
+ );
+ }
+
+ initValueTransactionDialog(idTransaction) {
+ let categoryName;
+
+ if (idTransaction) {
+ // Get transaction object
+ const transactionArr = this.listTransactions.filter(
+ (obj) => obj.id === idTransaction,
+ );
+ const transaction = Object.assign({}, ...transactionArr);
+ // Get category object
+ const categoryArr = this.listCategory.filter(
+ (obj) => obj.name === transaction.categoryName,
+ );
+ const category = Object.assign({}, ...categoryArr);
+
+ const idEl = this.transactionDialog.querySelector(
+ "[name='id_transaction']",
+ );
+ const dateEl = this.transactionDialog.querySelector(
+ "[name='selected_date']",
+ );
+ const categoryEl = this.transactionDialog.querySelector(
+ "[name='category_name']",
+ );
+ const amountEl = this.transactionDialog.querySelector("[name='amount']");
+ const noteEl = this.transactionDialog.querySelector("[name='note']");
+ const iconEl = this.transactionDialog.querySelector('.category-icon');
+
+ idEl.value = transaction.id;
+ dateEl.value = transaction.date;
+ categoryEl.value = transaction.categoryName;
+ amountEl.value = Math.abs(transaction.amount);
+ noteEl.value = transaction.note;
+ iconEl.src = category.url;
+
+ categoryName = categoryEl.value;
+ }
+
+ // Show delete button only if it is a edit form and not a income transaction
+ const deleteBtn = this.transactionDialog.querySelector('.form__delete-btn');
+ const showDeleteBtn = () => {
+ return idTransaction && categoryName !== 'Income';
+ };
+
+ deleteBtn.classList.toggle('hide', !showDeleteBtn());
+ }
+
+ async submitTransactionDialog() {
+ try {
+ const dateEl = this.transactionForm.querySelector(
+ "[name='selected_date']",
+ );
+ const categoryNameEl = this.transactionForm.querySelector(
+ "[name='category_name']",
+ );
+ const amountEl = this.transactionForm.querySelector("[name='amount']");
+ const noteEl = this.transactionForm.querySelector("[name='note']");
+ const idEl = this.transactionForm.querySelector(
+ "[name='id_transaction']",
+ );
+ const amount =
+ categoryNameEl.value === 'Income' ? +amountEl.value : -+amountEl.value; // Check if this is a income transaction, amount must plus
+
+ // Validate data user input
+ if (
+ this.validateTransactionForm(dateEl.value, categoryNameEl.value, amount)
+ ) {
+ this.toggleLoaderSpinner();
+ this.transactionDialog.close();
+
+ const transaction = new Transaction({
+ id: idEl.value,
+ categoryName: categoryNameEl.value,
+ date: dateEl.value,
+ amount,
+ note: noteEl.value,
+ idUser: this.wallet.idUser,
+ });
+
+ await this.saveTransaction(transaction);
+
+ // Reload data
+ await this.loadTransactionData();
+ this.updateAmountWallet();
+ await this.loadData();
+
+ if (!idEl.value) {
+ // Add success
+ this.showSuccessToast(
+ MESSAGE.ADD_TRANSACTION_SUCCESS,
+ MESSAGE.DEFAULT_MESSAGE,
+ );
+ } else {
+ // Update success
+ this.showSuccessToast(
+ MESSAGE.UPDATE_TRANSACTION_SUCCESS,
+ MESSAGE.DEFAULT_MESSAGE,
+ );
+ }
+
+ this.toggleLoaderSpinner();
+ this.clearInputTransactionForm(this.transactionForm);
+ }
+ } catch (error) {
+ this.showErrorToast(error);
+ this.toggleLoaderSpinner();
+ }
+ }
+
+ validateTransactionForm(date, categoryName, amount) {
+ const inputFieldEls =
+ this.transactionDialog.querySelectorAll('.form__input-field');
+
+ if (!date || !categoryName || !amount) {
+ if (!date) {
+ renderRequiredText('date', inputFieldEls[0]);
+ inputFieldEls[0].classList.add('error-input');
+ }
+
+ if (!categoryName) {
+ renderRequiredText('category', inputFieldEls[1]);
+ inputFieldEls[1].classList.add('error-input');
+ }
+
+ if (!amount) {
+ renderRequiredText('amount', inputFieldEls[2]);
+ inputFieldEls[2].classList.add('error-input');
+ }
+
+ return false;
+ }
+
+ return true;
+ }
+
+ clearInputTransactionForm() {
+ const categoryIcon = this.transactionForm.querySelector('.category-icon');
+
+ categoryIcon.src = defaultCategoryIcon;
+ this.keySearchCategory = null; // Delete keyword search
+ CategoryView.renderCategoryList(this.keySearchCategory);
+ this.transactionForm.reset();
+ }
+
+ showTransactionDialog(idTransaction = null) {
+ this.clearInputTransactionForm();
+
+ // Init data transaction to dialog
+ this.initValueTransactionDialog(idTransaction);
+
+ if (!idTransaction)
+ this.transactionDialog.querySelector(
+ "[name='selected_date']",
+ ).valueAsDate = new Date(); // Set default value for date input
+
+ // Change style submit btn
+ this.changeBtnStyleTransactionDialog();
+ this.transactionDialog.showModal();
+ }
+}
+
+export default new TransactionView();
diff --git a/javascript-practice/src/js/views/walletView.js b/javascript-practice/src/js/views/walletView.js
new file mode 100644
index 0000000..5636457
--- /dev/null
+++ b/javascript-practice/src/js/views/walletView.js
@@ -0,0 +1,180 @@
+import { FIRST_ADD_WALLET_NOTE } from '../constants/config';
+import Transaction from '../models/transaction';
+import Wallet from '../models/wallet';
+import * as MESSAGE from '../constants/message';
+import { renderRequiredText } from '../helpers/validateForm';
+
+class WalletView {
+ constructor() {
+ this.walletDialog = document.getElementById('walletDialog');
+
+ this.addHandlerEventWalletForm();
+ }
+
+ initFunction(
+ transform,
+ toggleLoaderSpinner,
+ saveWallet,
+ saveTransaction,
+ loadTransactionData,
+ loadData,
+ loadEvent,
+ showSuccessToast,
+ showErrorToast,
+ ) {
+ this.transform = transform;
+ this.toggleLoaderSpinner = toggleLoaderSpinner;
+ this.saveWallet = saveWallet;
+ this.saveTransaction = saveTransaction;
+ this.loadTransactionData = loadTransactionData;
+ this.loadData = loadData;
+ this.loadEvent = loadEvent;
+ this.showSuccessToast = showSuccessToast;
+ this.showErrorToast = showErrorToast;
+ }
+
+ subscribe() {
+ this.transform.create('walletView', this.updateData.bind(this));
+ }
+
+ sendData() {
+ const data = {
+ wallet: this.wallet,
+ listTransactions: this.listTransactions,
+ user: this.user,
+ };
+
+ this.transform.onSendSignal('walletView', data);
+ }
+
+ updateData(data) {
+ if (data.wallet) this.wallet = data.wallet;
+
+ if (data.listTransactions) this.listTransaction = data.listTransactions;
+
+ if (data.listCategory) this.listCategory = data.listCategory;
+
+ if (data.user) this.user = data.user;
+ }
+
+ showDialog() {
+ this.walletDialog.showModal();
+ }
+
+ addHandlerEventWalletForm() {
+ this.walletDialog.addEventListener('submit', (e) => {
+ e.preventDefault();
+
+ this.clearErrorStyleWalletDialog();
+ this.submitWalletForm();
+ });
+
+ this.walletDialog.addEventListener('input', (e) => {
+ const bodyDialog = e.target.closest('.dialog__body');
+
+ this.changeBtnStyleWalletDialog(bodyDialog);
+ this.clearErrorStyleWalletDialog();
+ });
+ }
+
+ clearErrorStyleWalletDialog() {
+ const inputFieldEls =
+ this.walletDialog.querySelectorAll('.form__input-field');
+ const errorTextEls = this.walletDialog.querySelectorAll('.error-text');
+
+ inputFieldEls.forEach((item) => {
+ if (item.classList.contains('error-input'))
+ item.classList.remove('error-input');
+ });
+
+ errorTextEls.forEach((item) => {
+ if (item) item.remove();
+ });
+ }
+
+ async submitWalletForm() {
+ try {
+ // Wallet info
+ const { walletForm } = document.forms;
+ const form = new FormData(walletForm);
+ const walletName = form.get('walletName');
+ const amount = form.get('amount');
+
+ if (this.validateWalletDialog(walletName, amount)) {
+ this.walletDialog.close();
+ this.toggleLoaderSpinner();
+
+ this.wallet = new Wallet({
+ walletName,
+ amount: +amount,
+ idUser: this.user.id,
+ inflow: +amount,
+ });
+
+ this.sendData();
+
+ await this.saveWallet(this.wallet);
+ // Transaction info
+ const transaction = new Transaction({
+ categoryName: 'Income',
+ date: new Date().toISOString().slice(0, 10),
+ note: FIRST_ADD_WALLET_NOTE,
+ amount: +amount,
+ idUser: this.user.id,
+ });
+ await this.saveTransaction(transaction);
+ // Load data and event
+ await this.loadTransactionData();
+ await this.loadData();
+ this.loadEvent();
+ this.showSuccessToast(
+ MESSAGE.ADD_WALLET_SUCCESS,
+ MESSAGE.DEFAULT_MESSAGE,
+ );
+ await this.loadData(); // Load data from database into page
+
+ this.toggleLoaderSpinner();
+ }
+ } catch (error) {
+ // Show toast error
+ this.showErrorToast(error);
+ this.toggleLoaderSpinner();
+ }
+ }
+
+ validateWalletDialog(walletName, amount) {
+ const inputFieldEls =
+ this.walletDialog.querySelectorAll('.form__input-field');
+
+ if (!walletName || !amount) {
+ if (!walletName) {
+ renderRequiredText('wallet name', inputFieldEls[0]);
+ inputFieldEls[0].classList.add('error-input');
+ }
+
+ if (!amount) {
+ renderRequiredText('amount', inputFieldEls[2]);
+ inputFieldEls[2].classList.add('error-input');
+ }
+
+ return false;
+ }
+
+ return true;
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ changeBtnStyleWalletDialog(bodyDialog) {
+ const walletName = bodyDialog.querySelector('.form__input-text').value;
+ const amount = bodyDialog.querySelector('.form__input-balance').value;
+ const saveBtn = bodyDialog.querySelector('.form__save-btn');
+
+ if (walletName.length >= 3 && amount >= 1) {
+ saveBtn.classList.add('active');
+ } else {
+ saveBtn.classList.remove('active');
+ }
+ }
+}
+
+export default new WalletView();
diff --git a/javascript-practice/src/pages/index.html b/javascript-practice/src/pages/index.html
index 67e906f..2ddb89a 100644
--- a/javascript-practice/src/pages/index.html
+++ b/javascript-practice/src/pages/index.html
@@ -5,6 +5,7 @@
+
Money Lover Web
@@ -20,7 +21,7 @@