mirror of
https://github.com/Nezumi-2711/typescript-training.git
synced 2026-09-22 20:02:19 +00:00
Merge pull request #8 from Nez27/feat/add-home-view
Implement home page.
This commit is contained in:
Vendored
+1
@@ -0,0 +1 @@
|
||||
declare module '*.svg';
|
||||
@@ -284,5 +284,6 @@
|
||||
</main>
|
||||
<!-- End app -->
|
||||
</div>
|
||||
<script type="module" src="../ts/index.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user