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} Icon +

${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 ` -
-
-
-
- Transportation icon category -
-
-

- ${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} Icon -

${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 ` +
+
+
+
+ Transportation icon category +
+
+

+ ${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 @@
-

Wallet name

+
-

Currency

+
-

Initial Balance

+
- +
@@ -71,20 +74,20 @@
-

Date

+
-
+
-

Amount

+
-

Note

+
- +
@@ -127,7 +132,7 @@
-

Date

+
-

Category

+
-
+
-

Amount

+
-

Note

+
- +
@@ -222,7 +231,7 @@
- Wallet Icon + Wallet Icon

User

diff --git a/javascript-practice/src/pages/login.html b/javascript-practice/src/pages/login.html index bf48433..53cb035 100644 --- a/javascript-practice/src/pages/login.html +++ b/javascript-practice/src/pages/login.html @@ -5,6 +5,7 @@ + Login - Money Lover diff --git a/javascript-practice/src/pages/register.html b/javascript-practice/src/pages/register.html index 79ac8d5..c12b343 100644 --- a/javascript-practice/src/pages/register.html +++ b/javascript-practice/src/pages/register.html @@ -5,6 +5,7 @@ + Register - Money Lover diff --git a/javascript-practice/src/styles/abstracts/_variables.scss b/javascript-practice/src/styles/abstracts/_variables.scss index 322974e..a73b15f 100644 --- a/javascript-practice/src/styles/abstracts/_variables.scss +++ b/javascript-practice/src/styles/abstracts/_variables.scss @@ -6,8 +6,6 @@ $light-primary-color: #00bc2a; $primary-color: #2db84c; $dark-primary-color: #00710f; $primary-text-color: #000; -$secondary-text-color: #0000008a; -$secondary-light-text-color: #00000042; $amount-outcome-color: #e51c23; $amount-income-color: #039be5; $background-input-color: #f5f5f5; @@ -15,10 +13,9 @@ $dark-error-color: #ce1414; $light-error-color: #ffc3c3; $light-gray: #f4f4f4; $gray: #dddddd; -$dark-gray: #00000042; $dark-x-gray: #0000008a; -$white: #ffffff; -$black: #000000; +$white: #fff; +$black: #000; // Font size $fs-2x-sm: 11px; diff --git a/javascript-practice/src/styles/pages/_index.scss b/javascript-practice/src/styles/pages/_index.scss index 3e3b2f8..f31d251 100644 --- a/javascript-practice/src/styles/pages/_index.scss +++ b/javascript-practice/src/styles/pages/_index.scss @@ -108,6 +108,8 @@ background-color: $primary-color; border-radius: 10px; transition: all 0.3s ease-in-out; + width: 116px; + left: 152px; } } @@ -245,7 +247,7 @@ &__note { @extend .transaction__date-time; - color: $dark-gray; + color: $black; } &__outcome { @@ -506,13 +508,8 @@ line-height: 16.1px; font-weight: 500; padding: 10px 20px; - color: $dark-gray; + color: $black; cursor: pointer; - - &.active { - background-color: $primary-color; - color: $white; - } } &__cancel-btn { @@ -573,14 +570,15 @@ top: 10px; right: 5px; background-color: transparent; - border-top: 3px solid $dark-gray; - border-right: 3px solid $dark-gray; + border-top: 3px solid $black; + border-right: 3px solid $black; border-bottom: none; border-left: none; height: 7px; width: 7px; rotate: 45deg; pointer-events: none; + opacity: 0.56; } .category-input-container { @@ -666,6 +664,22 @@ @extend %d-flex; @include flex-layout($direction: column, $gap: 7px); } + + .form__save-btn { + .text { + color: $black; + opacity: 0.54; + } + + &.active { + background-color: $primary-color; + + & .text { + color: $white; + opacity: 1; + } + } + } } .close-icon { diff --git a/javascript-practice/src/styles/pages/_login-register.scss b/javascript-practice/src/styles/pages/_login-register.scss index 3171f3d..1aef124 100644 --- a/javascript-practice/src/styles/pages/_login-register.scss +++ b/javascript-practice/src/styles/pages/_login-register.scss @@ -57,7 +57,8 @@ &__description { margin-top: 15px; - color: $secondary-text-color; + color: $black; + opacity: 0.54; font-size: $fs-md; line-height: 24px; }