Merge pull request #8 from Nez27/feat/add-home-view

Implement home page.
This commit is contained in:
Loi Phan
2023-10-02 20:17:40 +07:00
committed by GitHub
32 changed files with 2128 additions and 75 deletions
+1
View File
@@ -0,0 +1 @@
declare module '*.svg';
+1
View File
@@ -284,5 +284,6 @@
</main>
<!-- End app -->
</div>
<script type="module" src="../ts/index.ts"></script>
</body>
</html>
+5 -4
View File
@@ -3,14 +3,15 @@ import Service from './services/index';
import View from './views/index';
export default class App {
private _controller: Controller;
controller: Controller;
constructor() {
this._controller = new Controller(new Service(), new View());
this.controller = new Controller(new Service(), new View());
}
start() {
this._controller.registerController.init();
this._controller.loginController.init();
this.controller.registerController.init();
this.controller.loginController.init();
this.controller.homeController.init();
}
}
@@ -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',
];
@@ -0,0 +1 @@
export const FIRST_ADD_WALLET_NOTE: string = 'Init Wallet';
@@ -0,0 +1,67 @@
import HomeView from 'views/home/homeView';
import Transform from '../helpers/transform';
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;
constructor(
public service: Service,
public view: View,
) {
this.service = service;
this.homeView = view.homeView;
}
handlerGetInfoUserLogin() {
return this.service.userService.getInfoUserLogin();
}
handlerGetWalletByIdUser(idUser: string): Promise<Wallet | null> {
return this.service.walletService.getWalletByIdUser(idUser);
}
handlerSaveWallet(wallet: Wallet): Promise<void> {
return this.service.walletService.saveWallet(wallet);
}
handlerSaveTransaction(transaction: Transaction): Promise<void> {
return this.service.transactionService.saveTransaction(transaction);
}
handlerGetAllCategory(): Promise<Category[] | null> {
return this.service.categoryService.getAllCategory();
}
handlerGetAllTransactions(idUser: string): Promise<Transaction[] | null> {
return this.service.transactionService.getListTransactionByIdUser(idUser);
}
handlerDeleteTransaction(idTransaction: string): Promise<void> {
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(),
);
this.homeView.loadPage();
// Subscribe listener data update
this.homeView.subscribeListenerData();
}
}
}
@@ -2,17 +2,21 @@ import Service from 'services';
import RegisterController from './registerController';
import View from 'views';
import LoginController from './loginController';
import HomeController from './homeController';
export default class Controller {
public registerController: RegisterController;
public loginController: LoginController;
public homeController: HomeController;
constructor(
public service: Service,
public view: View,
) {
this.registerController = new RegisterController(service, view);
this.loginController = new LoginController(service, view);
this.homeController = new HomeController(service, view);
}
}
+30 -3
View File
@@ -1,3 +1,8 @@
import Category from 'models/category';
import Transaction from 'models/transaction';
import User from 'models/user';
import Wallet from 'models/wallet';
export interface IDataObject<T> {
id: string;
data: T;
@@ -10,14 +15,14 @@ export class DataObject<T> {
constructor(dataObject: IDataObject<T>) {
this.id = dataObject?.id ?? null;
this.data = dataObject.data as T;
this.data = <T>dataObject.data;
}
}
export type TError = {
export interface TError {
title: string;
message: string;
};
}
export class CustomError extends Error {
constructor(
@@ -27,3 +32,25 @@ export class CustomError extends Error {
super(message);
}
}
export type TSignal = {
[key: string]: {
name: string;
handler: (value: Data) => void;
};
};
export interface Data {
wallet?: Wallet;
listTransactions?: Transaction[];
listCategories?: Category[];
user?: User;
}
export interface ItemTransaction {
id: string;
day: string;
fullDateString: string;
note: string;
amount: number;
}
+96 -3
View File
@@ -1,7 +1,12 @@
import { DataObject, IDataObject } from '../global/types';
import localStorageService from 'services/localStorageService';
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 createIdUser = (): number => {
return new Date().getTime();
export const generateId = (): string => {
return new Date().getTime().toString();
};
export const convertDataObjectToModel = <T>(dataObj: DataObject<T>): T => {
@@ -30,3 +35,91 @@ export const createToken = (): string => {
}
return token;
};
export const formatNumber = (number: number): string => {
return number.toLocaleString(undefined, {
minimumFractionDigits: 2,
});
};
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) => parseInt(b.id) - parseInt(a.id));
return results;
};
return new TransactionDetail(
category.name,
category.url,
totalTransaction,
totalAmount(),
listTransaction(),
);
};
@@ -0,0 +1,33 @@
import { TSignal, Data } from 'global/types';
export default class Transform {
public signal: TSignal;
constructor() {
this.signal = {};
}
onSendSignal(fromClass: string, value: Data) {
Object.keys(this.signal).forEach((key) => {
const item = this.signal[key as keyof TSignal];
// Same receiver
if (item.name === fromClass) {
return;
}
// Send signal
const { handler } = this.signal[key];
if (handler) {
handler(value);
}
});
}
create(
className: string,
handler: ((value: Data) => void) | null = null,
): void {
if (handler) this.signal[className] = { name: className, handler };
}
}
@@ -0,0 +1,13 @@
export default class Category {
readonly id: string;
url: string;
name: string;
constructor(id: string, url: string, name: string) {
this.id = id;
this.url = url;
this.name = name;
}
}
@@ -0,0 +1,19 @@
import { generateId } from 'helpers/data';
export default class Transaction {
constructor(
public id: string,
public categoryName: string,
public date: string,
public note: string,
public amount: number,
public idUser: string,
) {
this.id = id ? id : generateId();
this.categoryName = categoryName;
this.date = date;
this.note = note;
this.amount = amount;
this.idUser = idUser;
}
}
@@ -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;
}
}
+9 -29
View File
@@ -1,38 +1,18 @@
import { createIdUser } from '../helpers/data';
import { generateId } from '../helpers/data';
export default class User {
private readonly _id: number;
id: string;
private _email: string;
email: string;
private _password: string;
password: string;
private _accessToken: string;
accessToken: string;
constructor(email: string, password: string, accessToken?: string) {
this._id = createIdUser();
this._email = email;
this._password = password || '';
this._accessToken = accessToken || '';
}
get password() {
return this._password;
}
set accessToken(accessToken: string) {
this._accessToken = accessToken;
}
get accessToken() {
return this._accessToken;
}
get email() {
return this._email;
}
get id() {
return this._id;
this.id = generateId();
this.email = email;
this.password = password || '';
this.accessToken = accessToken || '';
}
}
@@ -0,0 +1,26 @@
import { generateId } from '../helpers/data';
export default class Wallet {
id: string;
walletName: string;
inflow: number;
outflow: number;
idUser: string;
constructor(
walletName: string,
inflow: number,
outflow: number,
idUser: string,
) {
this.id = generateId();
this.walletName = walletName;
this.inflow = inflow;
this.outflow = outflow;
this.idUser = idUser;
}
}
@@ -0,0 +1,32 @@
import CommonService from './commonService';
import Category from '../models/category';
export default class CategoryService extends CommonService<Category> {
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;
}
}
@@ -68,7 +68,7 @@ export default class CommonService<T> {
) {
this.connectToDb();
const data = this.firebaseService.getDataFromProp(path, property, value);
const result = (await timeOutConnect(data)) as DataObject<T>;
const result = <DataObject<T>>await timeOutConnect(data);
if (result && typeof result === 'object') {
return convertDataObjectToModel(result);
@@ -77,7 +77,7 @@ export default class CommonService<T> {
return null;
}
async getAllDataFromPath(path = this.defaultPath): Promise<object[] | null> {
async getAllDataFromPath(path = this.defaultPath): Promise<T[] | null> {
this.connectToDb();
const results = await timeOutConnect(
@@ -90,7 +90,7 @@ export default class CommonService<T> {
const tempData = data as DataObject<T>;
return convertDataObjectToModel(tempData);
}) as object[];
}) as T[];
}
return null;
@@ -98,9 +98,9 @@ export default class CommonService<T> {
async getListDataFromProp(
property: string,
value: object,
value: string | number,
path: string = this.defaultPath,
): Promise<object[] | null> {
): Promise<T[] | null> {
this.connectToDb();
const results = await timeOutConnect(
@@ -114,7 +114,7 @@ export default class CommonService<T> {
const tempData = data as DataObject<T>;
return convertDataObjectToModel(tempData);
}) as object[];
}) as T[];
}
return null;
@@ -12,16 +12,16 @@ import {
import { DATABASE_URL } from '../constants/config';
class FirebaseService {
private _app: FirebaseApp;
app: FirebaseApp;
private _db: Database;
db: Database;
constructor() {
const firebaseConfig = {
databaseURL: DATABASE_URL,
};
this._app = initializeApp(firebaseConfig);
this._db = getDatabase(this._app);
this.app = initializeApp(firebaseConfig);
this.db = getDatabase(this.app);
}
/**
@@ -31,25 +31,25 @@ class FirebaseService {
* @returns {Promise} Return the resolves when write to database completed
*/
save(data: object, path: string): Promise<void> {
return set(ref(this._db, path), data);
return set(ref(this.db, path), data);
}
delete(id: string, path: string): Promise<void> {
return remove(ref(this._db, path + id));
return remove(ref(this.db, path + id));
}
/**
* Disconnect to database
*/
disconnect(): void {
goOffline(this._db);
goOffline(this.db);
}
/**
* Reconnect to database
*/
reconnect(): void {
goOnline(this._db);
goOnline(this.db);
}
/**
@@ -66,7 +66,7 @@ class FirebaseService {
): Promise<object | null> {
return new Promise((resolve) => {
onValue(
ref(this._db, path),
ref(this.db, path),
(snapshot) => {
let id: string | null = null;
let data: object | null = null;
@@ -104,7 +104,7 @@ class FirebaseService {
getDataFromId(id: string, path: string): Promise<object> {
return new Promise((resolve) => {
onValue(
ref(this._db, path + id),
ref(this.db, path + id),
(snapshot) => {
resolve(snapshot.val());
},
@@ -118,7 +118,7 @@ class FirebaseService {
getAllDataFromPath(path: string): Promise<object[]> {
return new Promise((resolve) => {
onValue(
ref(this._db, path),
ref(this.db, path),
(snapshot) => {
const listData: object[] = [];
@@ -145,11 +145,11 @@ class FirebaseService {
getListDataFromProp(
path: string,
property: string,
value: object,
value: string | number,
): Promise<object[]> {
return new Promise((resolve) => {
onValue(
ref(this._db, path),
ref(this.db, path),
(snapshot) => {
let id: string;
let data: object;
@@ -1,9 +1,21 @@
import CategoryService from './categoryService';
import TransactionService from './transactionService';
import UserService from './userService';
import WalletService from './walletService';
export default class Service {
public userService: UserService;
public walletService: WalletService;
public transactionService: TransactionService;
public categoryService: CategoryService;
constructor() {
this.userService = new UserService();
this.walletService = new WalletService();
this.transactionService = new TransactionService();
this.categoryService = new CategoryService();
}
}
@@ -1,24 +1,24 @@
class LocalStorageService {
private _localStorage: Storage;
localStorage: Storage;
constructor() {
this._localStorage = localStorage;
this.localStorage = localStorage;
}
add(key: string, value: string): void {
this._localStorage.setItem(key, value);
this.localStorage.setItem(key, value);
}
get(key: string): string | null {
return this._localStorage.getItem(key);
return this.localStorage.getItem(key);
}
remove(key: string): void {
this._localStorage.removeItem(key);
this.localStorage.removeItem(key);
}
clear(): void {
this._localStorage.clear();
this.localStorage.clear();
}
}
@@ -0,0 +1,34 @@
import Transaction from 'models/transaction';
import CommonService from './commonService';
export default class TransactionService extends CommonService<Transaction> {
constructor() {
super();
this.defaultPath = 'transactions/';
}
/**
* Save transaction into database
* @param {Object} transaction The wallet object need to be saved into database
*/
async saveTransaction(transaction: Transaction): Promise<void> {
await this.save(transaction);
}
async getListTransactionByIdUser(
idUser: string,
): Promise<Transaction[] | null> {
const results = await this.getListDataFromProp(
'idUser',
idUser,
this.defaultPath,
);
return results || null;
}
async deleteTransaction(idTransaction: string) {
await this.deleteData(idTransaction);
}
}
@@ -5,10 +5,10 @@ import CommonService from './commonService';
import LocalStorageService from './localStorageService';
export default class UserService {
private _commonService: CommonService<User>;
private commonService: CommonService<User>;
constructor() {
this._commonService = new CommonService<User>('users/');
this.commonService = new CommonService<User>('users/');
}
/**
@@ -16,7 +16,7 @@ export default class UserService {
* @param {Object} user The user object need to be saved into database
*/
async saveUser(user: User): Promise<void> {
await this._commonService.save(user);
await this.commonService.save(user);
}
/**
@@ -36,7 +36,7 @@ export default class UserService {
* @returns {Object || null} Return new User Object if find, otherwise return null.
*/
async getUserByEmail(email: string): Promise<User | null> {
const result = await this._commonService.getDataFromProp('email', email);
const result = await this.commonService.getDataFromProp('email', email);
return result || null;
}
@@ -73,7 +73,7 @@ export default class UserService {
// Add token to user object
newUserData.accessToken = createToken();
this._commonService.save(newUserData);
this.commonService.save(newUserData);
// Add access token to local storage
LocalStorageService.add(
@@ -102,15 +102,11 @@ export default class UserService {
* @returns {Object || null} Return new User Object if find, otherwise return null.
*/
async getUserByToken(accessToken: string): Promise<User | null> {
const result = await this._commonService.getDataFromProp(
const result = await this.commonService.getDataFromProp(
'accessToken',
accessToken,
);
return result || null;
}
static clearAccessToken(): void {
LocalStorageService.remove(LOCAL_STORAGE.ACCESS_TOKEN);
}
}
@@ -0,0 +1,40 @@
import Wallet from '../models/wallet';
import CommonService from './commonService';
export default class WalletService extends CommonService<Wallet> {
constructor() {
super();
this.defaultPath = 'wallets/';
}
/**
* Save wallet into database
* @param {Object} wallet The wallet object need to be saved into database
*/
async saveWallet(wallet: Wallet): Promise<void> {
await this.save(wallet);
}
/**
* Check wallet exist in database
* @param {string} idUser The id user to find user's wallet
* @returns {boolean} Return true if find, otherwise return false
*/
async isValidWallet(idUser: string): Promise<boolean> {
const wallet = await this.getWalletByIdUser(idUser);
return !!wallet;
}
/**
* Get wallet data from idUser
* @param {string} email The id user to find user's wallet
* @returns {Object || null} Return new Wallet Object if find, otherwise return null.
*/
async getWalletByIdUser(idUser: string): Promise<Wallet | null> {
const result = (await this.getDataFromProp('idUser', idUser)) as Wallet;
return result || null;
}
}
@@ -14,7 +14,7 @@ export default class CommonView {
public spinner: HTMLBodyElement | null = null;
constructor() {
this.toastDialog = document.querySelector('.dialog');
this.toastDialog = document.querySelector('.dialog .toast');
this.spinner = null;
}
@@ -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<void>) | null = null;
loadTransactionData: (() => Promise<void>) | null = null;
updateAmountWallet: (() => Promise<void>) | null = null;
loadData: (() => Promise<void>) | 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<void>) | null,
loadTransactionData: () => Promise<void>,
updateAmountWallet: () => Promise<void>,
loadData: () => Promise<void>,
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(
'',
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);
(<HTMLFormElement>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 = (<HTMLDataElement>(
this.budgetDialog!.querySelector('.input-date')!
)).value;
const amount = +(<HTMLDataElement>(
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
(<HTMLInputElement>(
this.budgetDialog!.querySelector("[name='selected_date']")
))!.valueAsDate = new Date();
this.budgetDialog!.showModal();
});
this.addHandlerEventBudgetForm();
}
}
@@ -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<Category[] | null>) | 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<Category[] | null>,
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 = `
<div class="category-item ${
category.name === categorySelected ? 'selected' : ''
}" data-value='${category.name}' data-url='${category.url}'>
<img
class="icon-category"
src="${category.url}"
alt="${category.name} Icon"
/>
<p class="name-category">${category.name}</p>
</div>
`;
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 = (<Element>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();
});
}
}
@@ -0,0 +1,434 @@
import { BTN_CONTENT, TypeToast } from '../../constants/config';
import CommonView from '../commonView';
import { clearAccessToken, formatNumber } from '../../helpers/data';
import WalletView from './walletView';
import Wallet from 'models/wallet';
import Transform from 'helpers/transform';
import Transaction from 'models/transaction';
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 {
walletView: WalletView;
categoryView: CategoryView;
budgetView: BudgetView;
transactionView: TransactionView;
summaryTabView: SummaryTabView;
transactionTabView: TransactionTabView;
tabs: NodeListOf<Element>;
allContent: NodeListOf<Element>;
cancelBtns: NodeListOf<Element>;
dialogs: NodeListOf<Element>;
amountInputs: NodeListOf<Element>;
user: User | null = null;
wallet: Wallet | null = null;
listTransactions: Transaction[] | null = null;
transactionDetails: TransactionDetail[] = [];
getInfoUserLogin: (() => Promise<User | null>) | null = null;
getWalletByIdUser: ((idUser: string) => Promise<Wallet | null>) | null = null;
getAllCategory: (() => Promise<Category[] | null>) | null = null;
getAllTransactions:
| ((idUser: string) => Promise<Transaction[] | null>)
| null = null;
deleteTransaction: ((idTransaction: string) => Promise<void>) | null = null;
saveWallet: ((wallet: Wallet) => Promise<void>) | null = null;
saveTransaction: ((transaction: Transaction) => Promise<void>) | 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.amountInputs = document.querySelectorAll('.form__input-balance');
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();
this.handleEventToast();
}
initFunction(
getInfoUserLogin: () => Promise<User | null>,
getWalletByIdUser: (idUser: string) => Promise<Wallet | null>,
getAllCategory: () => Promise<Category[] | null>,
getAllTransactions: (idUser: string) => Promise<Transaction[] | null>,
saveWallet: (wallet: Wallet) => Promise<void>,
saveTransaction: (transaction: Transaction) => Promise<void>,
deleteTransaction: (idTransaction: string) => Promise<void>,
transform: Transform,
) {
this.getInfoUserLogin = getInfoUserLogin;
this.getWalletByIdUser = getWalletByIdUser;
this.getAllCategory = getAllCategory;
this.getAllTransactions = getAllTransactions;
this.saveWallet = saveWallet;
this.saveTransaction = saveTransaction;
this.deleteTransaction = deleteTransaction;
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.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();
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();
await this.updateAmountWallet();
}
async loadPage() {
this.toggleLoaderSpinner();
const user = await this.getInfoUserLogin!();
if (!user) {
// If user not login yet
clearAccessToken();
window.location.replace('/login');
} else {
// If user already login
this.user = user;
this.wallet = <Wallet>await this.getWalletByIdUser!(user.id);
this.sendData();
// Check user's wallet if have or not
if (!this.wallet) {
// Show add wallet dialog
this.walletView.showDialog();
} else {
// Init data
await this.loadTransactionData();
await this.loadData();
// Load event page
this.loadEvent();
}
}
this.toggleLoaderSpinner();
}
// ---------------------LOAD DATA---------------------//
subscribe() {
this.transform!.create('homeView', this.updateData.bind(this));
}
sendData() {
const data: Data = {
wallet: this.wallet!,
listTransactions: this.listTransactions!,
user: this.user!,
};
this.transform!.onSendSignal('homeView', data);
}
updateData(data: Data) {
if (data.wallet) this.wallet = data.wallet;
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 walletName = document.querySelector('.wallet__name');
const walletPrice = document.querySelector('.wallet__price');
const walletNameValue = wallet!.walletName;
const walletAmountValue = wallet!.inflow + wallet!.outflow;
const sign = walletAmountValue >= 0 ? '+' : '-';
this.wallet = wallet; // Make wallet into global variable
walletName!.textContent = walletNameValue;
walletPrice!.textContent = `${sign}$ ${formatNumber(
Math.abs(walletAmountValue),
)}`; // Math.abs(walletAmountValue) to keep the value always > 0
// Update amount wallet into database
this.saveWallet!(this.wallet!);
}
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;
this.initToastContent(typeToast, title, message, btnContent);
this.toastDialog!.showModal();
}
/**
* Implement error toast in site
* @param {string} content The content will show in error toast
*/
showErrorToast(error: TError | string): void {
const title =
typeof error === 'object' && error.title
? error.title
: DEFAULT_TITLE_ERROR_TOAST;
const content =
typeof error === 'object' && error.message
? error.message
: (error as string);
this.initToastContent(TypeToast.error, title, content, BTN_CONTENT.OK);
if (this.toastDialog && this.toastBtn) {
// Show toast
this.toastDialog.showModal();
// Remove event for toast button
this.toastBtn.removeEventListener('click', redirectToLoginPage);
}
}
// ---------------------END--------------------- //
// --------------------- HANDLER EVENT --------------------- //
loadEvent() {
this.addCommonEventPage();
this.handlerTabsTransfer();
}
/**
* Handle event when click on tabs
*/
handlerTabsTransfer() {
this.tabs.forEach((tab, index) => {
tab.addEventListener('click', (e: Event) => {
this.removeActiveTab();
tab.classList.add('active');
const line = document.querySelector<HTMLElement>(
'.app__line',
) as HTMLElement;
const eventTarget = e.target as HTMLElement;
line.style.width = `${eventTarget.offsetWidth}px`;
line.style.left = `${eventTarget.offsetLeft}px`;
this.allContent.forEach((content) => {
content.classList.remove('active');
});
this.allContent[index].classList.add('active');
});
});
}
addCommonEventPage() {
// Add event close dialog when click outside
this.dialogs.forEach((dialog) => {
dialog.addEventListener('click', (e) => {
const dialogDimensions = dialog.getBoundingClientRect();
const mouseEvent = e as MouseEvent;
if (
mouseEvent.clientX < dialogDimensions.left ||
mouseEvent.clientX > dialogDimensions.right ||
mouseEvent.clientY < dialogDimensions.top ||
mouseEvent.clientY > dialogDimensions.bottom
) {
(<HTMLDialogElement>dialog).close();
}
});
});
this.cancelBtns.forEach((btn) => {
btn.addEventListener('click', () => {
this.closeAllDialog();
});
});
// Prevent input =,-,e into amount input
this.amountInputs.forEach((item) => {
item.addEventListener(
'keypress',
(e) =>
['+', '-', 'e'].includes((<KeyboardEvent>e).key) &&
e.preventDefault(),
);
});
// Prevent user input at category search
const categorySearchEl = document.querySelector(
'.category-name',
) as Element;
categorySearchEl.addEventListener('keydown', (e) => {
e.preventDefault();
});
const logoutBtn = document.querySelector('.logout-text') as Element;
logoutBtn.addEventListener('click', () => {
clearAccessToken();
window.location.replace('/login');
});
}
closeAllDialog() {
this.dialogs.forEach((dialog) => {
(<HTMLDialogElement>dialog).close();
});
}
removeActiveTab() {
this.tabs.forEach((tab) => {
tab.classList.remove('active');
});
}
}
@@ -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),
)}`;
}
}
@@ -0,0 +1,194 @@
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(
<string>categoryName,
this.listTransactions,
);
return createTransactionDetailObject(
Object.assign({}, ...category),
transactions,
);
},
);
tempList.sort(
(a, b) => parseInt(b.transactions[0].id) - parseInt(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 = `
<div class="transaction__time" data-id=${transaction.id}>
<div class="transaction__details">
<p class="transaction__day">${transaction.day}</p>
<div class="transaction__time-details">
<p class="transaction__date-time">
${transaction.fullDateString}
</p>
<p class="transaction__note">${
transaction.note === '' ? 'None' : transaction.note
}</p>
</div>
</div>
<p class="transaction__${
transaction.amount >= 0 ? 'income' : 'outcome'
}">${transaction.amount >= 0 ? '+' : '-'}$ ${formatNumber(
Math.abs(transaction.amount),
)}</p>
</div>
`;
listMarkup.push(markup);
});
return listMarkup.join('\n');
};
return `
<div class="transaction__item">
<div class="transaction__category">
<div class="transaction__category-infor">
<div class="transaction__category-icon-container">
<img
class="icon-category"
src="${transactionDetail.url}"
alt="Transportation icon category"
/>
</div>
<div class="transaction__category-content">
<p class="transaction__category-name">
${transactionDetail.categoryName}
</p>
<p class="transaction__total">${
transactionDetail.totalTransaction
} Transactions</p>
</div>
</div>
<p class="transaction__category-total">${
transactionDetail.totalAmount >= 0 ? '+' : '-'
}$ ${formatNumber(Math.abs(transactionDetail.totalAmount))}</p>
</div>
<div class="transaction__line"></div>
<!-- Transaction time item -->
${itemTransaction()}
</div>
`;
}
// 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 = (<Element>e.target).closest(
'.transaction__time',
);
const categoryNameEl: HTMLInputElement = item.querySelector(
'.transaction__category-name',
)!;
if (transactionTime) {
const idTransaction = (<HTMLElement>transactionTime).dataset.id!;
// If it is income transaction, don't show dialog
if (
categoryNameEl.textContent &&
categoryNameEl.textContent.trim() !== 'Income'
)
this.transactionView.showTransactionDialog(idTransaction);
}
});
});
}
}
}
@@ -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: string) => Promise<void>) | null = null;
loadTransactionData: (() => Promise<void>) | null = null;
updateAmountWallet: (() => Promise<void>) | null = null;
loadData: (() => Promise<void>) | null = null;
showSuccessToast: ((title: string, message: string) => void) | null = null;
showErrorToast: ((error: string | TError) => void) | null = null;
saveTransaction: ((transaction: Transaction) => Promise<void>) | null = null;
transform: Transform | null = null;
constructor(categoryView: CategoryView) {
this.addTransactionBtn = document.getElementById('addTransaction');
this.transactionDialog = document.getElementById(
'transactionDialog',
) as HTMLDialogElement;
this.transactionForm = <HTMLFormElement>(
document.getElementById('formAddTransaction')
);
this.handlerEventTransactionDialog();
this.categoryView = categoryView;
}
initFunction(
toggleLoaderSpinner: () => void,
deleteTransaction: (idTransaction: string) => Promise<void>,
loadTransactionData: () => Promise<void>,
updateAmountWallet: () => Promise<void>,
loadData: () => Promise<void>,
showSuccessToast: (title: string, message: string) => void,
showErrorToast: (error: string | TError) => void,
saveTransaction: (transaction: Transaction) => Promise<void>,
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 = (<HTMLDataElement>(
this.transactionDialog!.querySelector("[name='selected_date']")
)).value;
const categoryName = (<HTMLDataElement>(
this.transactionDialog!.querySelector("[name='category_name']")
)).value;
const amountInput = +(<HTMLDataElement>(
this.transactionDialog!.querySelector("[name='amount']")
)).value;
const saveBtn = this.transactionDialog!.querySelector('.form__save-btn')!;
saveBtn.classList.toggle(
'active',
<boolean>(dateInput && categoryName && amountInput >= 1),
);
}
initValueTransactionDialog(idTransaction: string | 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 = <HTMLDataElement>(
this.transactionDialog!.querySelector("[name='id_transaction']")
);
const dateEl = <HTMLDataElement>(
this.transactionDialog!.querySelector("[name='selected_date']"!)
);
const categoryEl = <HTMLDataElement>(
this.transactionDialog!.querySelector("[name='category_name']")
);
const amountEl = <HTMLDataElement>(
this.transactionDialog!.querySelector("[name='amount']")
);
const noteEl = <HTMLDataElement>(
this.transactionDialog!.querySelector("[name='note']")
);
const iconEl = <HTMLImageElement>(
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 = <HTMLDataElement>(
this.transactionForm!.querySelector("[name='selected_date']")
);
const categoryNameEl = <HTMLDataElement>(
this.transactionForm!.querySelector("[name='category_name']")
);
const amountEl = <HTMLDataElement>(
this.transactionForm!.querySelector("[name='amount']")
);
const noteEl = <HTMLDataElement>(
this.transactionForm!.querySelector("[name='note']")
);
const idEl = <HTMLDataElement>(
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 = <HTMLImageElement>(
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: string | null = null) {
this.clearInputTransactionForm();
// Init data transaction to dialog
this.initValueTransactionDialog(idTransaction);
if (!idTransaction)
(<HTMLInputElement>(
this.transactionDialog!.querySelector("[name='selected_date']")
)).valueAsDate = new Date(); // Set default value for date input
// Change style submit btn
this.changeBtnStyleTransactionDialog();
this.transactionDialog!.showModal();
}
}
@@ -0,0 +1,201 @@
import Transaction from '../../models/transaction';
import Wallet from '../../models/wallet';
import { renderRequiredText } from '../../helpers/validatorForm';
import Transform from 'helpers/transform';
import { Data, TError } from 'global/types';
import User from 'models/user';
import { FIRST_ADD_WALLET_NOTE } from 'constants/defaultVariable';
import { ADD_WALLET_SUCCESS, DEFAULT_MESSAGE } from 'constants/messages/dialog';
export default class WalletView {
walletDialog: HTMLDialogElement | null = null;
transform: Transform | null = null;
toggleLoaderSpinner: (() => void) | null = null;
saveWallet: ((wallet: Wallet) => Promise<void>) | null = null;
saveTransaction: ((transaction: Transaction) => Promise<void>) | null = null;
loadTransactionData: (() => Promise<void>) | null = null;
loadData: (() => Promise<void>) | null = null;
loadEvent: (() => void) | null = null;
showSuccessToast: ((title: string, message: string) => void) | null = null;
showErrorToast: ((error: string | TError) => void) | null = null;
user: User | null = null;
wallet: Wallet | null = null;
constructor() {
this.walletDialog = document.getElementById(
'walletDialog',
) as HTMLDialogElement;
this.addHandlerEventWalletForm();
}
initFunction(
transform: Transform | null,
toggleLoaderSpinner: () => void,
saveWallet: ((wallet: Wallet) => Promise<void>) | null,
saveTransaction: ((transaction: Transaction) => Promise<void>) | null,
loadTransactionData: () => Promise<void>,
loadData: () => Promise<void>,
loadEvent: () => void,
showSuccessToast: (title: string, message: string) => void,
showErrorToast: (error: string | TError) => void,
) {
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!,
user: this.user!,
};
this.transform!.onSendSignal('walletView', data);
}
updateData(data: Data) {
if (data.wallet) this.wallet = data.wallet;
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 = (<Element>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 form = document.getElementById('walletForm') as HTMLFormElement;
const walletForm = new FormData(form);
const walletName = walletForm.get('walletName') as string;
const amount = walletForm.get('amount') as string;
if (this.validateWalletDialog(walletName, +amount)) {
this.walletDialog!.close();
this.toggleLoaderSpinner!();
const wallet = new Wallet(walletName, +amount, 0, this.user!.id);
this.wallet = wallet;
this.sendData();
await this.saveWallet!(wallet);
// Transaction info
const transaction = new Transaction(
'',
'Income',
new Date().toISOString().slice(0, 10),
FIRST_ADD_WALLET_NOTE,
+amount,
this.user!.id,
);
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!();
}
} catch (error) {
// Show toast error
this.showErrorToast!(error as string | TError);
this.toggleLoaderSpinner!();
}
}
validateWalletDialog(walletName: string, amount: number) {
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: Element) {
const walletName = (<HTMLDataElement>(
bodyDialog.querySelector('.form__input-text')
))!.value;
const amount = +(<HTMLDataElement>(
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');
}
}
}
@@ -2,12 +2,15 @@ import { getSubdirectoryURL } from 'helpers/url';
import LoginView from './loginView';
import RegisterView from './registerView';
import { URL } from 'constants/config';
import HomeView from './home/homeView';
export default class View {
public registerView: RegisterView | null = null;
public loginView: LoginView | null = null;
public homeView: HomeView | null = null;
constructor() {
switch (getSubdirectoryURL()) {
case URL.LOGIN:
@@ -16,6 +19,8 @@ export default class View {
case URL.REGISTER:
this.registerView = new RegisterView();
break;
case URL.HOME:
this.homeView = new HomeView();
default:
}
}