From 9313353b3069fb50250de5448ffae28a1a92c4b4 Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Mon, 2 Oct 2023 17:46:22 +0700 Subject: [PATCH] Add transaction, budget, category view --- typescript-practice/globals.d.ts | 1 + .../src/ts/constants/config.ts | 31 ++ .../src/ts/controllers/homeController.ts | 22 +- typescript-practice/src/ts/global/types.ts | 18 +- typescript-practice/src/ts/helpers/data.ts | 85 +++- typescript-practice/src/ts/models/category.ts | 25 ++ .../src/ts/models/transaction.ts | 70 +--- .../src/ts/models/transactionDetail.ts | 17 + typescript-practice/src/ts/models/wallet.ts | 54 +-- .../src/ts/services/categoryService.ts | 32 ++ .../src/ts/services/commonService.ts | 8 +- typescript-practice/src/ts/services/index.ts | 4 + .../src/ts/services/transactionService.ts | 2 +- .../src/ts/views/home/budgetView.ts | 205 ++++++++++ .../src/ts/views/home/categoryView.ts | 167 ++++++++ .../src/ts/views/home/homeView.ts | 237 ++++++++--- .../src/ts/views/home/summaryTabView.ts | 48 +++ .../src/ts/views/home/transactionTabView.ts | 192 +++++++++ .../src/ts/views/home/transactionView.ts | 371 ++++++++++++++++++ .../src/ts/views/home/walletView.ts | 12 +- 20 files changed, 1425 insertions(+), 176 deletions(-) create mode 100644 typescript-practice/globals.d.ts create mode 100644 typescript-practice/src/ts/models/category.ts create mode 100644 typescript-practice/src/ts/models/transactionDetail.ts create mode 100644 typescript-practice/src/ts/services/categoryService.ts create mode 100644 typescript-practice/src/ts/views/home/budgetView.ts create mode 100644 typescript-practice/src/ts/views/home/categoryView.ts create mode 100644 typescript-practice/src/ts/views/home/summaryTabView.ts create mode 100644 typescript-practice/src/ts/views/home/transactionTabView.ts create mode 100644 typescript-practice/src/ts/views/home/transactionView.ts diff --git a/typescript-practice/globals.d.ts b/typescript-practice/globals.d.ts new file mode 100644 index 0000000..bff9471 --- /dev/null +++ b/typescript-practice/globals.d.ts @@ -0,0 +1 @@ +declare module '*.svg'; diff --git a/typescript-practice/src/ts/constants/config.ts b/typescript-practice/src/ts/constants/config.ts index 863c954..d631bcd 100644 --- a/typescript-practice/src/ts/constants/config.ts +++ b/typescript-practice/src/ts/constants/config.ts @@ -32,3 +32,34 @@ export const URL = { REGISTER: 'register', HOME: '', }; + +export const DEFAULT_CATEGORY = { + INCOME: 'Income', +}; + +export const REMOVE_CATEGORY = ['Income']; + +export const DAY = [ + 'Sunday', + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', +]; + +export const MONTH = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', +]; diff --git a/typescript-practice/src/ts/controllers/homeController.ts b/typescript-practice/src/ts/controllers/homeController.ts index e9f254e..ba2f180 100644 --- a/typescript-practice/src/ts/controllers/homeController.ts +++ b/typescript-practice/src/ts/controllers/homeController.ts @@ -4,6 +4,7 @@ import Service from 'services'; import View from 'views'; import Wallet from 'models/wallet'; import Transaction from 'models/transaction'; +import Category from 'models/category'; export default class HomeController { public homeView: HomeView | null = null; @@ -20,25 +21,40 @@ export default class HomeController { return this.service.userService.getInfoUserLogin(); } - handlerGetWalletByIdUser(idUser: number) { + handlerGetWalletByIdUser(idUser: number): Promise { return this.service.walletService.getWalletByIdUser(idUser); } - handlerSaveWallet(wallet: Wallet) { + handlerSaveWallet(wallet: Wallet): Promise { return this.service.walletService.saveWallet(wallet); } - handlerSaveTransaction(transaction: Transaction) { + handlerSaveTransaction(transaction: Transaction): Promise { return this.service.transactionService.saveTransaction(transaction); } + handlerGetAllCategory(): Promise { + return this.service.categoryService.getAllCategory(); + } + + handlerGetAllTransactions(idUser: number): Promise { + return this.service.transactionService.getListTransactionByIdUser(idUser); + } + + handlerDeleteTransaction(idTransaction: number): Promise { + return this.service.transactionService.deleteTransaction(idTransaction); + } + init() { if (this.homeView) { this.homeView.initFunction( this.handlerGetInfoUserLogin.bind(this), this.handlerGetWalletByIdUser.bind(this), + this.handlerGetAllCategory.bind(this), + this.handlerGetAllTransactions.bind(this), this.handlerSaveWallet.bind(this), this.handlerSaveTransaction.bind(this), + this.handlerDeleteTransaction.bind(this), new Transform(), ); diff --git a/typescript-practice/src/ts/global/types.ts b/typescript-practice/src/ts/global/types.ts index b4198ae..53aba9c 100644 --- a/typescript-practice/src/ts/global/types.ts +++ b/typescript-practice/src/ts/global/types.ts @@ -1,3 +1,5 @@ +import Category from 'models/category'; +import Transaction from 'models/transaction'; import User from 'models/user'; import Wallet from 'models/wallet'; @@ -13,7 +15,7 @@ export class DataObject { constructor(dataObject: IDataObject) { this.id = dataObject?.id ?? null; - this.data = dataObject.data as T; + this.data = dataObject.data; } } @@ -39,6 +41,16 @@ export type TSignal = { }; export interface Data { - wallet: Wallet; - user: User; + wallet?: Wallet; + listTransactions?: Transaction[]; + listCategories?: Category[]; + user?: User; +} + +export interface ItemTransaction { + id: number; + day: string; + fullDateString: string; + note: string; + amount: number; } diff --git a/typescript-practice/src/ts/helpers/data.ts b/typescript-practice/src/ts/helpers/data.ts index 5987417..297e20e 100644 --- a/typescript-practice/src/ts/helpers/data.ts +++ b/typescript-practice/src/ts/helpers/data.ts @@ -1,6 +1,9 @@ import localStorageService from 'services/localStorageService'; -import { DataObject, IDataObject } from '../global/types'; -import { LOCAL_STORAGE } from 'constants/config'; +import { DataObject, IDataObject, ItemTransaction } from '../global/types'; +import { DAY, LOCAL_STORAGE, MONTH } from 'constants/config'; +import Transaction from 'models/transaction'; +import TransactionDetail from 'models/transactionDetail'; +import Category from 'models/category'; export const generateId = (): number => { return new Date().getTime(); @@ -39,6 +42,84 @@ export const formatNumber = (number: number): string => { }); }; +export const changeDateFormat = (oldFormatDate: string) => { + const tempDate = new Date(oldFormatDate); + + const day = DAY[tempDate.getDay()]; + const date = tempDate.getDate(); + const month = MONTH[tempDate.getMonth()]; + const year = tempDate.getFullYear(); + + return `${day}, ${date}, ${month}, ${year}`; +}; + export const clearAccessToken = (): void => { localStorageService.remove(LOCAL_STORAGE.ACCESS_TOKEN); }; + +export const getAllCategoryNameInTransactions = ( + transactions: Transaction[], +): string[] => { + const categoryName = new Set(); + + transactions.forEach((transaction) => + categoryName.add(transaction.categoryName), + ); + + return Array.from(categoryName) as string[]; +}; + +export const getAllTransactionByCategoryName = ( + categoryName: string, + transactions: Transaction[], +) => { + const results = transactions.filter((transaction) => { + return transaction.categoryName === categoryName; + }); + + return results; +}; + +export const createTransactionDetailObject = ( + category: Category, + transactions: Transaction[], +): TransactionDetail => { + const totalTransaction = transactions.length; + const totalAmount = () => { + let amount = 0; + + transactions.forEach((transaction) => { + amount += transaction.amount; + }); + + return amount; + }; + const listTransaction = (): ItemTransaction[] => { + const results: ItemTransaction[] = 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: ItemTransaction = { + id: transaction.id, + day, + fullDateString, + note: transaction.note, + amount: transaction.amount, + }; + + return tempData; + }); + + results.sort((a, b) => b.id - a.id); + + return results; + }; + + return new TransactionDetail( + category.name, + category.url, + totalTransaction, + totalAmount(), + listTransaction(), + ); +}; diff --git a/typescript-practice/src/ts/models/category.ts b/typescript-practice/src/ts/models/category.ts new file mode 100644 index 0000000..e82b366 --- /dev/null +++ b/typescript-practice/src/ts/models/category.ts @@ -0,0 +1,25 @@ +export default class Category { + private readonly _id: number; + + private _url: string; + + private _name: string; + + constructor(id: number, url: string, name: string) { + this._id = id; + this._url = url; + this._name = name; + } + + get id() { + return this._id; + } + + get url() { + return this._url; + } + + get name() { + return this._name; + } +} diff --git a/typescript-practice/src/ts/models/transaction.ts b/typescript-practice/src/ts/models/transaction.ts index 839c485..33f9a34 100644 --- a/typescript-practice/src/ts/models/transaction.ts +++ b/typescript-practice/src/ts/models/transaction.ts @@ -1,65 +1,19 @@ import { generateId } from 'helpers/data'; export default class Transaction { - private readonly _id: number; - - private _categoryName: string; - - private _date: string; - - private _note: string; - - private _amount: number; - - private _idUser: number; - constructor( - categoryName: string, - date: string, - note: string, - amount: number, - idUser: number, + public id: number, + public categoryName: string, + public date: string, + public note: string, + public amount: number, + public idUser: number, ) { - this._id = generateId(); - this._categoryName = categoryName; - this._date = date; - this._note = note; - this._amount = amount; - this._idUser = idUser; - } - - get id() { - return this._id; - } - - get categoryName() { - return this._categoryName; - } - - get date() { - return this._date; - } - - get note() { - return this._note; - } - - get amount() { - return this._amount; - } - - get idUser() { - return this._idUser; - } - - get toObject() { - return { - id: this._id, - categoryName: this._categoryName, - date: this._date, - note: this._note, - amount: this._amount, - idUser: this._idUser, - } as Transaction; + this.id = id !== 0 ? id : generateId(); + this.categoryName = categoryName; + this.date = date; + this.note = note; + this.amount = amount; + this.idUser = idUser; } } diff --git a/typescript-practice/src/ts/models/transactionDetail.ts b/typescript-practice/src/ts/models/transactionDetail.ts new file mode 100644 index 0000000..0586936 --- /dev/null +++ b/typescript-practice/src/ts/models/transactionDetail.ts @@ -0,0 +1,17 @@ +import { ItemTransaction } from 'global/types'; + +export default class TransactionDetail { + constructor( + public categoryName: string, + public url: string, + public totalTransaction: number, + public totalAmount: number, + public transactions: ItemTransaction[], + ) { + this.categoryName = categoryName; + this.url = url; + this.totalTransaction = totalTransaction; + this.totalAmount = totalAmount; + this.transactions = transactions; + } +} diff --git a/typescript-practice/src/ts/models/wallet.ts b/typescript-practice/src/ts/models/wallet.ts index 6cdabd6..88002be 100644 --- a/typescript-practice/src/ts/models/wallet.ts +++ b/typescript-practice/src/ts/models/wallet.ts @@ -1,15 +1,15 @@ import { generateId } from '../helpers/data'; export default class Wallet { - private readonly _id: number; + id: number; - private _walletName: string; + walletName: string; - private _inflow: number; + inflow: number; - private _outflow: number; + outflow: number; - private _idUser: number; + idUser: number; constructor( walletName: string, @@ -17,44 +17,10 @@ export default class Wallet { outflow: number, idUser: number, ) { - this._id = generateId(); - this._walletName = walletName; - this._inflow = inflow; - this._outflow = outflow; - this._idUser = idUser; - } - - get id() { - return this._id; - } - - get walletName() { - return this._walletName; - } - - get inflow() { - return this._inflow; - } - - get outflow() { - return this._outflow; - } - - get idUser() { - return this._idUser; - } - - get amountWallet() { - return this._inflow + this._outflow; - } - - get toObject() { - return { - id: this._id, - walletName: this._walletName, - inflow: this._inflow, - outflow: this._outflow, - idUser: this._idUser, - }; + this.id = generateId(); + this.walletName = walletName; + this.inflow = inflow; + this.outflow = outflow; + this.idUser = idUser; } } diff --git a/typescript-practice/src/ts/services/categoryService.ts b/typescript-practice/src/ts/services/categoryService.ts new file mode 100644 index 0000000..b471a33 --- /dev/null +++ b/typescript-practice/src/ts/services/categoryService.ts @@ -0,0 +1,32 @@ +import CommonService from './commonService'; +import Category from '../models/category'; + +export default class CategoryService extends CommonService { + constructor() { + super(); + + this.defaultPath = 'categories/'; + } + + async getAllCategory() { + const data = await this.getAllDataFromPath(this.defaultPath); + + if (data) { + return data.reverse().map((category): Category => category); + } + + return null; + } + + async getCategoryByName(nameCategory: string) { + const data = await this.getDataFromProp( + 'name', + nameCategory, + this.defaultPath, + ); + + if (data) return data; + + return null; + } +} diff --git a/typescript-practice/src/ts/services/commonService.ts b/typescript-practice/src/ts/services/commonService.ts index ebbe8fc..3c8dd9d 100644 --- a/typescript-practice/src/ts/services/commonService.ts +++ b/typescript-practice/src/ts/services/commonService.ts @@ -77,7 +77,7 @@ export default class CommonService { return null; } - async getAllDataFromPath(path = this.defaultPath): Promise { + async getAllDataFromPath(path = this.defaultPath): Promise { this.connectToDb(); const results = await timeOutConnect( @@ -90,7 +90,7 @@ export default class CommonService { const tempData = data as DataObject; return convertDataObjectToModel(tempData); - }) as T; + }) as T[]; } return null; @@ -100,7 +100,7 @@ export default class CommonService { property: string, value: string, path: string = this.defaultPath, - ): Promise { + ): Promise { this.connectToDb(); const results = await timeOutConnect( @@ -114,7 +114,7 @@ export default class CommonService { const tempData = data as DataObject; return convertDataObjectToModel(tempData); - }) as T; + }) as T[]; } return null; diff --git a/typescript-practice/src/ts/services/index.ts b/typescript-practice/src/ts/services/index.ts index 6331bee..5375744 100644 --- a/typescript-practice/src/ts/services/index.ts +++ b/typescript-practice/src/ts/services/index.ts @@ -1,3 +1,4 @@ +import CategoryService from './categoryService'; import TransactionService from './transactionService'; import UserService from './userService'; import WalletService from './walletService'; @@ -9,9 +10,12 @@ export default class Service { public transactionService: TransactionService; + public categoryService: CategoryService; + constructor() { this.userService = new UserService(); this.walletService = new WalletService(); this.transactionService = new TransactionService(); + this.categoryService = new CategoryService(); } } diff --git a/typescript-practice/src/ts/services/transactionService.ts b/typescript-practice/src/ts/services/transactionService.ts index c12f9ff..7743e16 100644 --- a/typescript-practice/src/ts/services/transactionService.ts +++ b/typescript-practice/src/ts/services/transactionService.ts @@ -18,7 +18,7 @@ export default class TransactionService extends CommonService { async getListTransactionByIdUser( idUser: number, - ): Promise { + ): Promise { const results = this.getListDataFromProp( 'idUser', idUser.toString(), diff --git a/typescript-practice/src/ts/views/home/budgetView.ts b/typescript-practice/src/ts/views/home/budgetView.ts new file mode 100644 index 0000000..ab062e1 --- /dev/null +++ b/typescript-practice/src/ts/views/home/budgetView.ts @@ -0,0 +1,205 @@ +import Transaction from '../../models/transaction'; +import { renderRequiredText } from '../../helpers/validatorForm'; +import { Data, TError } from 'global/types'; +import Transform from 'helpers/transform'; +import Wallet from 'models/wallet'; +import User from 'models/user'; +import { DEFAULT_CATEGORY } from 'constants/config'; +import { + ADD_TRANSACTION_SUCCESS, + DEFAULT_MESSAGE, +} from 'constants/messages/dialog'; + +export default class BudgetView { + budgetDialog: HTMLDialogElement | null = null; + + addBudgetBtn: HTMLElement | null = null; + + transform: Transform | null = null; + + toggleLoaderSpinner: (() => void) | null = null; + + saveTransaction: ((transaction: Transaction) => Promise) | null = null; + + loadTransactionData: (() => Promise) | null = null; + + updateAmountWallet: (() => Promise) | null = null; + + loadData: (() => Promise) | null = null; + + showSuccessToast: ((title: string, message: string) => void) | null = null; + + showErrorToast: ((error: string | TError) => void) | null = null; + + wallet: Wallet | null = null; + + user: User | null = null; + + constructor() { + this.budgetDialog = document.getElementById( + 'budgetDialog', + ) as HTMLDialogElement; + this.addBudgetBtn = document.getElementById('addBudget'); + + this.handlerEventBudgetView(); + } + + initFunction( + showErrorToast: (error: string | TError) => void, + showSuccessToast: (title: string, message: string) => void, + toggleLoaderSpinner: () => void, + saveTransaction: ((transaction: Transaction) => Promise) | null, + loadTransactionData: () => Promise, + updateAmountWallet: () => Promise, + loadData: () => Promise, + transform: Transform | null, + ) { + 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! }; + + this.transform!.onSendSignal('budgetView', data); + } + + updateData(data: Data) { + if (data.wallet) this.wallet = data.wallet; + + if (data.user) this.user = data.user; + } + + 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 form = document.getElementById('formAddBudget') as HTMLFormElement; + const formAddBudget = new FormData(form); + const date = formAddBudget.get('selected_date') as string; + const amount = formAddBudget.get('amount') as string; + const note = formAddBudget.get('note') as string; + + // Validate data user input + + if (this.validateBudgetForm(date, +amount)) { + this.toggleLoaderSpinner!(); // Enable loader spinner + this.budgetDialog!.close(); // Close dialog + + const transaction = new Transaction( + 0, + DEFAULT_CATEGORY.INCOME, + date, + note, + +amount, + +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!(ADD_TRANSACTION_SUCCESS, DEFAULT_MESSAGE); + + (document.getElementById('formAddBudget')!).reset(); + } + } catch (error) { + this.showErrorToast!(error as string | TError); + } + } + + validateBudgetForm(date: string, amount: number) { + 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(); + } +} diff --git a/typescript-practice/src/ts/views/home/categoryView.ts b/typescript-practice/src/ts/views/home/categoryView.ts new file mode 100644 index 0000000..759428c --- /dev/null +++ b/typescript-practice/src/ts/views/home/categoryView.ts @@ -0,0 +1,167 @@ +import Transform from 'helpers/transform'; +import { REMOVE_CATEGORY } from '../../constants/config'; +import Category from 'models/category'; +import { Data } from 'global/types'; + +export default class CategoryView { + categoryDialog: HTMLDialogElement | null = null; + + categoryField: HTMLElement | null = null; + + closeIcon: HTMLElement | null = null; + + getAllCategory: (() => Promise) | null = null; + + transform: Transform | null = null; + + listCategory: Category[] | null = null; + + categorySelected: string; + + constructor() { + this.categorySelected = ''; + + this.categoryDialog = document.getElementById( + 'categoryDialog', + ) as HTMLDialogElement; + this.categoryField = document.getElementById('selectCategory'); + this.closeIcon = document.querySelector('.close-icon') as HTMLElement; + + this.handlerEventCategoryDialog(); + this.addEventSelectCategoryDialog(); + } + + initFunction( + getAllCategory: () => Promise, + transform: Transform, + ) { + this.getAllCategory = getAllCategory; + this.transform = transform; + } + + sendData() { + const data: Data = { listCategories: 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: HTMLDataElement = + this.categoryDialog!.querySelector("[name='category']")!; + const searchValue = searchCategoryEl.value.trim().toLowerCase(); + let newListCategory: Category[] = []; + + 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.categorySelected, newListCategory); + } + + renderCategoryList( + categorySelected?: string, + 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', + ) as HTMLImageElement; + const categoryNameEl = this.categoryField!.querySelector( + '.category-name', + ) as HTMLDataElement; + + categoryListEl!.addEventListener('click', (e) => { + const categoryItem = (e.target).closest( + '.category-item', + ) as HTMLElement; + + 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.categorySelected = categoryNameEl.value; + + this.renderCategoryList(this.categorySelected); + } + this.categoryDialog!.showModal(); + }); + + this.closeIcon!.addEventListener('click', () => { + this.categoryDialog!.close(); + }); + } +} diff --git a/typescript-practice/src/ts/views/home/homeView.ts b/typescript-practice/src/ts/views/home/homeView.ts index 7687b59..5a457fe 100644 --- a/typescript-practice/src/ts/views/home/homeView.ts +++ b/typescript-practice/src/ts/views/home/homeView.ts @@ -10,49 +10,79 @@ import User from 'models/user'; import { Data, TError } from 'global/types'; import { DEFAULT_TITLE_ERROR_TOAST } from 'constants/messages/dialog'; import { redirectToLoginPage } from 'helpers/url'; +import Category from 'models/category'; +import CategoryView from './categoryView'; +import BudgetView from './budgetView'; +import TransactionView from './transactionView'; +import SummaryTabView from './summaryTabView'; +import TransactionDetail from 'models/transactionDetail'; +import TransactionTabView from './transactionTabView'; export default class HomeView extends CommonView { - private _tabs: NodeListOf; + walletView: WalletView; - private _allContent: NodeListOf; + categoryView: CategoryView; - private _cancelBtns: NodeListOf; + budgetView: BudgetView; - private _dialogs: NodeListOf; + transactionView: TransactionView; - private _amountInputs: NodeListOf; + summaryTabView: SummaryTabView; - private _walletView: WalletView; + transactionTabView: TransactionTabView; - private _user: User | null = null; + tabs: NodeListOf; - private _wallet: Wallet | null = null; + allContent: NodeListOf; - private _getInfoUserLogin: (() => Promise) | null = null; + cancelBtns: NodeListOf; - private _getWalletByIdUser: - | ((idUser: number) => Promise) + dialogs: NodeListOf; + + amountInputs: NodeListOf; + + user: User | null = null; + + wallet: Wallet | null = null; + + listTransactions: Transaction[] | null = null; + + transactionDetails: TransactionDetail[] = []; + + getInfoUserLogin: (() => Promise) | null = null; + + getWalletByIdUser: ((idUser: number) => Promise) | null = null; + + getAllCategory: (() => Promise) | null = null; + + getAllTransactions: + | ((idUser: number) => Promise) | null = null; - private _saveWallet: ((wallet: Wallet) => Promise) | null = null; + deleteTransaction: ((idTransaction: number) => Promise) | null = null; - private _saveTransaction: - | ((transaction: Transaction) => Promise) - | null = null; + saveWallet: ((wallet: Wallet) => Promise) | null = null; - private _transform: Transform | null = null; + saveTransaction: ((transaction: Transaction) => Promise) | null = null; + + transform: Transform | null = null; constructor() { super(); - this._tabs = document.querySelectorAll('.app__tab-item'); - this._allContent = document.querySelectorAll('.app__content-item'); - this._cancelBtns = document.querySelectorAll('.form__cancel-btn'); - this._dialogs = document.querySelectorAll('.dialog'); + this.tabs = document.querySelectorAll('.app__tab-item'); + this.allContent = document.querySelectorAll('.app__content-item'); + this.cancelBtns = document.querySelectorAll('.form__cancel-btn'); + this.dialogs = document.querySelectorAll('.dialog'); - this._amountInputs = document.querySelectorAll('.form__input-balance'); + this.amountInputs = document.querySelectorAll('.form__input-balance'); - this._walletView = new WalletView(); + this.walletView = new WalletView(); + this.categoryView = new CategoryView(); + this.budgetView = new BudgetView(); + this.transactionView = new TransactionView(this.categoryView); + this.summaryTabView = new SummaryTabView(); + this.transactionTabView = new TransactionTabView(this.transactionView); this.initToast(); this.initLoader(); @@ -62,27 +92,78 @@ export default class HomeView extends CommonView { initFunction( getInfoUserLogin: () => Promise, getWalletByIdUser: (idUser: number) => Promise, + getAllCategory: () => Promise, + getAllTransactions: (idUser: number) => Promise, saveWallet: (wallet: Wallet) => Promise, saveTransaction: (transaction: Transaction) => Promise, + deleteTransaction: (idTransaction: number) => Promise, transform: Transform, ) { - this._getInfoUserLogin = getInfoUserLogin; - this._getWalletByIdUser = getWalletByIdUser; - this._saveWallet = saveWallet; - this._saveTransaction = saveTransaction; + this.getInfoUserLogin = getInfoUserLogin; + this.getWalletByIdUser = getWalletByIdUser; + this.getAllCategory = getAllCategory; + this.getAllTransactions = getAllTransactions; + this.saveWallet = saveWallet; + this.saveTransaction = saveTransaction; + this.deleteTransaction = deleteTransaction; - this._transform = transform; + this.transform = transform; // Init function for child view this.initWalletViewFunction(); + this.initBudgetViewFunction(); + this.initTransactionViewFunction(); + this.initCategoryViewFunction(); + this.initSummaryTabViewFunction(); + this.initTransactionTabViewFunction(); + } + + initTransactionViewFunction() { + this.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() { + this.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, + ); + } + + initCategoryViewFunction() { + this.categoryView.initFunction(this.getAllCategory!, this.transform!); + } + + initSummaryTabViewFunction() { + this.summaryTabView.initFunction(this.transform!); + } + + initTransactionTabViewFunction() { + this.transactionTabView.initFunction(this.transform!); } initWalletViewFunction() { - this._walletView.initFunction( - this._transform, + this.walletView.initFunction( + this.transform, this.toggleLoaderSpinner.bind(this), - this._saveWallet, - this._saveTransaction, + this.saveWallet, + this.saveTransaction, + this.loadTransactionData.bind(this), this.loadData.bind(this), this.loadEvent.bind(this), this.showSuccessToast.bind(this), @@ -92,19 +173,29 @@ export default class HomeView extends CommonView { subscribeListenerData() { this.subscribe(); - this._walletView.subscribe(); + this.transactionView.subscribe(); + this.budgetView.subscribe(); + this.summaryTabView.subscribe(); + this.transactionTabView.subscribe(); + this.walletView.subscribe(); } async loadData() { // Send data to other class this.sendData(); + await this.categoryView.loadCategory(); + await this.loadWalletUser(); + + this.summaryTabView.load(); + + this.transactionTabView.loadTransactionTab(); } async loadPage() { this.toggleLoaderSpinner(); - const user = await this._getInfoUserLogin!(); + const user = await this.getInfoUserLogin!(); if (!user) { // If user not login yet @@ -112,16 +203,17 @@ export default class HomeView extends CommonView { window.location.replace('/login'); } else { // If user already login - this._user = user; - this._wallet = await this._getWalletByIdUser!(user.id); + this.user = user; + this.wallet = await this.getWalletByIdUser!(user.id); this.sendData(); // Check user's wallet if have or not - if (!this._wallet) { + if (!this.wallet) { // Show add wallet dialog - this._walletView.showDialog(); + this.walletView.showDialog(); } else { // Init data + await this.loadTransactionData(); await this.loadData(); // Load event page @@ -135,31 +227,32 @@ export default class HomeView extends CommonView { // ---------------------LOAD DATA---------------------// subscribe() { - this._transform!.create('homeView', this.updateData.bind(this)); + this.transform!.create('homeView', this.updateData.bind(this)); } sendData() { const data: Data = { - wallet: this._wallet!, - user: this._user!, + wallet: this.wallet!, + listTransactions: this.listTransactions!, + user: this.user!, }; - this._transform!.onSendSignal('homeView', data); + this.transform!.onSendSignal('homeView', data); } updateData(data: Data) { - if (data.wallet) this._wallet = data.wallet; + if (data.wallet) this.wallet = data.wallet; - if (data.user) this._user = data.user; + if (data.user) this.user = data.user; } /** * Load wallet user */ async loadWalletUser() { - const wallet = this._wallet - ? this._wallet - : await this._getWalletByIdUser!(this._user!.id); + const wallet = this.wallet + ? this.wallet + : await this.getWalletByIdUser!(this.user!.id); const walletName = document.querySelector('.wallet__name'); const walletPrice = document.querySelector('.wallet__price'); const walletNameValue = wallet!.walletName; @@ -167,7 +260,7 @@ export default class HomeView extends CommonView { const sign = walletAmountValue >= 0 ? '+' : '-'; - this._wallet = wallet; // Make wallet into global variable + this.wallet = wallet; // Make wallet into global variable walletName!.textContent = walletNameValue; walletPrice!.textContent = `${sign}$ ${formatNumber( @@ -175,11 +268,38 @@ export default class HomeView extends CommonView { )}`; // Math.abs(walletAmountValue) to keep the value always > 0 // Update amount wallet into database - this._saveWallet!(this._wallet!); + this.saveWallet!(this.wallet!); } - // ---------------------END---------------------// + async loadTransactionData() { + this.listTransactions = await this.getAllTransactions!(this.wallet!.idUser); + this.sendData(); + } + + async updateAmountWallet() { + let inflow = 0; + let outflow = 0; + // Init data first + this.transactionDetails = + this.transactionTabView!.loadTransactionDetailsData(); + + this.transactionDetails.forEach((transaction) => { + if (transaction.totalAmount >= 0) { + inflow += transaction.totalAmount; + } else { + outflow -= transaction.totalAmount; + } + }); + // Reassign value for wallet user; + this.wallet!.inflow = inflow; + this.wallet!.outflow = -outflow; + await this.saveWallet!(this.wallet!); + } + + // ---------------------END--------------------- // + + // ---------------------TOAST--------------------- // showSuccessToast(title: string, message: string) { const typeToast = TypeToast.success; const btnContent = BTN_CONTENT.OK; @@ -214,8 +334,9 @@ export default class HomeView extends CommonView { this.toastBtn.removeEventListener('click', redirectToLoginPage); } } + // ---------------------END--------------------- // - // ------------------------------- HANDLER EVENT ------------------------------- // + // --------------------- HANDLER EVENT --------------------- // loadEvent() { this.addCommonEventPage(); @@ -226,7 +347,7 @@ export default class HomeView extends CommonView { * Handle event when click on tabs */ handlerTabsTransfer() { - this._tabs.forEach((tab, index) => { + this.tabs.forEach((tab, index) => { tab.addEventListener('click', (e: Event) => { this.removeActiveTab(); tab.classList.add('active'); @@ -240,17 +361,17 @@ export default class HomeView extends CommonView { line.style.width = `${eventTarget.offsetWidth}px`; line.style.left = `${eventTarget.offsetLeft}px`; - this._allContent.forEach((content) => { + this.allContent.forEach((content) => { content.classList.remove('active'); }); - this._allContent[index].classList.add('active'); + this.allContent[index].classList.add('active'); }); }); } addCommonEventPage() { // Add event close dialog when click outside - this._dialogs.forEach((dialog) => { + this.dialogs.forEach((dialog) => { dialog.addEventListener('click', (e) => { const dialogDimensions = dialog.getBoundingClientRect(); const mouseEvent = e as MouseEvent; @@ -265,14 +386,14 @@ export default class HomeView extends CommonView { }); }); - this._cancelBtns.forEach((btn) => { + this.cancelBtns.forEach((btn) => { btn.addEventListener('click', () => { this.closeAllDialog(); }); }); // Prevent input =,-,e into amount input - this._amountInputs.forEach((item) => { + this.amountInputs.forEach((item) => { item.addEventListener( 'keypress', (e) => @@ -298,13 +419,13 @@ export default class HomeView extends CommonView { } closeAllDialog() { - this._dialogs.forEach((dialog) => { + this.dialogs.forEach((dialog) => { (dialog).close(); }); } removeActiveTab() { - this._tabs.forEach((tab) => { + this.tabs.forEach((tab) => { tab.classList.remove('active'); }); } diff --git a/typescript-practice/src/ts/views/home/summaryTabView.ts b/typescript-practice/src/ts/views/home/summaryTabView.ts new file mode 100644 index 0000000..75277e0 --- /dev/null +++ b/typescript-practice/src/ts/views/home/summaryTabView.ts @@ -0,0 +1,48 @@ +import Transform from 'helpers/transform'; +import { formatNumber } from '../../helpers/data'; +import { Data } from 'global/types'; +import Wallet from 'models/wallet'; +import Transaction from 'models/transaction'; +import Category from 'models/category'; + +export default class SummaryTabView { + transform: Transform | null = null; + + wallet: Wallet | null = null; + + listTransactions: Transaction[] = []; + + listCategories: Category[] = []; + + initFunction(transform: Transform) { + this.transform = transform; + } + + subscribe() { + this.transform!.create('summaryTabView', this.updateData.bind(this)); + } + + updateData(data: Data) { + if (data.wallet) this.wallet = data.wallet; + + if (data.listTransactions) this.listTransactions = data.listTransactions; + + if (data.listCategories) this.listCategories = data.listCategories; + } + + 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), + )}`; + } +} diff --git a/typescript-practice/src/ts/views/home/transactionTabView.ts b/typescript-practice/src/ts/views/home/transactionTabView.ts new file mode 100644 index 0000000..594d1c4 --- /dev/null +++ b/typescript-practice/src/ts/views/home/transactionTabView.ts @@ -0,0 +1,192 @@ +import Transform from 'helpers/transform'; +import { + createTransactionDetailObject, + getAllCategoryNameInTransactions, + getAllTransactionByCategoryName, +} from '../../helpers/data'; +import { formatNumber } from '../../helpers/data'; +import TransactionView from './transactionView'; +import { Data } from 'global/types'; +import Wallet from 'models/wallet'; +import Transaction from 'models/transaction'; +import Category from 'models/category'; +import TransactionDetail from 'models/transactionDetail'; + +export default class TransactionTabView { + transactionView: TransactionView; + + transform: Transform | null = null; + + wallet: Wallet | null = null; + + listTransactions: Transaction[] = []; + + listCategories: Category[] = []; + + transactionDetails: TransactionDetail[] = []; + + constructor(transactionView: TransactionView) { + this.transactionView = transactionView; + this.addEventTransactionItem(); + } + + initFunction(transform: Transform) { + this.transform = transform; + } + + subscribe() { + this.transform!.create('transactionTabView', this.updateData.bind(this)); + } + + updateData(data: Data) { + if (data.wallet) this.wallet = data.wallet; + + if (data.listTransactions) this.listTransactions = data.listTransactions; + + if (data.listCategories) this.listCategories = data.listCategories; + } + + async loadTransactionTab() { + // Load category + // Init data first + this.transactionDetails = this.loadTransactionDetailsData(); + const transactionEl = document.querySelector('.transaction')!; + const listTransactionDetailEl = + transactionEl.querySelector('.transaction__list')!; + const markup: string[] = []; + + 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(): TransactionDetail[] { + // Get all category user have in transactions + const listCategoryInTransaction = getAllCategoryNameInTransactions( + this.listTransactions, + ); + + // Create transactions details object + const tempList: TransactionDetail[] = listCategoryInTransaction.map( + (categoryName) => { + // Get category object from list category has been loaded. + const category = this.listCategories.filter( + (item) => item.name === categoryName, + ); + + const transactions = getAllTransactionByCategoryName( + categoryName, + this.listTransactions, + ); + + return createTransactionDetailObject( + Object.assign({}, ...category), + transactions, + ); + }, + ); + + tempList.sort((a, b) => b.transactions[0].id - a.transactions[0].id); + + return tempList; + } + + // eslint-disable-next-line class-methods-use-this + transactionDetailMarkup(transactionDetail: TransactionDetail) { + const itemTransaction = () => { + const listMarkup: string[] = []; + + 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))}

+
+
+ + ${itemTransaction()} +
+ `; + } + + // 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: HTMLInputElement = item.querySelector( + '.transaction__category-name', + )!; + + if (transactionTime) { + const idTransaction = +(transactionTime).dataset.id!; + + // If it is income transaction, don't show dialog + if ( + categoryNameEl.textContent && + categoryNameEl.textContent.trim() !== 'Income' + ) + this.transactionView.showTransactionDialog(idTransaction); + } + }); + }); + } + } +} diff --git a/typescript-practice/src/ts/views/home/transactionView.ts b/typescript-practice/src/ts/views/home/transactionView.ts new file mode 100644 index 0000000..80e97d1 --- /dev/null +++ b/typescript-practice/src/ts/views/home/transactionView.ts @@ -0,0 +1,371 @@ +import Transaction from 'models/transaction'; +import defaultCategoryIcon from '../../../assets/images/question-icon.svg'; +import CategoryView from './categoryView'; +import Transform from 'helpers/transform'; +import { Data, TError } from 'global/types'; +import Wallet from 'models/wallet'; +import Category from 'models/category'; +import { + ADD_TRANSACTION_SUCCESS, + DEFAULT_MESSAGE, + UPDATE_TRANSACTION_SUCCESS, +} from 'constants/messages/dialog'; +import { renderRequiredText } from 'helpers/validatorForm'; + +export default class TransactionView { + wallet: Wallet | null = null; + + listTransactions: Transaction[] = []; + + listCategories: Category[] = []; + + categoryView: CategoryView; + + addTransactionBtn: HTMLElement | null = null; + + transactionDialog: HTMLDialogElement | null = null; + + transactionForm: HTMLFormElement | null = null; + + toggleLoaderSpinner: (() => void) | null = null; + + deleteTransaction: ((idTransaction: number) => Promise) | null = null; + + loadTransactionData: (() => Promise) | null = null; + + updateAmountWallet: (() => Promise) | null = null; + + loadData: (() => Promise) | null = null; + + showSuccessToast: ((title: string, message: string) => void) | null = null; + + showErrorToast: ((error: string | TError) => void) | null = null; + + saveTransaction: ((transaction: Transaction) => Promise) | null = null; + + transform: Transform | null = null; + + constructor(categoryView: CategoryView) { + this.addTransactionBtn = document.getElementById('addTransaction'); + this.transactionDialog = document.getElementById( + 'transactionDialog', + ) as HTMLDialogElement; + this.transactionForm = ( + document.getElementById('formAddTransaction') + ); + + this.handlerEventTransactionDialog(); + + this.categoryView = categoryView; + } + + initFunction( + toggleLoaderSpinner: () => void, + deleteTransaction: (idTransaction: number) => Promise, + loadTransactionData: () => Promise, + updateAmountWallet: () => Promise, + loadData: () => Promise, + showSuccessToast: (title: string, message: string) => void, + showErrorToast: (error: string | TError) => void, + saveTransaction: (transaction: Transaction) => Promise, + transform: Transform | null, + ) { + 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: Data = { + wallet: this.wallet!, + listTransactions: this.listTransactions!, + }; + + this.transform!.onSendSignal('transactionView', data); + } + + updateData(data: Data) { + if (data.wallet) this.wallet = data.wallet; + + if (data.listTransactions) this.listTransactions = data.listTransactions; + + if (data.listCategories) this.listCategories = data.listCategories; + } + + 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', + ) as Element; + + deleteTransactionBtn.addEventListener('click', async () => { + const idEl = this.transactionDialog!.querySelector( + "[name='id_transaction']", + ) as HTMLDataElement; + + 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!', DEFAULT_MESSAGE); + } catch (error) { + this.showErrorToast!(error as string | TError); + } + 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: number | null) { + let categoryName: string; + + 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.listCategories.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).toString(); + 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( + +idEl.value, + categoryNameEl.value, + dateEl.value, + noteEl.value, + +amount, + +this.wallet!.idUser, + ); + + await this.saveTransaction!(transaction); + + // Reload data + await this.loadTransactionData!(); + await this.updateAmountWallet!(); + await this.loadData!(); + + if (!idEl.value) { + // Add success + this.showSuccessToast!(ADD_TRANSACTION_SUCCESS, DEFAULT_MESSAGE); + } else { + // Update success + this.showSuccessToast!(UPDATE_TRANSACTION_SUCCESS, DEFAULT_MESSAGE); + } + + this.toggleLoaderSpinner!(); + this.clearInputTransactionForm(); + } + } catch (error) { + this.showErrorToast!(error as string | TError); + this.toggleLoaderSpinner!(); + } + } + + validateTransactionForm(date: string, categoryName: string, amount: number) { + 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; + const keySearchCategory: string = ''; // Delete keyword search + this.categoryView.renderCategoryList( + keySearchCategory, + this.listCategories, + ); + this.transactionForm!.reset(); + } + + showTransactionDialog(idTransaction: number | null = 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(); + } +} diff --git a/typescript-practice/src/ts/views/home/walletView.ts b/typescript-practice/src/ts/views/home/walletView.ts index 6abd27e..26c1795 100644 --- a/typescript-practice/src/ts/views/home/walletView.ts +++ b/typescript-practice/src/ts/views/home/walletView.ts @@ -20,6 +20,8 @@ export default class WalletView { | ((transaction: Transaction) => Promise) | null = null; + private _loadTransactionData: (() => Promise) | null = null; + private _loadData: (() => Promise) | null = null; private _loadEvent: (() => void) | null = null; @@ -46,6 +48,7 @@ export default class WalletView { toggleLoaderSpinner: () => void, saveWallet: ((wallet: Wallet) => Promise) | null, saveTransaction: ((transaction: Transaction) => Promise) | null, + loadTransactionData: () => Promise, loadData: () => Promise, loadEvent: () => void, showSuccessToast: (title: string, message: string) => void, @@ -55,6 +58,7 @@ export default class WalletView { this._toggleLoaderSpinner = toggleLoaderSpinner; this._saveWallet = saveWallet; this._saveTransaction = saveTransaction; + this._loadTransactionData = loadTransactionData; this._loadData = loadData; this._loadEvent = loadEvent; this._showSuccessToast = showSuccessToast; @@ -127,8 +131,7 @@ export default class WalletView { this._walletDialog!.close(); this._toggleLoaderSpinner!(); - const wallet = new Wallet(walletName, +amount, 0, this._user!.id) - .toObject as Wallet; + const wallet = new Wallet(walletName, +amount, 0, this._user!.id); this._wallet = wallet; this.sendData(); @@ -136,17 +139,20 @@ export default class WalletView { await this._saveWallet!(wallet); // Transaction info const transaction = new Transaction( + 0, 'Income', new Date().toISOString().slice(0, 10), FIRST_ADD_WALLET_NOTE, +amount, this._user!.id, - ).toObject; + ); await this._saveTransaction!(transaction); // Load data and event + await this._loadTransactionData!(); await this._loadData!(); this._loadEvent!(); + this._showSuccessToast!(ADD_WALLET_SUCCESS, DEFAULT_MESSAGE); this._toggleLoaderSpinner!();