Files
ezbookkeeping/src/models/account.ts
T
2025-09-14 01:43:04 +08:00

677 lines
23 KiB
TypeScript

import type { HiddenAmount, NumberWithSuffix } from '@/core/numeral.ts';
import type { ColorValue } from '@/core/color.ts';
import { AccountType, AccountCategory } from '@/core/account.ts';
import { PARENT_ACCOUNT_CURRENCY_PLACEHOLDER } from '@/consts/currency.ts';
import { DEFAULT_ACCOUNT_ICON_ID } from '@/consts/icon.ts';
import { DEFAULT_ACCOUNT_COLOR } from '@/consts/color.ts';
export class Account implements AccountInfoResponse {
public id: string;
public name: string;
public parentId: string;
public category: number;
public type: number;
public icon: string;
public color: ColorValue;
public currency: string;
public balance: number;
public balanceTime?: number;
public comment: string;
public creditCardStatementDate?: number;
public displayOrder: number;
public visible: boolean;
public subAccounts?: Account[];
private readonly _isAsset?: boolean;
private readonly _isLiability?: boolean;
protected constructor(id: string, name: string, parentId: string, category: number, type: number, icon: string, color: string, currency: string, balance: number, comment: string, displayOrder: number, visible: boolean, balanceTime?: number, creditCardStatementDate?: number, isAsset?: boolean, isLiability?: boolean, subAccounts?: Account[]) {
this.id = id;
this.name = name;
this.parentId = parentId;
this.category = category;
this.type = type;
this.icon = icon;
this.color = color;
this.currency = currency;
this.balance = balance;
this.balanceTime = balanceTime;
this.comment = comment;
this.displayOrder = displayOrder;
this.visible = visible;
this.creditCardStatementDate = creditCardStatementDate;
this._isAsset = isAsset;
this._isLiability = isLiability;
if (typeof(subAccounts) !== 'undefined') {
this.subAccounts = subAccounts;
} else {
this.subAccounts = undefined;
}
}
public get isAsset(): boolean {
if (typeof(this._isAsset) !== 'undefined') {
return this._isAsset;
}
const accountCategory = AccountCategory.valueOf(this.category);
if (accountCategory) {
return accountCategory.isAsset;
}
return false;
}
public get isLiability(): boolean {
if (typeof(this._isLiability) !== 'undefined') {
return this._isLiability;
}
const accountCategory = AccountCategory.valueOf(this.category);
if (accountCategory) {
return accountCategory.isLiability;
}
return false;
}
public get hidden(): boolean {
return !this.visible;
}
public equals(other: Account): boolean {
const isEqual = this.id === other.id &&
this.name === other.name &&
this.parentId === other.parentId &&
this.category === other.category &&
this.type === other.type &&
this.icon === other.icon &&
this.color === other.color &&
this.currency === other.currency &&
this.balance === other.balance &&
this.balanceTime === other.balanceTime &&
this.comment === other.comment &&
this.displayOrder === other.displayOrder &&
this.visible === other.visible &&
this.creditCardStatementDate === other.creditCardStatementDate;
if (!isEqual) {
return false;
}
if (this.subAccounts && other.subAccounts) {
if (this.subAccounts.length !== other.subAccounts.length) {
return false;
}
for (let i = 0; i < this.subAccounts.length; i++) {
if (!(this.subAccounts[i] as Account).equals(other.subAccounts[i] as Account)) {
return false;
}
}
} else if ((this.subAccounts && this.subAccounts.length) || (other.subAccounts && other.subAccounts.length)) {
return false;
}
return true;
}
public fillFrom(other: Account): void {
this.id = other.id;
this.category = other.category;
this.type = other.type;
this.name = other.name;
this.icon = other.icon;
this.color = other.color;
this.currency = other.currency;
this.balance = other.balance;
this.balanceTime = other.balanceTime;
this.comment = other.comment;
this.creditCardStatementDate = other.creditCardStatementDate;
this.visible = other.visible;
}
public setSuitableIcon(oldCategory: number, newCategory: number): void {
const allCategories = AccountCategory.values();
for (const category of allCategories) {
if (category.type === oldCategory) {
if (this.icon !== category.defaultAccountIconId) {
return;
} else {
break;
}
}
}
for (const category of allCategories) {
if (category.type === newCategory) {
this.icon = category.defaultAccountIconId;
}
}
}
public toCreateRequest(clientSessionId: string, subAccounts?: Account[], parentAccount?: Account): AccountCreateRequest {
let subAccountCreateRequests: AccountCreateRequest[] | undefined = undefined;
if (this.type === AccountType.MultiSubAccounts.type) {
subAccountCreateRequests = [];
if (!subAccounts) {
subAccounts = this.subAccounts;
}
if (subAccounts) {
for (const subAccount of subAccounts) {
subAccountCreateRequests.push(subAccount.toCreateRequest(clientSessionId, undefined, this));
}
}
}
return {
name: this.name,
category: parentAccount ? parentAccount.category : this.category,
type: parentAccount ? AccountType.SingleAccount.type : this.type,
icon: this.icon,
color: this.color,
currency: parentAccount || this.type === AccountType.SingleAccount.type ? this.currency : PARENT_ACCOUNT_CURRENCY_PLACEHOLDER,
balance: parentAccount || this.type === AccountType.SingleAccount.type ? this.balance : 0,
balanceTime: (parentAccount || this.type === AccountType.SingleAccount.type) && this.balanceTime ? this.balanceTime : 0,
comment: this.comment,
creditCardStatementDate: !parentAccount && this.category === AccountCategory.CreditCard.type ? this.creditCardStatementDate : undefined,
subAccounts: !parentAccount ? subAccountCreateRequests : undefined,
clientSessionId: !parentAccount ? clientSessionId : undefined
};
}
public toModifyRequest(clientSessionId: string, subAccounts?: Account[], parentAccount?: Account): AccountModifyRequest {
let subAccountModifyRequests: AccountModifyRequest[] | undefined = undefined;
if (this.type === AccountType.MultiSubAccounts.type) {
subAccountModifyRequests = [];
if (!subAccounts) {
subAccounts = this.subAccounts;
}
if (subAccounts) {
for (const subAccount of subAccounts) {
subAccountModifyRequests.push(subAccount.toModifyRequest(clientSessionId, undefined, this));
}
}
}
return {
id: this.id || '0',
name: this.name,
category: parentAccount ? parentAccount.category : this.category,
icon: this.icon,
color: this.color,
currency: parentAccount && (!this.id || this.id === '0') ? this.currency : undefined,
balance: parentAccount && (!this.id || this.id === '0') ? this.balance : undefined,
balanceTime: parentAccount && (!this.id || this.id === '0') ? this.balanceTime : undefined,
comment: this.comment,
creditCardStatementDate: !parentAccount && this.category === AccountCategory.CreditCard.type ? this.creditCardStatementDate : undefined,
hidden: !this.visible,
subAccounts: !parentAccount ? subAccountModifyRequests : undefined,
clientSessionId: !parentAccount ? clientSessionId : undefined
};
}
public getAccountOrSubAccountId(subAccountId: string): string | null {
if (this.type === AccountType.SingleAccount.type) {
return this.id;
} else if (this.type === AccountType.MultiSubAccounts.type && !subAccountId) {
return this.id;
} else if (this.type === AccountType.MultiSubAccounts.type && subAccountId) {
if (!this.subAccounts || !this.subAccounts.length) {
return null;
}
for (const subAccount of this.subAccounts) {
if (subAccountId && subAccountId === subAccount.id) {
return subAccount.id;
}
}
return null;
} else {
return null;
}
}
public isAccountOrSubAccountHidden(subAccountId: string): boolean {
if (this.type === AccountType.SingleAccount.type) {
return this.hidden;
} else if (this.type === AccountType.MultiSubAccounts.type && !subAccountId) {
return this.hidden;
} else if (this.type === AccountType.MultiSubAccounts.type && subAccountId) {
if (!this.subAccounts || !this.subAccounts.length) {
return false;
}
for (const subAccount of this.subAccounts) {
if (subAccountId && subAccountId === subAccount.id) {
return subAccount.hidden;
}
}
return false;
} else {
return false;
}
}
public getAccountOrSubAccountComment(subAccountId: string): string | null {
if (this.type === AccountType.SingleAccount.type) {
return this.comment;
} else if (this.type === AccountType.MultiSubAccounts.type && !subAccountId) {
return this.comment;
} else if (this.type === AccountType.MultiSubAccounts.type && subAccountId) {
if (!this.subAccounts || !this.subAccounts.length) {
return null;
}
for (const subAccount of this.subAccounts) {
if (subAccountId && subAccountId === subAccount.id) {
return subAccount.comment;
}
}
return null;
} else {
return null;
}
}
public getAccountOrSubAccount(subAccountId: string): Account | null {
if (this.type === AccountType.SingleAccount.type) {
return this;
} else if (this.type === AccountType.MultiSubAccounts.type && !subAccountId) {
return this;
} else if (this.type === AccountType.MultiSubAccounts.type && subAccountId) {
if (!this.subAccounts || !this.subAccounts.length) {
return null;
}
for (const subAccount of this.subAccounts) {
if (subAccountId && subAccountId === subAccount.id) {
return subAccount;
}
}
return null;
} else {
return null;
}
}
public getSubAccount(subAccountId: string): Account | null {
if (!this.subAccounts || !this.subAccounts.length) {
return null;
}
for (const subAccount of this.subAccounts) {
if (subAccountId && subAccountId === subAccount.id) {
return subAccount;
}
}
return null;
}
public getSubAccountCurrencies(showHidden: boolean, subAccountId: string): string[] {
if (!this.subAccounts || !this.subAccounts.length) {
return [];
}
const subAccountCurrenciesMap: Record<string, boolean> = {};
const subAccountCurrencies: string[] = [];
for (const subAccount of this.subAccounts) {
if (!showHidden && subAccount.hidden) {
continue;
}
if (subAccountId && subAccountId === subAccount.id) {
return [subAccount.currency];
} else {
if (!subAccountCurrenciesMap[subAccount.currency]) {
subAccountCurrenciesMap[subAccount.currency] = true;
subAccountCurrencies.push(subAccount.currency);
}
}
}
return subAccountCurrencies;
}
public clone(): Account {
return new Account(
this.id,
this.name,
this.parentId,
this.category,
this.type,
this.icon,
this.color,
this.currency,
this.balance,
this.comment,
this.displayOrder,
this.visible,
this.balanceTime,
this.creditCardStatementDate,
this.isAsset,
this.isLiability,
typeof(this.subAccounts) !== 'undefined' ? Account.cloneAccounts(this.subAccounts) : undefined);
}
public createNewSubAccount(currency: string, balanceTime: number): Account {
return new Account(
'', // id
'', // name
'', // parentId
0, // category
0, // type
this.icon, // icon
this.color, // color
currency, // currency
0, // balance
'', // comment
0, // displayOrder
true, // visible
balanceTime, // balanceTime
0 // creditCardStatementDate
);
}
public static createNewAccount(currency: string, balanceTime: number): Account {
return new Account(
'', // id
'', // name
'', // parentId
AccountCategory.Cash.type, // category
AccountType.SingleAccount.type, // type
DEFAULT_ACCOUNT_ICON_ID, // icon
DEFAULT_ACCOUNT_COLOR, // color
currency, // currency
0, // balance
'', // comment
0, // displayOrder
true, // visible
balanceTime, // balanceTime
0 // creditCardStatementDate
);
}
public static of(accountResponse: AccountInfoResponse): Account {
return new Account(
accountResponse.id,
accountResponse.name,
accountResponse.parentId,
accountResponse.category,
accountResponse.type,
accountResponse.icon,
accountResponse.color,
accountResponse.currency,
accountResponse.balance,
accountResponse.comment,
accountResponse.displayOrder,
!accountResponse.hidden,
undefined,
accountResponse.creditCardStatementDate,
accountResponse.isAsset,
accountResponse.isLiability,
accountResponse.subAccounts ? Account.ofMulti(accountResponse.subAccounts) : undefined
);
}
public static ofMulti(accountResponses: AccountInfoResponse[]): Account[] {
const accounts: Account[] = [];
for (const accountResponse of accountResponses) {
accounts.push(Account.of(accountResponse));
}
return accounts;
}
public static findAccountNameById(accounts: Account[], accountId: string, defaultName?: string): string | undefined {
for (const account of accounts) {
if (account.id === accountId) {
return account.name;
}
}
return defaultName;
}
public static cloneAccounts(accounts: Account[]): Account[] {
const clonedAccounts: Account[] = [];
for (const account of accounts) {
clonedAccounts.push(account.clone());
}
return clonedAccounts;
}
public static sortAccounts(accounts: Account[], allAccountsMap?: Record<string, Account>): Account[] {
if (!accounts || !accounts.length) {
return accounts;
}
return accounts.sort(function (account1, account2) {
if (account1.category !== account2.category) {
const account1Category = AccountCategory.valueOf(account1.category);
const account2Category = AccountCategory.valueOf(account2.category);
if (!account1Category) {
return 1;
}
if (!account2Category) {
return -1;
}
return account1Category.displayOrder - account2Category.displayOrder;
}
if (account1.parentId === account2.parentId) {
return account1.displayOrder - account2.displayOrder;
}
if (account1.id === account2.parentId) {
return -1;
} else if (account2.id === account1.parentId) {
return 1;
}
let account1DisplayOrder: number | null = account1.displayOrder;
let account2DisplayOrder: number | null = account2.displayOrder;
if (account1.parentId && account1.parentId !== '0') {
if (allAccountsMap && allAccountsMap[account1.parentId]) {
account1DisplayOrder = (allAccountsMap[account1.parentId] as Account).displayOrder;
} else {
account1DisplayOrder = null;
}
}
if (account2.parentId && account2.parentId !== '0') {
if (allAccountsMap && allAccountsMap[account2.parentId]) {
account2DisplayOrder = (allAccountsMap[account2.parentId] as Account).displayOrder;
} else {
account2DisplayOrder = null;
}
}
if (account1DisplayOrder !== null && account2DisplayOrder !== null) {
return account1DisplayOrder - account2DisplayOrder;
} else {
return account1.id.localeCompare(account2.id);
}
});
}
}
export class AccountWithDisplayBalance extends Account {
public displayBalance: string;
private constructor(account: Account, displayBalance: string) {
super(
account.id,
account.name,
account.parentId,
account.category,
account.type,
account.icon,
account.color,
account.currency,
account.balance,
account.comment,
account.displayOrder,
account.visible,
account.balanceTime,
account.creditCardStatementDate,
account.isAsset,
account.isLiability,
account.subAccounts
);
this.displayBalance = displayBalance;
}
public static fromAccount(account: Account, displayBalance: string): AccountWithDisplayBalance {
return new AccountWithDisplayBalance(account, displayBalance);
}
}
export interface AccountCreateRequest {
readonly name: string;
readonly category: number;
readonly type: number;
readonly icon: string;
readonly color: string;
readonly currency: string;
readonly balance: number;
readonly balanceTime: number;
readonly comment: string;
readonly creditCardStatementDate?: number;
readonly subAccounts?: AccountCreateRequest[];
readonly clientSessionId?: string;
}
export interface AccountModifyRequest {
readonly id: string;
readonly name: string;
readonly category: number;
readonly icon: string;
readonly color: string;
readonly currency?: string;
readonly balance?: number;
readonly balanceTime?: number;
readonly comment: string;
readonly creditCardStatementDate?: number;
readonly hidden: boolean;
readonly subAccounts?: AccountModifyRequest[];
readonly clientSessionId?: string;
}
export interface AccountInfoResponse {
readonly id: string;
readonly name: string;
readonly parentId: string;
readonly category: number;
readonly type: number;
readonly icon: string;
readonly color: string;
readonly currency: string;
readonly balance: number;
readonly comment: string;
readonly creditCardStatementDate?: number;
readonly displayOrder: number;
readonly isAsset?: boolean;
readonly isLiability?: boolean;
readonly hidden: boolean;
readonly subAccounts?: AccountInfoResponse[];
}
export interface AccountHideRequest {
readonly id: string;
readonly hidden: boolean;
}
export interface AccountMoveRequest {
readonly newDisplayOrders: AccountNewDisplayOrderRequest[];
}
export interface AccountNewDisplayOrderRequest {
readonly id: string;
readonly displayOrder: number;
}
export interface AccountDeleteRequest {
readonly id: string;
}
export interface AccountBalance {
readonly balance: number;
readonly isAsset: boolean;
readonly isLiability: boolean;
readonly currency: string;
}
export interface AccountDisplayBalance {
readonly balance: number | HiddenAmount | NumberWithSuffix;
readonly currency: string;
}
export interface CategorizedAccount {
readonly category: number;
readonly name: string;
readonly icon: string;
readonly accounts: Account[];
}
export class CategorizedAccountWithDisplayBalance {
public category: number;
public name: string;
public icon: string;
public accounts: AccountWithDisplayBalance[];
public displayBalance: string;
private constructor(category: number, name: string, icon: string, accounts: AccountWithDisplayBalance[], displayBalance: string) {
this.category = category;
this.name = name;
this.icon = icon;
this.accounts = accounts;
this.displayBalance = displayBalance;
}
public static of(categorizedAccount: CategorizedAccount, accounts: AccountWithDisplayBalance[], displayBalance: string): CategorizedAccountWithDisplayBalance {
return new CategorizedAccountWithDisplayBalance(categorizedAccount.category, categorizedAccount.name, categorizedAccount.icon, accounts, displayBalance);
}
}
export interface AccountCategoriesWithVisibleCount {
readonly category: number;
readonly name: string;
readonly icon: string;
readonly allAccounts: Account[];
readonly allVisibleAccountCount: number;
readonly firstVisibleAccountIndex: number;
readonly allSubAccounts: Record<string, Account[]>;
readonly allVisibleSubAccountCounts: Record<string, number>;
readonly allFirstVisibleSubAccountIndexes: Record<string, number>;
}
export interface AccountShowingIds {
readonly accounts: Record<number, string>;
readonly subAccounts: Record<string, string>;
}