From 782bc119509bd01e0dd813dd4b6f962d685c9ee7 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Tue, 28 Jan 2025 11:58:42 +0800 Subject: [PATCH] migrate transaction store to composition API and typescript --- src/lib/common.ts | 35 + src/lib/{transaction.js => transaction.ts} | 58 +- src/lib/userstate.ts | 7 +- src/models/imported_transaction.ts | 105 +- src/models/transaction.ts | 458 +++++- src/stores/index.ts | 3 +- src/stores/transaction.js | 1330 ----------------- src/stores/transaction.ts | 1241 +++++++++++++++ src/stores/transactionTemplate.js | 6 +- src/views/base/UnlockPageBase.ts | 3 +- .../base/settings/AppSettingsPageBase.ts | 3 +- .../app/settings/tabs/AppLockSettingTab.vue | 2 +- .../cards/AccountFilterSettingsCard.vue | 2 +- .../cards/CategoryFilterSettingsCard.vue | 2 +- .../TransactionTagFilterSettingsCard.vue | 2 +- src/views/desktop/transactions/ListPage.vue | 4 +- .../transactions/list/dialogs/EditDialog.vue | 64 +- .../list/dialogs/ImportDialog.vue | 48 +- src/views/mobile/ApplicationLockPage.vue | 2 +- .../settings/AccountFilterSettingsPage.vue | 2 +- .../settings/CategoryFilterSettingsPage.vue | 2 +- .../TransactionTagFilterSettingsPage.vue | 2 +- .../mobile/transactions/AmountFilterPage.vue | 2 +- src/views/mobile/transactions/EditPage.vue | 57 +- src/views/mobile/transactions/ListPage.vue | 4 +- 25 files changed, 1948 insertions(+), 1496 deletions(-) rename src/lib/{transaction.js => transaction.ts} (70%) delete mode 100644 src/stores/transaction.js create mode 100644 src/stores/transaction.ts diff --git a/src/lib/common.ts b/src/lib/common.ts index 676f2f54..dfc72cbb 100644 --- a/src/lib/common.ts +++ b/src/lib/common.ts @@ -489,6 +489,41 @@ export function arrayItemToObjectField(array: string[], value: T): Record { + const ret: Record = {}; + + if (!str) { + return ret; + } + + const items = str.split(separator); + + for (let i = 0; i < items.length; i++) { + if (items[i]) { + ret[items[i]] = true; + } + } + + return ret; +} + +export function countSplitItems(str: string | undefined | null, separator: string): number { + if (!str) { + return 0; + } + + const items = str.split(separator); + let count = 0; + + for (let i = 0; i < items.length; i++) { + if (items[i]) { + count++; + } + } + + return count; +} + export function categorizedArrayToPlainArray(object: Record): T[] { const ret: T[] = []; diff --git a/src/lib/transaction.js b/src/lib/transaction.ts similarity index 70% rename from src/lib/transaction.js rename to src/lib/transaction.ts index 448576d1..5906741c 100644 --- a/src/lib/transaction.js +++ b/src/lib/transaction.ts @@ -1,5 +1,10 @@ import { CategoryType } from '@/core/category.ts'; import { TransactionType } from '@/core/transaction.ts'; +import { Account } from '@/models/account.ts'; +import { TransactionCategory } from '@/models/transaction_category.ts'; +import { TransactionTag } from '@/models/transaction_tag.ts'; +import {Transaction, TransactionPicture} from '@/models/transaction.ts'; + import { isNumber } from './common.ts'; @@ -14,7 +19,18 @@ import { getFirstAvailableSubCategoryId } from './category.ts'; -function getDisplayAmount(amount, currency, hideAmount, formatAmountWithCurrencyFunc) { +export interface SetTransactionOptions { + type: number; + categoryId: string; + accountId: string; + destinationAccountId: string; + amount: number; + destinationAmount: number; + tagIds: string; + comment: string; +} + +function getDisplayAmount(amount: number, currency: string, hideAmount: boolean, formatAmountWithCurrencyFunc: (value: number | string, currencyCode?: string) => string): string { if (hideAmount) { return formatAmountWithCurrencyFunc('***', currency); } @@ -22,8 +38,8 @@ function getDisplayAmount(amount, currency, hideAmount, formatAmountWithCurrency return formatAmountWithCurrencyFunc(amount, currency); } -export function setTransactionModelByTransaction(transaction, transaction2, allCategories, allCategoriesMap, allVisibleAccounts, allAccountsMap, allTagsMap, defaultAccountId, options, setContextData, convertContextTime) { - if ((!options.type || options.type === '0') && options.categoryId && options.categoryId !== '0' && allCategoriesMap[options.categoryId]) { +export function setTransactionModelByTransaction(transaction: Transaction, transaction2: Transaction | null | undefined, allCategories: Record, allCategoriesMap: Record, allVisibleAccounts: Account[], allAccountsMap: Record, allTagsMap: Record, defaultAccountId: string, options: SetTransactionOptions, setContextData: boolean, convertContextTime: boolean): void { + if (!options.type && options.categoryId && options.categoryId !== '0' && allCategoriesMap[options.categoryId]) { const category = allCategoriesMap[options.categoryId]; const type = categoryTypeToTransactionType(category.type); @@ -36,14 +52,14 @@ export function setTransactionModelByTransaction(transaction, transaction2, allC allCategories[CategoryType.Expense].length) { if (options.categoryId && options.categoryId !== '0') { if (isSubCategoryIdAvailable(allCategories[CategoryType.Expense], options.categoryId)) { - transaction.expenseCategory = options.categoryId; + transaction.expenseCategoryId = options.categoryId; } else { - transaction.expenseCategory = getFirstAvailableSubCategoryId(allCategories[CategoryType.Expense], options.categoryId); + transaction.expenseCategoryId = getFirstAvailableSubCategoryId(allCategories[CategoryType.Expense], options.categoryId); } } - if (!transaction.expenseCategory) { - transaction.expenseCategory = getFirstAvailableCategoryId(allCategories[CategoryType.Expense]); + if (!transaction.expenseCategoryId) { + transaction.expenseCategoryId = getFirstAvailableCategoryId(allCategories[CategoryType.Expense]); } } @@ -51,14 +67,14 @@ export function setTransactionModelByTransaction(transaction, transaction2, allC allCategories[CategoryType.Income].length) { if (options.categoryId && options.categoryId !== '0') { if (isSubCategoryIdAvailable(allCategories[CategoryType.Income], options.categoryId)) { - transaction.incomeCategory = options.categoryId; + transaction.incomeCategoryId = options.categoryId; } else { - transaction.incomeCategory = getFirstAvailableSubCategoryId(allCategories[CategoryType.Income], options.categoryId); + transaction.incomeCategoryId = getFirstAvailableSubCategoryId(allCategories[CategoryType.Income], options.categoryId); } } - if (!transaction.incomeCategory) { - transaction.incomeCategory = getFirstAvailableCategoryId(allCategories[CategoryType.Income]); + if (!transaction.incomeCategoryId) { + transaction.incomeCategoryId = getFirstAvailableCategoryId(allCategories[CategoryType.Income]); } } @@ -66,14 +82,14 @@ export function setTransactionModelByTransaction(transaction, transaction2, allC allCategories[CategoryType.Transfer].length) { if (options.categoryId && options.categoryId !== '0') { if (isSubCategoryIdAvailable(allCategories[CategoryType.Transfer], options.categoryId)) { - transaction.transferCategory = options.categoryId; + transaction.transferCategoryId = options.categoryId; } else { - transaction.transferCategory = getFirstAvailableSubCategoryId(allCategories[CategoryType.Transfer], options.categoryId); + transaction.transferCategoryId = getFirstAvailableSubCategoryId(allCategories[CategoryType.Transfer], options.categoryId); } } - if (!transaction.transferCategory) { - transaction.transferCategory = getFirstAvailableCategoryId(allCategories[CategoryType.Transfer]); + if (!transaction.transferCategoryId) { + transaction.transferCategoryId = getFirstAvailableCategoryId(allCategories[CategoryType.Transfer]); } } @@ -129,11 +145,11 @@ export function setTransactionModelByTransaction(transaction, transaction2, allC transaction.type = transaction2.type; if (transaction.type === TransactionType.Expense) { - transaction.expenseCategory = transaction2.categoryId || ''; + transaction.expenseCategoryId = transaction2.categoryId || ''; } else if (transaction.type === TransactionType.Income) { - transaction.incomeCategory = transaction2.categoryId || ''; + transaction.incomeCategoryId = transaction2.categoryId || ''; } else if (transaction.type === TransactionType.Transfer) { - transaction.transferCategory = transaction2.categoryId || ''; + transaction.transferCategoryId = transaction2.categoryId || ''; } if (setContextData) { @@ -165,17 +181,17 @@ export function setTransactionModelByTransaction(transaction, transaction2, allC transaction.hideAmount = transaction2.hideAmount; transaction.tagIds = transaction2.tagIds || []; - transaction.pictures = transaction2.pictures || []; + transaction.setPictures(TransactionPicture.ofMany(transaction2.pictures || [])); transaction.comment = transaction2.comment; if (setContextData) { - transaction.geoLocation = transaction2.geoLocation; + transaction.setGeoLocation(transaction2.geoLocation); } } } -export function getTransactionDisplayAmount(transaction, allFilterAccountIdsCount, allFilterAccountIds, formatAmountWithCurrencyFunc) { +export function getTransactionDisplayAmount(transaction: Transaction, allFilterAccountIdsCount: number, allFilterAccountIds: Record, formatAmountWithCurrencyFunc: (value: number | string, currencyCode?: string) => string): string { if (allFilterAccountIdsCount < 1) { if (transaction.sourceAccount) { return getDisplayAmount(transaction.sourceAmount, transaction.sourceAccount.currency, transaction.hideAmount, formatAmountWithCurrencyFunc); diff --git a/src/lib/userstate.ts b/src/lib/userstate.ts index e99af77e..691f4cef 100644 --- a/src/lib/userstate.ts +++ b/src/lib/userstate.ts @@ -2,6 +2,7 @@ import CryptoJS from 'crypto-js'; import type { ApplicationLockState, WebAuthnConfig } from '@/core/setting.ts'; import type { UserBasicInfo } from '@/models/user.ts'; +import type { TransactionDraft } from '@/models/transaction.ts'; import { isString, isObject } from './common.ts'; import { isEnableApplicationLock } from './settings.ts'; @@ -257,7 +258,7 @@ export function clearCurrentUserInfo(): void { localStorage.removeItem(userInfoLocalStorageKey); } -export function getUserTransactionDraft(): unknown | null { +export function getUserTransactionDraft(): TransactionDraft | null { let data = localStorage.getItem(transactionDraftLocalStorageKey); if (!data) { @@ -274,10 +275,10 @@ export function getUserTransactionDraft(): unknown | null { data = getDecryptedToken(data, appLockState); } - return JSON.parse(data); + return JSON.parse(data) as TransactionDraft; } -export function updateUserTransactionDraft(transaction: unknown): void { +export function updateUserTransactionDraft(transaction?: TransactionDraft | null): void { if (!isObject(transaction)) { return; } diff --git a/src/models/imported_transaction.ts b/src/models/imported_transaction.ts index 62db7e3a..2142bb13 100644 --- a/src/models/imported_transaction.ts +++ b/src/models/imported_transaction.ts @@ -1,4 +1,107 @@ -import type { TransactionGeoLocationResponse } from './transaction.ts'; +import { TransactionType } from '@/core/transaction.ts'; + +import type { TransactionCreateRequest, TransactionGeoLocationResponse } from './transaction.ts'; + +export class ImportTransaction implements ImportTransactionResponse { + public type: number; + public categoryId: string; + public originalCategoryName: string; + public time: number; + public utcOffset: number; + public sourceAccountId: string; + public originalSourceAccountName: string; + public originalSourceAccountCurrency: string; + public destinationAccountId: string; + public originalDestinationAccountName?: string; + public originalDestinationAccountCurrency?: string; + public sourceAmount: number; + public destinationAmount: number; + public tagIds: string[]; + public originalTagNames: string[]; + public comment: string; + public geoLocation?: TransactionGeoLocationResponse; + + public actualCategoryName: string; + public actualSourceAccountName: string; + public actualDestinationAccountName?: string; + public index: number; + public selected: boolean; + public valid: boolean; + + private constructor(response: ImportTransactionResponse, index: number) { + this.type = response.type; + this.categoryId = response.categoryId; + this.originalCategoryName = response.originalCategoryName; + this.time = response.time; + this.utcOffset = response.utcOffset; + this.sourceAccountId = response.sourceAccountId; + this.originalSourceAccountName = response.originalSourceAccountName; + this.originalSourceAccountCurrency = response.originalSourceAccountCurrency; + this.destinationAccountId = response.destinationAccountId || ''; + this.originalDestinationAccountName = response.originalDestinationAccountName; + this.originalDestinationAccountCurrency = response.originalDestinationAccountCurrency; + this.sourceAmount = response.sourceAmount; + this.destinationAmount = response.destinationAmount || 0; + this.tagIds = response.tagIds; + this.originalTagNames = response.originalTagNames; + this.comment = response.comment; + this.geoLocation = response.geoLocation; + + this.actualCategoryName = response.originalCategoryName; + this.actualSourceAccountName = response.originalSourceAccountName; + this.actualDestinationAccountName = response.originalDestinationAccountName; + this.index = index; + this.selected = false; + this.valid = this.isTransactionValid(); + } + + public toCreateRequest(): TransactionCreateRequest { + return { + type: this.type, + categoryId: this.categoryId, + time: this.time, + utcOffset: this.utcOffset, + sourceAccountId: this.sourceAccountId, + destinationAccountId: this.type === TransactionType.Transfer ? this.destinationAccountId : '0', + sourceAmount: this.sourceAmount, + destinationAmount: this.type === TransactionType.Transfer ? this.destinationAmount : 0, + hideAmount: false, + tagIds: this.tagIds, + pictureIds: [], + comment: this.comment, + geoLocation: this.geoLocation, + clientSessionId: '' + }; + } + + public isTransactionValid(): boolean { + if (this.type !== TransactionType.ModifyBalance && (!this.categoryId || this.categoryId === '0')) { + return false; + } + + if (!this.sourceAccountId || this.sourceAccountId === '0') { + return false; + } + + if (this.type === TransactionType.Transfer && (!this.destinationAccountId || this.destinationAccountId === '0')) { + return false; + } + + if (this.tagIds && this.tagIds.length) { + for (const tagId of this.tagIds) { + if (!tagId || tagId === '0') { + return false; + } + } + } + + return true; + } + + public static of(response: ImportTransactionResponse, index: number): ImportTransaction { + return new ImportTransaction(response, index); + } +} export interface ImportTransactionResponse { readonly type: number; diff --git a/src/models/transaction.ts b/src/models/transaction.ts index d2e7dfe4..33f4ada5 100644 --- a/src/models/transaction.ts +++ b/src/models/transaction.ts @@ -1,10 +1,450 @@ import type { PartialRecord } from '@/core/base.ts'; import type { YearMonth, StartEndTime } from '@/core/datetime.ts'; +import { TransactionType } from '@/core/transaction.ts'; -import type { AccountInfoResponse } from './account.ts'; -import type { TransactionCategoryInfoResponse } from './transaction_category.ts'; +import { Account, type AccountInfoResponse } from './account.ts'; +import { TransactionCategory, type TransactionCategoryInfoResponse } from './transaction_category.ts'; +import { TransactionTag, type TransactionTagInfoResponse } from './transaction_tag.ts'; import type { TransactionPictureInfoBasicResponse } from './transaction_picture_info.ts'; -import type { TransactionTagInfoResponse } from './transaction_tag.ts'; + +export class Transaction implements TransactionInfoResponse { + public id: string; + public timeSequenceId: string; + public type: number; + public expenseCategoryId: string = ''; + public incomeCategoryId: string = ''; + public transferCategoryId: string = ''; + public time: number; + public timeZone?: string; // only in new transaction + public utcOffset: number; + public sourceAccountId: string; + public destinationAccountId: string; + public sourceAmount: number; + public destinationAmount: number; + public hideAmount: boolean; + public tagIds: string[]; + public comment: string; + public editable: boolean; + + private _pictures?: TransactionPicture[]; + private _geoLocation?: TransactionGeoLocation; + + private _category?: TransactionCategory; // only for displaying transaction + private _sourceAccount?: Account; // only for displaying transaction + private _destinationAccount?: Account; // only for displaying transaction + private _tags?: TransactionTag[]; // only for displaying transaction + + private _date?: string = undefined; // only for displaying transaction in transaction list + private _day?: number = undefined; // only for displaying transaction in transaction list + private _dayOfWeek?: string = undefined; // only for displaying transaction in transaction list + + private constructor(id: string, timeSequenceId: string, type: number, categoryId: string, time: number, timeZone: string | undefined, utcOffset: number, sourceAccountId: string, destinationAccountId: string, sourceAmount: number, destinationAmount: number, hideAmount: boolean, tagIds: string[], comment: string, editable: boolean) { + this.id = id; + this.timeSequenceId = timeSequenceId; + this.type = type; + this.time = time; + this.timeZone = timeZone; + this.utcOffset = utcOffset; + this.sourceAccountId = sourceAccountId; + this.destinationAccountId = destinationAccountId; + this.sourceAmount = sourceAmount; + this.destinationAmount = destinationAmount; + this.hideAmount = hideAmount; + this.tagIds = tagIds; + this.comment = comment; + this.editable = editable; + this.setCategoryId(categoryId); + } + + public get pictures(): TransactionPictureInfoBasicResponse[] | undefined { + const ret: TransactionPictureInfoBasicResponse[] = []; + + if (this._pictures) { + for (const picture of this._pictures) { + ret.push(picture); + } + } + + return ret; + } + + public get geoLocation(): TransactionGeoLocationResponse | undefined { + return this._geoLocation; + } + + public get categoryId(): string { + return this.getCategoryId(); + } + + public get category(): TransactionCategoryInfoResponse | undefined { + return this._category; + } + + public get sourceAccount(): AccountInfoResponse | undefined { + return this._sourceAccount; + } + + public get destinationAccount(): AccountInfoResponse | undefined { + return this._destinationAccount; + } + + public get tags(): TransactionTagInfoResponse[] | undefined { + const ret: TransactionTagInfoResponse[] = []; + + if (this._tags) { + for (const tag of this._tags) { + ret.push(tag); + } + } + + return ret; + } + + public get date(): string | undefined { + return this._date; + } + + public get day(): number | undefined { + return this._day; + } + + public get dayOfWeek(): string | undefined { + return this._dayOfWeek; + } + + public getCategoryId(): string { + if (this.type === TransactionType.Expense) { + return this.expenseCategoryId; + } else if (this.type === TransactionType.Income) { + return this.incomeCategoryId; + } else if (this.type === TransactionType.Transfer) { + return this.transferCategoryId; + } else { + return ''; + } + } + + public setCategoryId(categoryId: string): void { + if (this.type === TransactionType.Expense) { + this.expenseCategoryId = categoryId; + } else if (this.type === TransactionType.Income) { + this.incomeCategoryId = categoryId; + } else if (this.type === TransactionType.Transfer) { + this.transferCategoryId = categoryId; + } + } + + public setCategory(category: TransactionCategory): void { + this._category = category; + } + + public setSourceAccount(sourceAccount: Account): void { + this._sourceAccount = sourceAccount; + } + + public setDestinationAccount(destinationAccount: Account): void { + this._destinationAccount = destinationAccount; + } + + public setTags(tags: TransactionTag[]): void { + this._tags = tags; + } + + public getPictureIds(): string[] { + const pictureIds: string[] = []; + + if (this._pictures) { + for (const picture of this._pictures) { + pictureIds.push(picture.pictureId); + } + } + + return pictureIds; + } + + public setPictures(pictures: TransactionPicture[]): void { + this._pictures = pictures; + } + + public addPicture(pictureInfo: TransactionPictureInfoBasicResponse): void { + if (!this._pictures) { + this._pictures = []; + } + + this._pictures.push(TransactionPicture.of(pictureInfo)); + } + + public removePicture(pictureInfo: TransactionPictureInfoBasicResponse): void { + if (!this._pictures) { + return; + } + + for (let i = 0; i < this._pictures.length; i++) { + if (this._pictures[i].pictureId === pictureInfo.pictureId) { + this._pictures.splice(i, 1); + } + } + } + + public clearPictures(): void { + this._pictures = []; + } + + public setGeoLocation(geoLocation?: TransactionGeoLocation): void { + this._geoLocation = geoLocation; + } + + public setLatitudeAndLongitude(latitude: number, longitude: number): void { + this._geoLocation = TransactionGeoLocation.createNewGeoLocation(latitude, longitude); + } + + public removeGeoLocation(): void { + this._geoLocation = undefined; + } + + public setDisplayDate(date: string, day: number, dayOfWeek: string): void { + this._date = date; + this._day = day; + this._dayOfWeek = dayOfWeek; + } + + public toCreateRequest(clientSessionId: string, actualTime?: number): TransactionCreateRequest { + return { + type: this.type, + categoryId: this.getCategoryId(), + time: actualTime ? actualTime : this.time, + utcOffset: this.utcOffset, + sourceAccountId: this.sourceAccountId, + destinationAccountId: this.type === TransactionType.Transfer ? this.destinationAccountId : '0', + sourceAmount: this.sourceAmount, + destinationAmount: this.type === TransactionType.Transfer ? this.destinationAmount : 0, + hideAmount: this.hideAmount, + tagIds: this.tagIds, + pictureIds: this.getPictureIds(), + comment: this.comment, + geoLocation: this.geoLocation, + clientSessionId: clientSessionId + }; + } + + public toModifyRequest(actualTime?: number): TransactionModifyRequest { + return { + id: this.id, + categoryId: this.getCategoryId(), + time: actualTime ? actualTime : this.time, + utcOffset: this.utcOffset, + sourceAccountId: this.sourceAccountId, + destinationAccountId: this.type === TransactionType.Transfer ? this.destinationAccountId : '0', + sourceAmount: this.sourceAmount, + destinationAmount: this.type === TransactionType.Transfer ? this.destinationAmount : 0, + hideAmount: this.hideAmount, + tagIds: this.tagIds, + pictureIds: this.getPictureIds(), + comment: this.comment, + geoLocation: this.geoLocation + }; + } + + public toTransactionDraft(): TransactionDraft | null { + if (this.type !== TransactionType.Expense && + this.type !== TransactionType.Income && + this.type !== TransactionType.Transfer) { + return null; + } + + return { + type: this.type, + categoryId: this.getCategoryId(), + sourceAccountId: this.sourceAccountId, + sourceAmount: this.sourceAmount, + destinationAccountId: this.type === TransactionType.Transfer ? this.destinationAccountId : '0', + destinationAmount: this.type === TransactionType.Transfer ? this.destinationAmount : 0, + hideAmount: this.hideAmount, + tagIds: this.tagIds, + pictures: this.pictures, + comment: this.comment, + }; + } + + public static createNewTransaction(type: number, time: number, timeZone: string, utcOffset: number): Transaction { + return new Transaction( + '', // id + '', // timeSequenceId + type, // type + '', // categoryId + time, // time + timeZone, // timeZone + utcOffset, // utcOffset + '', // sourceAccountId + '', // destinationAccountId + 0, // sourceAmount + 0, // destinationAmount + false, // hideAmount + [], // tagIds + '', // comment + true // editable + ); + } + + public static of(transactionResponse: TransactionInfoResponse): Transaction { + const transaction: Transaction = new Transaction( + transactionResponse.id, + transactionResponse.timeSequenceId, + transactionResponse.type, + transactionResponse.categoryId, + transactionResponse.time, + undefined, // only in new transaction + transactionResponse.utcOffset, + transactionResponse.sourceAccountId, + transactionResponse.destinationAccountId, + transactionResponse.sourceAmount, + transactionResponse.destinationAmount, + transactionResponse.hideAmount, + transactionResponse.tagIds, + transactionResponse.comment, + transactionResponse.editable + ); + + if (transactionResponse.category) { + transaction.setCategory(TransactionCategory.of(transactionResponse.category)); + } + + if (transactionResponse.sourceAccount) { + transaction.setSourceAccount(Account.of(transactionResponse.sourceAccount)); + } + + if (transactionResponse.destinationAccount) { + transaction.setDestinationAccount(Account.of(transactionResponse.destinationAccount)); + } + + if (transactionResponse.tags) { + transaction.setTags(TransactionTag.ofMany(transactionResponse.tags)); + } + + if (transactionResponse.pictures) { + const pictures: TransactionPicture[] = []; + + for (const picture of transactionResponse.pictures) { + pictures.push(TransactionPicture.of(picture)); + } + + transaction.setPictures(pictures); + } + + if (transactionResponse.geoLocation) { + transaction.setLatitudeAndLongitude(transactionResponse.geoLocation.latitude, transactionResponse.geoLocation.longitude); + } + + return transaction; + } + + public static ofMany(transactionResponses: TransactionInfoResponse[]): Transaction[] { + const transactions: Transaction[] = []; + + for (const transactionResponse of transactionResponses) { + transactions.push(Transaction.of(transactionResponse)); + } + + return transactions; + } + + public static ofDraft(transactionDraft?: TransactionDraft | null): Transaction | null { + if (!transactionDraft) { + return null; + } + + if (transactionDraft.type !== TransactionType.Expense && + transactionDraft.type !== TransactionType.Income && + transactionDraft.type !== TransactionType.Transfer) { + return null; + } + + const transaction: Transaction = new Transaction( + '', // id + '', // timeSequenceId + transactionDraft.type, // type + transactionDraft.categoryId ?? '', // categoryId + 0, // time + undefined, // only in new transaction + 0, // utcOffset + transactionDraft.sourceAccountId ?? '', // sourceAccountId + transactionDraft.destinationAccountId ?? '', // destinationAccountId + transactionDraft.sourceAmount ?? 0, // sourceAmount + transactionDraft.destinationAmount ?? 0, // destinationAmount + transactionDraft.hideAmount ?? false, // hideAmount + transactionDraft.tagIds ?? [], // tagIds + transactionDraft.comment ?? '', // comment + true // editable + ); + + if (transactionDraft.pictures) { + const pictures: TransactionPicture[] = []; + + for (const picture of transactionDraft.pictures) { + pictures.push(TransactionPicture.of(picture)); + } + + transaction.setPictures(pictures); + } + + return transaction; + } +} + +export class TransactionPicture implements TransactionPictureInfoBasicResponse { + public pictureId: string; + public originalUrl: string; + + private constructor(pictureId: string, originalUrl: string) { + this.pictureId = pictureId; + this.originalUrl = originalUrl; + } + + public static of(picture: TransactionPictureInfoBasicResponse): TransactionPicture { + return new TransactionPicture(picture.pictureId, picture.originalUrl); + } + + public static ofMany(pictureResponses: TransactionPictureInfoBasicResponse[]): TransactionPicture[] { + const pictures: TransactionPicture[] = []; + + for (const pictureResponse of pictureResponses) { + pictures.push(TransactionPicture.of(pictureResponse)); + } + + return pictures; + } +} + +export class TransactionGeoLocation implements TransactionGeoLocationRequest { + public latitude: number; + public longitude: number; + + private constructor(latitude: number, longitude: number) { + this.latitude = latitude; + this.longitude = longitude; + } + + public static createNewGeoLocation(latitude: number, longitude: number): TransactionGeoLocation { + return new TransactionGeoLocation(latitude, longitude); + } + + public static of(geoLocation: TransactionGeoLocationRequest): TransactionGeoLocation { + return new TransactionGeoLocation(geoLocation.latitude, geoLocation.longitude); + } +} + +export interface TransactionDraft { + readonly type?: number; + readonly categoryId?: string; + readonly sourceAccountId?: string; + readonly sourceAmount?: number; + readonly destinationAccountId?: string; + readonly destinationAmount?: number; + readonly hideAmount?: boolean; + readonly tagIds?: string[]; + readonly pictures?: TransactionPictureInfoBasicResponse[]; + readonly comment?: string; +} export interface TransactionGeoLocationRequest { readonly latitude: number; @@ -209,7 +649,7 @@ export class TransactionAmountsRequest { export interface TransactionInfoPageWrapperResponse { readonly items: TransactionInfoResponse[]; - readonly nextTimeSequenceId?: string; + readonly nextTimeSequenceId?: number; readonly totalCount?: number; } @@ -218,6 +658,11 @@ export interface TransactionInfoPageWrapperResponse2 { readonly totalCount: number; } +export interface TransactionPageWrapper { + readonly items: Transaction[]; + readonly totalCount?: number; +} + export interface TransactionStatisticResponse { readonly startTime: number; readonly endTime: number; @@ -344,3 +789,8 @@ export interface TransactionMonthlyIncomeAndExpenseData { readonly incompleteIncomeAmount: boolean; readonly incompleteExpenseAmount: boolean; } + +export const EMPTY_TRANSACTION_RESULT: TransactionPageWrapper = { + items: [], + totalCount: 0 +} diff --git a/src/stores/index.ts b/src/stores/index.ts index dc7459b1..9a33fe11 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -8,8 +8,7 @@ import { useTransactionCategoriesStore } from './transactionCategory.ts'; import { useTransactionTagsStore } from './transactionTag.ts'; // @ts-expect-error the above file is migrating to ts import { useTransactionTemplatesStore } from './transactionTemplate.js'; -// @ts-expect-error the above file is migrating to ts -import { useTransactionsStore } from './transaction.js'; +import { useTransactionsStore } from './transaction.ts'; import { useOverviewStore } from './overview.ts'; import { useStatisticsStore } from './statistics.ts'; import { useExchangeRatesStore } from './exchangeRates.ts'; diff --git a/src/stores/transaction.js b/src/stores/transaction.js deleted file mode 100644 index 4b45c5e0..00000000 --- a/src/stores/transaction.js +++ /dev/null @@ -1,1330 +0,0 @@ -import { defineStore } from 'pinia'; - -import { useSettingsStore } from './setting.ts'; -import { useUserStore } from './user.ts'; -import { useAccountsStore } from './account.ts'; -import { useTransactionCategoriesStore } from './transactionCategory.ts'; -import { useOverviewStore } from './overview.ts'; -import { useStatisticsStore } from './statistics.ts'; -import { useExchangeRatesStore } from './exchangeRates.ts'; - -import { DateRange } from '@/core/datetime.ts'; -import { CategoryType } from '@/core/category.ts'; -import { TransactionType, TransactionTagFilterType } from '@/core/transaction.ts'; -import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '@/consts/transaction.ts'; -import { - getUserTransactionDraft, - updateUserTransactionDraft, - clearUserTransactionDraft -} from '@/lib/userstate.ts'; -import services from '@/lib/services.ts'; -import logger from '@/lib/logger.ts'; -import { - isDefined, - isNumber, - isString -} from '@/lib/common.ts'; -import { - getCurrentUnixTime, - getTimezoneOffsetMinutes, - getBrowserTimezoneOffsetMinutes, - getActualUnixTimeForStore, - parseDateFromUnixTime, - getShortDate, - getYear, - getMonth, - getYearAndMonth, - getDay, - getDayOfWeekName -} from '@/lib/datetime.ts'; -import { getAmountWithDecimalNumberCount } from '@/lib/numeral.ts'; -import { getCurrencyFraction } from '@/lib/currency.ts'; -import { getFirstAvailableCategoryId } from '@/lib/category.ts'; - -const emptyTransactionResult = { - items: [], - transactionsNextTimeId: 0 -}; - -function loadTransactionList(state, settingsStore, exchangeRatesStore, { transactions, reload, autoExpand, defaultCurrency }) { - if (reload) { - state.transactions = []; - } - - if (transactions.items && transactions.items.length) { - const currentUtcOffset = getTimezoneOffsetMinutes(settingsStore.appSettings.timeZone); - let currentMonthListIndex = -1; - let currentMonthList = null; - - for (let i = 0; i < transactions.items.length; i++) { - const item = transactions.items[i]; - fillTransactionObject(state, item, currentUtcOffset); - - const transactionTime = parseDateFromUnixTime(item.time, item.utcOffset, currentUtcOffset); - const transactionYear = getYear(transactionTime); - const transactionMonth = getMonth(transactionTime); - const transactionYearMonth = getYearAndMonth(transactionTime); - - if (i === 0 && state.transactions.length > 0) { - const lastMonthList = state.transactions[state.transactions.length - 1]; - - if (lastMonthList.totalAmount.incompleteExpense || lastMonthList.totalAmount.incompleteIncome) { - // calculate the total amount of last month which has incomplete total amount before starting to process a new request - calculateMonthTotalAmount(exchangeRatesStore, lastMonthList, defaultCurrency, state.transactionsFilter.accountIds, false); - } - } - - if (currentMonthList && currentMonthList.year === transactionYear && currentMonthList.month === transactionMonth) { - currentMonthList.items.push(Object.freeze(item)); - - if (i === transactions.items.length - 1) { - // calculate the total amount of current month when processing the last transaction item of this request - calculateMonthTotalAmount(exchangeRatesStore, currentMonthList, defaultCurrency, state.transactionsFilter.accountIds, true); - } - continue; - } - - for (let j = currentMonthListIndex + 1; j < state.transactions.length; j++) { - if (state.transactions[j].year === transactionYear && state.transactions[j].month === transactionMonth) { - currentMonthListIndex = j; - currentMonthList = state.transactions[j]; - break; - } - } - - if (!currentMonthList || currentMonthList.year !== transactionYear || currentMonthList.month !== transactionMonth) { - // calculate the total amount of current month when processing the first transaction item of the next month - calculateMonthTotalAmount(exchangeRatesStore, currentMonthList, defaultCurrency, state.transactionsFilter.accountIds, false); - - state.transactions.push({ - year: transactionYear, - month: transactionMonth, - yearMonth: transactionYearMonth, - opened: autoExpand, - items: [] - }); - - currentMonthListIndex = state.transactions.length - 1; - currentMonthList = state.transactions[state.transactions.length - 1]; - } - - currentMonthList.items.push(Object.freeze(item)); - // init the total amount struct of current month when processing the first transaction item of current month - calculateMonthTotalAmount(exchangeRatesStore, currentMonthList, defaultCurrency, state.transactionsFilter.accountIds, true); - } - } - - if (transactions.nextTimeSequenceId) { - state.transactionsNextTimeId = transactions.nextTimeSequenceId; - } else { - calculateMonthTotalAmount(exchangeRatesStore, state.transactions[state.transactions.length - 1], defaultCurrency, state.transactionsFilter.accountIds, false); - state.transactionsNextTimeId = -1; - } -} - -function updateTransactionInTransactionList(state, settingsStore, exchangeRatesStore, { transaction, defaultCurrency }) { - const currentUtcOffset = getTimezoneOffsetMinutes(settingsStore.appSettings.timeZone); - const transactionTime = parseDateFromUnixTime(transaction.time, transaction.utcOffset, currentUtcOffset); - const transactionYear = getYear(transactionTime); - const transactionMonth = getMonth(transactionTime); - - for (let i = 0; i < state.transactions.length; i++) { - const transactionMonthList = state.transactions[i]; - - if (!transactionMonthList.items) { - continue; - } - - for (let j = 0; j < transactionMonthList.items.length; j++) { - if (transactionMonthList.items[j].id === transaction.id) { - fillTransactionObject(state, transaction, currentUtcOffset); - - if (transactionYear !== transactionMonthList.year || - transactionMonth !== transactionMonthList.month || - transaction.day !== transactionMonthList.items[j].day) { - state.transactionListStateInvalid = true; - return; - } - - if ((state.transactionsFilter.categoryIds && !state.allFilterCategoryIds[transaction.categoryId]) || - (state.transactionsFilter.accountIds && !state.allFilterAccountIds[transaction.sourceAccountId] && !state.allFilterAccountIds[transaction.destinationAccountId] && - (!transaction.sourceAccount || !state.allFilterAccountIds[transaction.sourceAccount.parentId]) && - (!transaction.destinationAccount || !state.allFilterAccountIds[transaction.destinationAccount.parentId]) - ) - ) { - transactionMonthList.items.splice(j, 1); - } else { - transactionMonthList.items.splice(j, 1, transaction); - } - - if (transactionMonthList.items.length < 1) { - state.transactions.splice(i, 1); - } else { - calculateMonthTotalAmount(exchangeRatesStore, transactionMonthList, defaultCurrency, state.transactionsFilter.accountIds, i >= state.transactions.length - 1 && state.transactionsNextTimeId > 0); - } - - return; - } - } - } -} - -function removeTransactionFromTransactionList(state, exchangeRatesStore, { transaction, defaultCurrency }) { - for (let i = 0; i < state.transactions.length; i++) { - const transactionMonthList = state.transactions[i]; - - if (!transactionMonthList.items || - transactionMonthList.items[0].time < transaction.time || - transactionMonthList.items[transactionMonthList.items.length - 1].time > transaction.time) { - continue; - } - - for (let j = 0; j < transactionMonthList.items.length; j++) { - if (transactionMonthList.items[j].id === transaction.id) { - transactionMonthList.items.splice(j, 1); - } - } - - if (transactionMonthList.items.length < 1) { - state.transactions.splice(i, 1); - } else { - calculateMonthTotalAmount(exchangeRatesStore, transactionMonthList, defaultCurrency, state.transactionsFilter.accountIds, i >= state.transactions.length - 1 && state.transactionsNextTimeId > 0); - } - } -} - -function calculateMonthTotalAmount(exchangeRatesStore, transactionMonthList, defaultCurrency, accountIds, incomplete) { - if (!transactionMonthList) { - return; - } - - let totalExpense = 0; - let totalIncome = 0; - let hasUnCalculatedTotalExpense = false; - let hasUnCalculatedTotalIncome = false; - - const allAccountIdsMap = {}; - let totalAccountIdsCount = 0; - - if (accountIds && accountIds !== '0') { - const allAccountIdsArray = accountIds.split(','); - - for (let i = 0; i < allAccountIdsArray.length; i++) { - if (allAccountIdsArray[i]) { - allAccountIdsMap[allAccountIdsArray[i]] = true; - totalAccountIdsCount++; - } - } - } - - for (let i = 0; i < transactionMonthList.items.length; i++) { - const transaction = transactionMonthList.items[i]; - - let amount = transaction.sourceAmount; - let account = transaction.sourceAccount; - - if (totalAccountIdsCount > 0 && transaction.destinationAccount - && (!allAccountIdsMap[transaction.sourceAccount.id] && !allAccountIdsMap[transaction.sourceAccount.parentId]) - && (allAccountIdsMap[transaction.destinationAccount.id] || allAccountIdsMap[transaction.destinationAccount.parentId])) { - amount = transaction.destinationAmount; - account = transaction.destinationAccount; - } - - if (!account) { - continue; - } - - if (account.currency !== defaultCurrency) { - const balance = exchangeRatesStore.getExchangedAmount(amount, account.currency, defaultCurrency); - - if (!isNumber(balance)) { - if (transaction.type === TransactionType.Expense) { - hasUnCalculatedTotalExpense = true; - } else if (transaction.type === TransactionType.Income) { - hasUnCalculatedTotalIncome = true; - } - - continue; - } - - amount = balance; - } - - if (transaction.type === TransactionType.Expense) { - totalExpense += amount; - } else if (transaction.type === TransactionType.Income) { - totalIncome += amount; - } else if (transaction.type === TransactionType.Transfer && totalAccountIdsCount > 0) { - if (allAccountIdsMap[transaction.sourceAccountId] && allAccountIdsMap[transaction.destinationAccountId]) { - // Do Nothing - } else if (transaction.sourceAccount && transaction.destinationAccount && allAccountIdsMap[transaction.sourceAccount.parentId] && allAccountIdsMap[transaction.destinationAccount.parentId]) { - // Do Nothing - } else if (transaction.sourceAccount && allAccountIdsMap[transaction.sourceAccount.parentId] && allAccountIdsMap[transaction.destinationAccountId]) { - // Do Nothing - } else if (transaction.destinationAccount && allAccountIdsMap[transaction.sourceAccountId] && allAccountIdsMap[transaction.destinationAccount.parentId]) { - // Do Nothing - } else if (allAccountIdsMap[transaction.sourceAccountId] || (transaction.sourceAccount && allAccountIdsMap[transaction.sourceAccount.parentId])) { - totalExpense += amount; - } else if (allAccountIdsMap[transaction.destinationAccountId] || (transaction.destinationAccount && allAccountIdsMap[transaction.destinationAccount.parentId])) { - totalIncome += amount; - } - } - } - - transactionMonthList.totalAmount = { - expense: Math.floor(totalExpense), - incompleteExpense: incomplete || hasUnCalculatedTotalExpense, - income: Math.floor(totalIncome), - incompleteIncome: incomplete || hasUnCalculatedTotalIncome - }; -} - -function fillTransactionObject(state, transaction, currentUtcOffset) { - if (!transaction) { - return; - } - - const accountsStore = useAccountsStore(); - const transactionCategoriesStore = useTransactionCategoriesStore(); - const transactionTime = parseDateFromUnixTime(transaction.time, transaction.utcOffset, currentUtcOffset); - - transaction.date = getShortDate(transactionTime); - transaction.day = getDay(transactionTime); - transaction.dayOfWeek = getDayOfWeekName(transactionTime); - - if (transaction.sourceAccountId) { - transaction.sourceAccount = accountsStore.allAccountsMap[transaction.sourceAccountId]; - } - - if (transaction.destinationAccountId) { - transaction.destinationAccount = accountsStore.allAccountsMap[transaction.destinationAccountId]; - } - - if (transaction.categoryId) { - transaction.category = transactionCategoriesStore.allTransactionCategoriesMap[transaction.categoryId]; - } - - return transaction; -} - -function buildBasicSubmitTransaction(transaction, dummyTime) { - const submitTransaction = { - type: transaction.type, - time: dummyTime ? getActualUnixTimeForStore(transaction.time, transaction.utcOffset, getBrowserTimezoneOffsetMinutes()) : transaction.time, - sourceAccountId: transaction.sourceAccountId, - sourceAmount: transaction.sourceAmount, - destinationAccountId: '0', - destinationAmount: 0, - hideAmount: transaction.hideAmount, - tagIds: transaction.tagIds, - comment: transaction.comment, - geoLocation: transaction.geoLocation, - utcOffset: transaction.utcOffset - }; - - if (transaction.type === TransactionType.Transfer) { - submitTransaction.destinationAccountId = transaction.destinationAccountId; - submitTransaction.destinationAmount = transaction.destinationAmount; - } - - return submitTransaction; -} - -function buildTransactionDraft(transaction) { - if (!transaction) { - return null; - } - - let categoryId = ''; - - if (transaction.type === TransactionType.Expense) { - categoryId = transaction.expenseCategory; - } else if (transaction.type === TransactionType.Income) { - categoryId = transaction.incomeCategory; - } else if (transaction.type === TransactionType.Transfer) { - categoryId = transaction.transferCategory; - } else { - return null; - } - - const transactionDraft = { - type: transaction.type, - categoryId: categoryId, - sourceAccountId: transaction.sourceAccountId, - sourceAmount: transaction.sourceAmount, - destinationAccountId: '0', - destinationAmount: 0, - hideAmount: transaction.hideAmount, - tagIds: transaction.tagIds, - pictures: transaction.pictures, - comment: transaction.comment, - }; - - if (transaction.type === TransactionType.Transfer) { - transactionDraft.destinationAccountId = transaction.destinationAccountId; - transactionDraft.destinationAmount = transaction.destinationAmount; - } - - return transactionDraft; -} - -export const useTransactionsStore = defineStore('transactions', { - state: () => ({ - transactionDraft: getUserTransactionDraft(), - transactionsFilter: { - dateType: DateRange.All.type, - maxTime: 0, - minTime: 0, - type: 0, - categoryIds: '', - accountIds: '', - tagIds: '', - tagFilterType: TransactionTagFilterType.Default.type, - amountFilter: '', - keyword: '' - }, - transactions: [], - transactionsNextTimeId: 0, - transactionListStateInvalid: true, - }), - getters: { - allFilterCategoryIds(state) { - if (!state.transactionsFilter.categoryIds) { - return {}; - } - - const allCategoryIds = state.transactionsFilter.categoryIds.split(','); - const ret = {}; - - for (let i = 0; i < allCategoryIds.length; i++) { - if (allCategoryIds[i]) { - ret[allCategoryIds[i]] = true; - } - } - - return ret; - }, - allFilterAccountIds(state) { - if (!state.transactionsFilter.accountIds) { - return {}; - } - - const allAccountIds = state.transactionsFilter.accountIds.split(','); - const ret = {}; - - for (let i = 0; i < allAccountIds.length; i++) { - if (allAccountIds[i]) { - ret[allAccountIds[i]] = true; - } - } - - return ret; - }, - allFilterTagIds(state) { - if (!state.transactionsFilter.tagIds) { - return {}; - } - - const allTagIds = state.transactionsFilter.tagIds.split(','); - const ret = {}; - - for (let i = 0; i < allTagIds.length; i++) { - if (allTagIds[i]) { - ret[allTagIds[i]] = true; - } - } - - return ret; - }, - allFilterCategoryIdsCount(state) { - if (!state.transactionsFilter.categoryIds) { - return 0; - } - - const allCategoryIds = state.transactionsFilter.categoryIds.split(','); - let count = 0; - - for (let i = 0; i < allCategoryIds.length; i++) { - if (allCategoryIds[i]) { - count++; - } - } - - return count; - }, - allFilterAccountIdsCount(state) { - if (!state.transactionsFilter.accountIds) { - return 0; - } - - const allAccountIds = state.transactionsFilter.accountIds.split(','); - let count = 0; - - for (let i = 0; i < allAccountIds.length; i++) { - if (allAccountIds[i]) { - count++; - } - } - - return count; - }, - allFilterTagIdsCount(state) { - if (!state.transactionsFilter.tagIds) { - return 0; - } - - const allTagIds = state.transactionsFilter.tagIds.split(','); - let count = 0; - - for (let i = 0; i < allTagIds.length; i++) { - if (allTagIds[i]) { - count++; - } - } - - return count; - }, - noTransaction(state) { - for (let i = 0; i < state.transactions.length; i++) { - const transactionMonthList = state.transactions[i]; - - for (let j = 0; j < transactionMonthList.items.length; j++) { - if (transactionMonthList.items[j]) { - return false; - } - } - } - - return true; - }, - hasMoreTransaction(state) { - return state.transactionsNextTimeId > 0; - } - }, - actions: { - initTransactionDraft() { - const settingsStore = useSettingsStore(); - - if (settingsStore.appSettings.autoSaveTransactionDraft === 'enabled' || settingsStore.appSettings.autoSaveTransactionDraft === 'confirmation') { - this.transactionDraft = getUserTransactionDraft(); - } else { - this.transactionDraft = null; - } - }, - isTransactionDraftModified(transaction) { - if (!transaction) { - return false; - } - - const userStore = useUserStore(); - const transactionCategoriesStore = useTransactionCategoriesStore(); - - if (transaction.sourceAmount !== 0) { - return true; - } - - if (transaction.type === TransactionType.Transfer && transaction.destinationAmount !== 0) { - return true; - } - - if (transaction.sourceAccountId && transaction.sourceAccountId !== '0' && transaction.sourceAccountId !== userStore.currentUserDefaultAccountId) { - return true; - } - - if (transaction.type === TransactionType.Transfer && transaction.destinationAccountId && transaction.destinationAccountId !== '0' && transaction.destinationAccountId !== userStore.currentUserDefaultAccountId) { - return true; - } - - const allCategories = transactionCategoriesStore.allTransactionCategories; - - if (allCategories) { - if (transaction.type === TransactionType.Expense) { - const defaultCategoryId = getFirstAvailableCategoryId(allCategories[CategoryType.Expense]); - - if (transaction.expenseCategory && transaction.expenseCategory !== '0' && transaction.expenseCategory !== defaultCategoryId) { - return true; - } - } else if (transaction.type === TransactionType.Income) { - const defaultCategoryId = getFirstAvailableCategoryId(allCategories[CategoryType.Income]); - - if (transaction.incomeCategory && transaction.incomeCategory !== '0' && transaction.incomeCategory !== defaultCategoryId) { - return true; - } - } else if (transaction.type === TransactionType.Transfer) { - const defaultCategoryId = getFirstAvailableCategoryId(allCategories[CategoryType.Transfer]); - - if (transaction.transferCategory && transaction.transferCategory !== '0' && transaction.transferCategory !== defaultCategoryId) { - return true; - } - } - } - - if (transaction.hideAmount) { - return true; - } - - if (transaction.tagIds && transaction.tagIds.length > 0) { - return true; - } - - if (transaction.pictures && transaction.pictures.length > 0) { - return true; - } - - if (transaction.comment && transaction.comment.trim()) { - return true; - } - - return false; - }, - saveTransactionDraft(transaction) { - const settingsStore = useSettingsStore(); - - if (settingsStore.appSettings.autoSaveTransactionDraft !== 'enabled' && settingsStore.appSettings.autoSaveTransactionDraft !== 'confirmation') { - this.clearTransactionDraft(); - return; - } - - if (transaction) { - if (!this.isTransactionDraftModified(transaction)) { - this.clearTransactionDraft(); - return; - } - - this.transactionDraft = buildTransactionDraft(transaction); - } - - updateUserTransactionDraft(this.transactionDraft); - }, - clearTransactionDraft() { - this.transactionDraft = null; - clearUserTransactionDraft(); - }, - generateNewTransactionModel(type) { - const settingsStore = useSettingsStore(); - const now = getCurrentUnixTime(); - const currentTimezone = settingsStore.appSettings.timeZone; - - let defaultType = TransactionType.Expense; - - if (type === TransactionType.Income.toString()) { - defaultType = TransactionType.Income; - } else if (type === TransactionType.Transfer.toString()) { - defaultType = TransactionType.Transfer; - } - - return { - type: defaultType, - time: now, - timeZone: currentTimezone, - utcOffset: getTimezoneOffsetMinutes(currentTimezone), - expenseCategory: '', - incomeCategory: '', - transferCategory: '', - sourceAccountId: '', - destinationAccountId: '', - sourceAmount: 0, - destinationAmount: 0, - hideAmount: false, - tagIds: [], - pictures: [], - comment: '', - geoLocation: null - }; - }, - setTransactionSuitableDestinationAmount(transaction, oldValue, newValue) { - const accountsStore = useAccountsStore(); - const exchangeRatesStore = useExchangeRatesStore(); - - if (transaction.type === TransactionType.Expense || transaction.type === TransactionType.Income) { - transaction.destinationAmount = newValue; - } else if (transaction.type === TransactionType.Transfer) { - const sourceAccount = accountsStore.allAccountsMap[transaction.sourceAccountId]; - const destinationAccount = accountsStore.allAccountsMap[transaction.destinationAccountId]; - - if (sourceAccount && destinationAccount && sourceAccount.currency !== destinationAccount.currency) { - const decimalNumberCount = getCurrencyFraction(destinationAccount.currency); - const exchangedOldValue = exchangeRatesStore.getExchangedAmount(oldValue, sourceAccount.currency, destinationAccount.currency); - const exchangedNewValue = exchangeRatesStore.getExchangedAmount(newValue, sourceAccount.currency, destinationAccount.currency); - - if (isNumber(exchangedOldValue)) { - oldValue = Math.floor(exchangedOldValue); - oldValue = getAmountWithDecimalNumberCount(oldValue, decimalNumberCount); - } - - if (isNumber(exchangedNewValue)) { - newValue = Math.floor(exchangedNewValue); - newValue = getAmountWithDecimalNumberCount(newValue, decimalNumberCount); - } else { - return; - } - } - - if ((!sourceAccount || !destinationAccount || transaction.destinationAmount === oldValue || transaction.destinationAmount === 0) && - (TRANSACTION_MIN_AMOUNT <= newValue && newValue <= TRANSACTION_MAX_AMOUNT)) { - transaction.destinationAmount = newValue; - } - } - }, - updateTransactionListInvalidState(invalidState) { - this.transactionListStateInvalid = invalidState; - }, - resetTransactions() { - this.transactionsFilter.dateType = DateRange.All.type; - this.transactionsFilter.maxTime = 0; - this.transactionsFilter.minTime = 0; - this.transactionsFilter.type = 0; - this.transactionsFilter.categoryIds = ''; - this.transactionsFilter.accountIds = ''; - this.transactionsFilter.tagIds = ''; - this.transactionsFilter.tagFilterType = TransactionTagFilterType.Default.type; - this.transactionsFilter.amountFilter = ''; - this.transactionsFilter.keyword = ''; - this.transactions = []; - this.transactionsNextTimeId = 0; - this.transactionListStateInvalid = true; - }, - clearTransactions() { - this.transactions = []; - this.transactionsNextTimeId = 0; - this.transactionListStateInvalid = true; - }, - initTransactionListFilter(filter) { - if (filter && isNumber(filter.dateType)) { - this.transactionsFilter.dateType = filter.dateType; - } else { - this.transactionsFilter.dateType = DateRange.All.type; - } - - if (filter && isNumber(filter.maxTime)) { - this.transactionsFilter.maxTime = filter.maxTime; - } else { - this.transactionsFilter.maxTime = 0; - } - - if (filter && isNumber(filter.minTime)) { - this.transactionsFilter.minTime = filter.minTime; - } else { - this.transactionsFilter.minTime = 0; - } - - if (filter && isNumber(filter.type)) { - this.transactionsFilter.type = filter.type; - } else { - this.transactionsFilter.type = 0; - } - - if (filter && isString(filter.categoryIds)) { - this.transactionsFilter.categoryIds = filter.categoryIds; - } else { - this.transactionsFilter.categoryIds = ''; - } - - if (filter && isString(filter.accountIds)) { - this.transactionsFilter.accountIds = filter.accountIds; - } else { - this.transactionsFilter.accountIds = ''; - } - - if (filter && isString(filter.tagIds)) { - this.transactionsFilter.tagIds = filter.tagIds; - } else { - this.transactionsFilter.tagIds = ''; - } - - if (filter && isNumber(filter.tagFilterType)) { - this.transactionsFilter.tagFilterType = filter.tagFilterType; - } else { - this.transactionsFilter.tagFilterType = TransactionTagFilterType.Default.type; - } - - if (filter && isString(filter.amountFilter)) { - this.transactionsFilter.amountFilter = filter.amountFilter; - } else { - this.transactionsFilter.amountFilter = ''; - } - - if (filter && isString(filter.keyword)) { - this.transactionsFilter.keyword = filter.keyword; - } else { - this.transactionsFilter.keyword = ''; - } - }, - updateTransactionListFilter(filter) { - const accountsStore = useAccountsStore(); - let changed = false; - - if (filter && isNumber(filter.dateType) && this.transactionsFilter.dateType !== filter.dateType) { - this.transactionsFilter.dateType = filter.dateType; - changed = true; - } - - if (filter && isNumber(filter.maxTime) && this.transactionsFilter.maxTime !== filter.maxTime) { - this.transactionsFilter.maxTime = filter.maxTime; - changed = true; - } - - if (filter && isNumber(filter.minTime) && this.transactionsFilter.minTime !== filter.minTime) { - this.transactionsFilter.minTime = filter.minTime; - changed = true; - } - - if (filter && isNumber(filter.type) && this.transactionsFilter.type !== filter.type) { - this.transactionsFilter.type = filter.type; - changed = true; - } - - if (filter && isString(filter.categoryIds) && this.transactionsFilter.categoryIds !== filter.categoryIds) { - this.transactionsFilter.categoryIds = filter.categoryIds; - changed = true; - } - - if (filter && isString(filter.accountIds) && this.transactionsFilter.accountIds !== filter.accountIds) { - if (DateRange.isBillingCycle(this.transactionsFilter.dateType) && - (!accountsStore.getAccountStatementDate(filter.accountIds) || accountsStore.getAccountStatementDate(filter.accountIds) !== accountsStore.getAccountStatementDate(this.transactionsFilter.accountIds))) { - this.transactionsFilter.dateType = DateRange.Custom.type; - } - - this.transactionsFilter.accountIds = filter.accountIds; - - changed = true; - } - - if (filter && isString(filter.tagIds) && this.transactionsFilter.tagIds !== filter.tagIds) { - this.transactionsFilter.tagIds = filter.tagIds; - changed = true; - } - - if (filter && isNumber(filter.tagFilterType) && this.transactionsFilter.tagFilterType !== filter.tagFilterType) { - this.transactionsFilter.tagFilterType = filter.tagFilterType; - changed = true; - } - - if (filter && isString(filter.amountFilter) && this.transactionsFilter.amountFilter !== filter.amountFilter) { - this.transactionsFilter.amountFilter = filter.amountFilter; - changed = true; - } - - if (filter && isString(filter.keyword) && this.transactionsFilter.keyword !== filter.keyword) { - this.transactionsFilter.keyword = filter.keyword; - changed = true; - } - - return changed; - }, - getTransactionListPageParams() { - const querys = []; - - if (this.transactionsFilter.type) { - querys.push('type=' + this.transactionsFilter.type); - } - - if (this.transactionsFilter.accountIds) { - querys.push('accountIds=' + this.transactionsFilter.accountIds); - } - - if (this.transactionsFilter.categoryIds) { - querys.push('categoryIds=' + this.transactionsFilter.categoryIds); - } - - if (this.transactionsFilter.tagIds) { - querys.push('tagIds=' + this.transactionsFilter.tagIds); - } - - if (this.transactionsFilter.tagFilterType) { - querys.push('tagFilterType=' + this.transactionsFilter.tagFilterType); - } - - querys.push('dateType=' + this.transactionsFilter.dateType); - - if (DateRange.isBillingCycle(this.transactionsFilter.dateType) || this.transactionsFilter.dateType === DateRange.Custom.type) { - querys.push('maxTime=' + this.transactionsFilter.maxTime); - querys.push('minTime=' + this.transactionsFilter.minTime); - } - - if (this.transactionsFilter.amountFilter) { - querys.push('amountFilter=' + encodeURIComponent(this.transactionsFilter.amountFilter)); - } - - if (this.transactionsFilter.keyword) { - querys.push('keyword=' + encodeURIComponent(this.transactionsFilter.keyword)); - } - - return querys.join('&'); - }, - loadTransactions({ reload, count, page, withCount, autoExpand, defaultCurrency }) { - const self = this; - const settingsStore = useSettingsStore(); - const exchangeRatesStore = useExchangeRatesStore(); - let actualMaxTime = self.transactionsNextTimeId; - - if (reload && self.transactionsFilter.maxTime > 0) { - actualMaxTime = self.transactionsFilter.maxTime * 1000 + 999; - } else if (reload && self.transactionsFilter.maxTime <= 0) { - actualMaxTime = 0; - } - - return new Promise((resolve, reject) => { - services.getTransactions({ - maxTime: actualMaxTime, - minTime: self.transactionsFilter.minTime * 1000, - count: count || 50, - page: page || 1, - withCount: (!!withCount) || false, - type: self.transactionsFilter.type, - categoryIds: self.transactionsFilter.categoryIds, - accountIds: self.transactionsFilter.accountIds, - tagIds: self.transactionsFilter.tagIds, - tagFilterType: self.transactionsFilter.tagFilterType, - amountFilter: self.transactionsFilter.amountFilter, - keyword: self.transactionsFilter.keyword - }).then(response => { - const data = response.data; - - if (!data || !data.success || !data.result) { - if (reload) { - loadTransactionList(self, settingsStore, exchangeRatesStore, { - transactions: emptyTransactionResult, - reload: reload, - autoExpand: autoExpand, - defaultCurrency: defaultCurrency - }); - - if (!self.transactionListStateInvalid) { - self.updateTransactionListInvalidState(true); - } - } - - reject({ message: 'Unable to retrieve transaction list' }); - return; - } - - loadTransactionList(self, settingsStore, exchangeRatesStore, { - transactions: data.result, - reload: reload, - autoExpand: autoExpand, - defaultCurrency: defaultCurrency - }); - - if (reload) { - if (self.transactionListStateInvalid) { - self.updateTransactionListInvalidState(false); - } - } - - resolve(data.result); - }).catch(error => { - logger.error('failed to load transaction list', error); - - if (reload) { - loadTransactionList(self, settingsStore, exchangeRatesStore, { - transactions: emptyTransactionResult, - reload: reload, - autoExpand: autoExpand, - defaultCurrency: defaultCurrency - }); - - if (!self.transactionListStateInvalid) { - self.updateTransactionListInvalidState(true); - } - } - - if (error.response && error.response.data && error.response.data.errorMessage) { - reject({ error: error.response.data }); - } else if (!error.processed) { - reject({ message: 'Unable to retrieve transaction list' }); - } else { - reject(error); - } - }); - }); - }, - loadMonthlyAllTransactions({ year, month, autoExpand, defaultCurrency }) { - const self = this; - const settingsStore = useSettingsStore(); - const exchangeRatesStore = useExchangeRatesStore(); - - return new Promise((resolve, reject) => { - services.getAllTransactionsByMonth({ - year: year, - month: month, - type: self.transactionsFilter.type, - categoryIds: self.transactionsFilter.categoryIds, - accountIds: self.transactionsFilter.accountIds, - tagIds: self.transactionsFilter.tagIds, - tagFilterType: self.transactionsFilter.tagFilterType, - amountFilter: self.transactionsFilter.amountFilter, - keyword: self.transactionsFilter.keyword - }).then(response => { - const data = response.data; - - if (!data || !data.success || !data.result) { - loadTransactionList(self, settingsStore, exchangeRatesStore, { - transactions: emptyTransactionResult, - reload: true, - autoExpand: autoExpand, - defaultCurrency: defaultCurrency - }); - - if (!self.transactionListStateInvalid) { - self.updateTransactionListInvalidState(true); - } - - reject({ message: 'Unable to retrieve transaction list' }); - return; - } - - loadTransactionList(self, settingsStore, exchangeRatesStore, { - transactions: data.result, - reload: true, - autoExpand: autoExpand, - defaultCurrency: defaultCurrency - }); - - if (self.transactionListStateInvalid) { - self.updateTransactionListInvalidState(false); - } - - resolve(data.result); - }).catch(error => { - logger.error('failed to load monthly all transaction list', error); - - loadTransactionList(self, settingsStore, exchangeRatesStore, { - transactions: emptyTransactionResult, - reload: true, - autoExpand: autoExpand, - defaultCurrency: defaultCurrency - }); - - if (!self.transactionListStateInvalid) { - self.updateTransactionListInvalidState(true); - } - - if (error.response && error.response.data && error.response.data.errorMessage) { - reject({ error: error.response.data }); - } else if (!error.processed) { - reject({ message: 'Unable to retrieve transaction list' }); - } else { - reject(error); - } - }); - }); - }, - getTransaction({ transactionId, withPictures }) { - return new Promise((resolve, reject) => { - if (!isDefined(withPictures)) { - withPictures = true; - } - - services.getTransaction({ - id: transactionId, - withPictures: withPictures - }).then(response => { - const data = response.data; - - if (!data || !data.success || !data.result) { - reject({ message: 'Unable to retrieve transaction' }); - return; - } - - resolve(data.result); - }).catch(error => { - logger.error('failed to load transaction info', error); - - if (error.response && error.response.data && error.response.data.errorMessage) { - reject({ error: error.response.data }); - } else if (!error.processed) { - reject({ message: 'Unable to retrieve transaction' }); - } else { - reject(error); - } - }); - }); - }, - saveTransaction({ transaction, defaultCurrency, isEdit, clientSessionId }) { - const self = this; - const settingsStore = useSettingsStore(); - const exchangeRatesStore = useExchangeRatesStore(); - - const submitTransaction = buildBasicSubmitTransaction(transaction, true); - - if (transaction.type === TransactionType.Expense) { - submitTransaction.categoryId = transaction.expenseCategory; - } else if (transaction.type === TransactionType.Income) { - submitTransaction.categoryId = transaction.incomeCategory; - } else if (transaction.type === TransactionType.Transfer) { - submitTransaction.categoryId = transaction.transferCategory; - } else { - return Promise.reject('An error occurred'); - } - - if (clientSessionId) { - submitTransaction.clientSessionId = clientSessionId; - } - - if (transaction.pictures && transaction.pictures.length > 0) { - const pictureIds = []; - - for (let i = 0; i < transaction.pictures.length; i++) { - if (transaction.pictures[i].pictureId) { - pictureIds.push(transaction.pictures[i].pictureId); - } - } - - submitTransaction.pictureIds = pictureIds; - } - - if (isEdit) { - submitTransaction.id = transaction.id; - } - - return new Promise((resolve, reject) => { - let promise = null; - - if (!submitTransaction.id) { - promise = services.addTransaction(submitTransaction); - } else { - promise = services.modifyTransaction(submitTransaction); - } - - promise.then(response => { - const data = response.data; - - if (!data || !data.success || !data.result) { - if (!submitTransaction.id) { - reject({ message: 'Unable to add transaction' }); - } else { - reject({ message: 'Unable to save transaction' }); - } - return; - } - - if (!submitTransaction.id) { - if (!self.transactionListStateInvalid) { - self.updateTransactionListInvalidState(true); - } - } else { - updateTransactionInTransactionList(self, settingsStore, exchangeRatesStore, { - transaction: data.result, - defaultCurrency: defaultCurrency - }); - } - - const accountsStore = useAccountsStore(); - if (!accountsStore.accountListStateInvalid) { - accountsStore.updateAccountListInvalidState(true); - } - - const overviewStore = useOverviewStore(); - if (!overviewStore.transactionOverviewStateInvalid) { - overviewStore.updateTransactionOverviewInvalidState(true); - } - - const statisticsStore = useStatisticsStore(); - if (!statisticsStore.transactionStatisticsStateInvalid) { - statisticsStore.updateTransactionStatisticsInvalidState(true); - } - - resolve(data.result); - }).catch(error => { - logger.error('failed to save transaction', error); - - if (error.response && error.response.data && error.response.data.errorMessage) { - reject({ error: error.response.data }); - } else if (!error.processed) { - if (!submitTransaction.id) { - reject({ message: 'Unable to add transaction' }); - } else { - reject({ message: 'Unable to save transaction' }); - } - } else { - reject(error); - } - }); - }); - }, - deleteTransaction({ transaction, defaultCurrency, beforeResolve }) { - const self = this; - const exchangeRatesStore = useExchangeRatesStore(); - - return new Promise((resolve, reject) => { - services.deleteTransaction({ - id: transaction.id - }).then(response => { - const data = response.data; - - if (!data || !data.success || !data.result) { - reject({ message: 'Unable to delete this transaction' }); - return; - } - - if (beforeResolve) { - beforeResolve(() => { - removeTransactionFromTransactionList(self, exchangeRatesStore, { - transaction: transaction, - defaultCurrency: defaultCurrency - }); - }); - } else { - removeTransactionFromTransactionList(self, exchangeRatesStore, { - transaction: transaction, - defaultCurrency: defaultCurrency - }); - } - - const accountsStore = useAccountsStore(); - if (!accountsStore.accountListStateInvalid) { - accountsStore.updateAccountListInvalidState(true); - } - - const overviewStore = useOverviewStore(); - if (!overviewStore.transactionOverviewStateInvalid) { - overviewStore.updateTransactionOverviewInvalidState(true); - } - - const statisticsStore = useStatisticsStore(); - if (!statisticsStore.transactionStatisticsStateInvalid) { - statisticsStore.updateTransactionStatisticsInvalidState(true); - } - - resolve(data.result); - }).catch(error => { - logger.error('failed to delete transaction', error); - - if (error.response && error.response.data && error.response.data.errorMessage) { - reject({ error: error.response.data }); - } else if (!error.processed) { - reject({ message: 'Unable to delete this transaction' }); - } else { - reject(error); - } - }); - }); - }, - parseImportTransaction({ fileType, importFile }) { - return new Promise((resolve, reject) => { - services.parseImportTransaction({ fileType, importFile }).then(response => { - const data = response.data; - - if (!data || !data.success || !data.result) { - reject({ message: 'Unable to parse import file' }); - return; - } - - resolve(data.result); - }).catch(error => { - logger.error('Unable to parse import file', error); - - if (error.response && error.response.data && error.response.data.errorMessage) { - reject({ error: error.response.data }); - } else if (!error.processed) { - reject({ message: 'Unable to parse import file' }); - } else { - reject(error); - } - }); - }); - }, - importTransactions({ transactions, clientSessionId }) { - const submitTransactions = []; - - if (transactions) { - for (let i = 0; i < transactions.length; i++) { - const transaction = transactions[i]; - const submitTransaction = buildBasicSubmitTransaction(transaction, false); - - submitTransaction.categoryId = transaction.categoryId; - submitTransactions.push(submitTransaction); - } - } - - return new Promise((resolve, reject) => { - services.importTransactions({ - transactions: submitTransactions, - clientSessionId: clientSessionId - }).then(response => { - const data = response.data; - - if (!data || !data.success || !data.result) { - reject({ message: 'Unable to import transactions' }); - return; - } - - resolve(data.result); - }).catch(error => { - logger.error('Unable to import transactions', error); - - if (error.response && error.response.data && error.response.data.errorMessage) { - reject({ error: error.response.data }); - } else if (!error.processed) { - reject({ message: 'Unable to import transactions' }); - } else { - reject(error); - } - }); - }); - }, - uploadTransactionPicture({ pictureFile, clientSessionId }) { - return new Promise((resolve, reject) => { - services.uploadTransactionPicture({ pictureFile, clientSessionId }).then(response => { - const data = response.data; - - if (!data || !data.success || !data.result) { - reject({ message: 'Unable to upload transaction picture' }); - return; - } - - resolve(data.result); - }).catch(error => { - logger.error('Unable to upload transaction picture', error); - - if (error.response && error.response.data && error.response.data.errorMessage) { - reject({ error: error.response.data }); - } else if (!error.processed) { - reject({ message: 'Unable to upload transaction picture' }); - } else { - reject(error); - } - }); - }); - }, - removeUnusedTransactionPicture({ pictureInfo }) { - return new Promise((resolve, reject) => { - services.removeUnusedTransactionPicture({ - id: pictureInfo.pictureId - }).then(response => { - const data = response.data; - - if (!data || !data.success || !data.result) { - reject({ message: 'Unable to remove transaction picture' }); - return; - } - - resolve(data.result); - }).catch(error => { - logger.error('failed to remove transaction picture', error); - - if (error.response && error.response.data && error.response.data.errorMessage) { - reject({ error: error.response.data }); - } else if (!error.processed) { - reject({ message: 'Unable to remove transaction picture' }); - } else { - reject(error); - } - }); - }); - }, - getTransactionPictureUrl(pictureInfo, disableBrowserCache) { - if (!pictureInfo || !pictureInfo.originalUrl) { - return null; - } - - return services.getTransactionPictureUrlWithToken(pictureInfo.originalUrl, disableBrowserCache); - }, - collapseMonthInTransactionList({ month, collapse }) { - if (month) { - month.opened = !collapse; - } - } - } -}); diff --git a/src/stores/transaction.ts b/src/stores/transaction.ts new file mode 100644 index 00000000..4a688009 --- /dev/null +++ b/src/stores/transaction.ts @@ -0,0 +1,1241 @@ +import { ref, computed } from 'vue'; +import { defineStore } from 'pinia'; + +import { useSettingsStore } from './setting.ts'; +import { useUserStore } from './user.ts'; +import { useAccountsStore } from './account.ts'; +import { useTransactionCategoriesStore } from './transactionCategory.ts'; +import { useOverviewStore } from './overview.ts'; +import { useStatisticsStore } from './statistics.ts'; +import { useExchangeRatesStore } from './exchangeRates.ts'; + +import type { BeforeResolveFunction } from '@/core/base.ts'; +import { DateRange } from '@/core/datetime.ts'; +import { CategoryType } from '@/core/category.ts'; +import { TransactionType, TransactionTagFilterType } from '@/core/transaction.ts'; +import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '@/consts/transaction.ts'; +import { + type TransactionDraft, + type TransactionCreateRequest, + type TransactionInfoResponse, + type TransactionPageWrapper, + Transaction, + EMPTY_TRANSACTION_RESULT +} from '@/models/transaction.ts'; +import type { + TransactionPictureInfoBasicResponse +} from '@/models/transaction_picture_info.ts'; +import { + type ImportTransactionResponsePageWrapper, + ImportTransaction +} from '@/models/imported_transaction.ts'; + +import { + getUserTransactionDraft, + updateUserTransactionDraft, + clearUserTransactionDraft +} from '@/lib/userstate.ts'; +import { + isDefined, + isNumber, + isString, + splitItemsToMap, + countSplitItems +} from '@/lib/common.ts'; +import { + getCurrentUnixTime, + getTimezoneOffsetMinutes, + getBrowserTimezoneOffsetMinutes, + getActualUnixTimeForStore, + parseDateFromUnixTime, + getShortDate, + getYear, + getMonth, + getYearAndMonth, + getDay, + getDayOfWeekName +} from '@/lib/datetime.ts'; +import { getAmountWithDecimalNumberCount } from '@/lib/numeral.ts'; +import { getCurrencyFraction } from '@/lib/currency.ts'; +import { getFirstAvailableCategoryId } from '@/lib/category.ts'; +import services, { type ApiResponsePromise } from '@/lib/services.ts'; +import logger from '@/lib/logger.ts'; + +export interface TransactionListPartialFilter { + dateType?: number; + maxTime?: number; + minTime?: number; + type?: number; + categoryIds?: string; + accountIds?: string; + tagIds?: string; + tagFilterType?: number; + amountFilter?: string; + keyword?: string; +} + +export interface TransactionListFilter extends TransactionListPartialFilter { + dateType: number; + maxTime: number; + minTime: number; + type: number; + categoryIds: string; + accountIds: string; + tagIds: string; + tagFilterType: number; + amountFilter: string; + keyword: string; +} + +export interface TransactionMonthList { + readonly year: number; + readonly month: number; + readonly yearMonth: string; + opened: boolean; + readonly items: Transaction[]; + readonly totalAmount: { + expense: number; + incompleteExpense: boolean; + income: number; + incompleteIncome: boolean; + }; +} + +export const useTransactionsStore = defineStore('transactions', () => { + const settingsStore = useSettingsStore(); + const userStore = useUserStore(); + const accountsStore = useAccountsStore(); + const transactionCategoriesStore = useTransactionCategoriesStore(); + const overviewStore = useOverviewStore(); + const statisticsStore = useStatisticsStore(); + const exchangeRatesStore = useExchangeRatesStore(); + + const transactionDraft = ref(getUserTransactionDraft()); + + const transactionsFilter = ref({ + dateType: DateRange.All.type, + maxTime: 0, + minTime: 0, + type: 0, + categoryIds: '', + accountIds: '', + tagIds: '', + tagFilterType: TransactionTagFilterType.Default.type, + amountFilter: '', + keyword: '' + }); + + const transactions = ref([]); + const transactionsNextTimeId = ref(0); + const transactionListStateInvalid = ref(true); + + const allFilterCategoryIds = computed>(() => splitItemsToMap(transactionsFilter.value.categoryIds, ',')); + const allFilterAccountIds = computed>(() => splitItemsToMap(transactionsFilter.value.accountIds, ',')); + const allFilterTagIds = computed>(() => splitItemsToMap(transactionsFilter.value.tagIds, ',')); + + const allFilterCategoryIdsCount = computed(() => countSplitItems(transactionsFilter.value.categoryIds, ',')); + const allFilterAccountIdsCount = computed(() => countSplitItems(transactionsFilter.value.accountIds, ',')); + const allFilterTagIdsCount = computed(() => countSplitItems(transactionsFilter.value.tagIds, ',')); + + const noTransaction = computed(() => { + for (let i = 0; i < transactions.value.length; i++) { + const transactionMonthList = transactions.value[i]; + + for (let j = 0; j < transactionMonthList.items.length; j++) { + if (transactionMonthList.items[j]) { + return false; + } + } + } + + return true; + }); + + const hasMoreTransaction = computed(() => { + return transactionsNextTimeId.value > 0; + }); + + function loadTransactionList({ transactionPageWrapper, reload, autoExpand, defaultCurrency, nextTimeSequenceId }: { transactionPageWrapper: TransactionPageWrapper, reload: boolean, autoExpand: boolean, defaultCurrency: string, nextTimeSequenceId?: number }): void { + if (reload) { + transactions.value = []; + } + + if (transactionPageWrapper.items && transactionPageWrapper.items.length) { + const currentUtcOffset = getTimezoneOffsetMinutes(settingsStore.appSettings.timeZone); + let currentMonthListIndex = -1; + let currentMonthList: TransactionMonthList | null = null; + + for (let i = 0; i < transactionPageWrapper.items.length; i++) { + const item = transactionPageWrapper.items[i]; + fillTransactionObject(item, currentUtcOffset); + + const transactionTime = parseDateFromUnixTime(item.time, item.utcOffset, currentUtcOffset); + const transactionYear = getYear(transactionTime); + const transactionMonth = getMonth(transactionTime); + const transactionYearMonth = getYearAndMonth(transactionTime); + + if (i === 0 && transactions.value.length > 0) { + const lastMonthList = transactions.value[transactions.value.length - 1]; + + if (lastMonthList.totalAmount.incompleteExpense || lastMonthList.totalAmount.incompleteIncome) { + // calculate the total amount of last month which has incomplete total amount before starting to process a new request + calculateMonthTotalAmount(lastMonthList, defaultCurrency, transactionsFilter.value.accountIds, false); + } + } + + if (currentMonthList && currentMonthList.year === transactionYear && currentMonthList.month === transactionMonth) { + currentMonthList.items.push(Object.freeze(item)); + + if (i === transactionPageWrapper.items.length - 1) { + // calculate the total amount of current month when processing the last transaction item of this request + calculateMonthTotalAmount(currentMonthList, defaultCurrency, transactionsFilter.value.accountIds, true); + } + continue; + } + + for (let j = currentMonthListIndex + 1; j < transactions.value.length; j++) { + if (transactions.value[j].year === transactionYear && transactions.value[j].month === transactionMonth) { + currentMonthListIndex = j; + currentMonthList = transactions.value[j]; + break; + } + } + + if (!currentMonthList || currentMonthList.year !== transactionYear || currentMonthList.month !== transactionMonth) { + // calculate the total amount of current month when processing the first transaction item of the next month + calculateMonthTotalAmount(currentMonthList, defaultCurrency, transactionsFilter.value.accountIds, false); + + const monthList: TransactionMonthList = { + year: transactionYear, + month: transactionMonth, + yearMonth: transactionYearMonth, + opened: autoExpand, + items: [], + totalAmount: { + expense: 0, + incompleteExpense: true, + income: 0, + incompleteIncome: true + } + }; + + transactions.value.push(monthList); + + currentMonthListIndex = transactions.value.length - 1; + currentMonthList = transactions.value[transactions.value.length - 1]; + } + + currentMonthList.items.push(Object.freeze(item)); + // init the total amount struct of current month when processing the first transaction item of current month + calculateMonthTotalAmount(currentMonthList, defaultCurrency, transactionsFilter.value.accountIds, true); + } + } + + if (nextTimeSequenceId) { + transactionsNextTimeId.value = nextTimeSequenceId; + } else { + calculateMonthTotalAmount(transactions.value[transactions.value.length - 1], defaultCurrency, transactionsFilter.value.accountIds, false); + transactionsNextTimeId.value = -1; + } + } + + function updateTransactionInTransactionList({ transaction, defaultCurrency }: { transaction: Transaction, defaultCurrency: string }): void { + const currentUtcOffset = getTimezoneOffsetMinutes(settingsStore.appSettings.timeZone); + const transactionTime = parseDateFromUnixTime(transaction.time, transaction.utcOffset, currentUtcOffset); + const transactionYear = getYear(transactionTime); + const transactionMonth = getMonth(transactionTime); + + for (let i = 0; i < transactions.value.length; i++) { + const transactionMonthList = transactions.value[i]; + + if (!transactionMonthList.items) { + continue; + } + + for (let j = 0; j < transactionMonthList.items.length; j++) { + if (transactionMonthList.items[j].id === transaction.id) { + fillTransactionObject(transaction, currentUtcOffset); + + if (transactionYear !== transactionMonthList.year || + transactionMonth !== transactionMonthList.month || + transaction.day !== transactionMonthList.items[j].day) { + transactionListStateInvalid.value = true; + return; + } + + if ((transactionsFilter.value.categoryIds && !allFilterCategoryIds.value[transaction.categoryId]) || + (transactionsFilter.value.accountIds && !allFilterAccountIds.value[transaction.sourceAccountId] && !allFilterAccountIds.value[transaction.destinationAccountId] && + (!transaction.sourceAccount || !allFilterAccountIds.value[transaction.sourceAccount.parentId]) && + (!transaction.destinationAccount || !allFilterAccountIds.value[transaction.destinationAccount.parentId]) + ) + ) { + transactionMonthList.items.splice(j, 1); + } else { + transactionMonthList.items.splice(j, 1, transaction); + } + + if (transactionMonthList.items.length < 1) { + transactions.value.splice(i, 1); + } else { + calculateMonthTotalAmount(transactionMonthList, defaultCurrency, transactionsFilter.value.accountIds, i >= transactions.value.length - 1 && transactionsNextTimeId.value > 0); + } + + return; + } + } + } + } + + function removeTransactionFromTransactionList({ transaction, defaultCurrency }: { transaction: TransactionInfoResponse, defaultCurrency: string }): void { + for (let i = 0; i transaction.time) { + continue; + } + + for (let j = 0; j < transactionMonthList.items.length; j++) { + if (transactionMonthList.items[j].id === transaction.id) { + transactionMonthList.items.splice(j, 1); + } + } + + if (transactionMonthList.items.length < 1) { + transactions.value.splice(i, 1); + } else { + calculateMonthTotalAmount(transactionMonthList, defaultCurrency, transactionsFilter.value.accountIds, i >= transactions.value.length - 1 && transactionsNextTimeId.value > 0); + } + } + } + + function calculateMonthTotalAmount(transactionMonthList: TransactionMonthList | null, defaultCurrency: string, accountIds: string, incomplete: boolean): void { + if (!transactionMonthList) { + return; + } + + let totalExpense = 0; + let totalIncome = 0; + let hasUnCalculatedTotalExpense = false; + let hasUnCalculatedTotalIncome = false; + + const allAccountIdsMap: Record = {}; + let totalAccountIdsCount = 0; + + if (accountIds && accountIds !== '0') { + const allAccountIdsArray = accountIds.split(','); + + for (let i = 0; i < allAccountIdsArray.length; i++) { + if (allAccountIdsArray[i]) { + allAccountIdsMap[allAccountIdsArray[i]] = true; + totalAccountIdsCount++; + } + } + } + + for (let i = 0; i < transactionMonthList.items.length; i++) { + const transaction = transactionMonthList.items[i]; + + let amount = transaction.sourceAmount; + let account = transaction.sourceAccount; + + if (totalAccountIdsCount > 0 && transaction.destinationAccount + && (!allAccountIdsMap[transaction.sourceAccount?.id || ''] && !allAccountIdsMap[transaction.sourceAccount?.parentId || '']) + && (allAccountIdsMap[transaction.destinationAccount.id] || allAccountIdsMap[transaction.destinationAccount.parentId])) { + amount = transaction.destinationAmount; + account = transaction.destinationAccount; + } + + if (!account) { + continue; + } + + if (account.currency !== defaultCurrency) { + const balance = exchangeRatesStore.getExchangedAmount(amount, account.currency, defaultCurrency); + + if (!isNumber(balance)) { + if (transaction.type === TransactionType.Expense) { + hasUnCalculatedTotalExpense = true; + } else if (transaction.type === TransactionType.Income) { + hasUnCalculatedTotalIncome = true; + } + + continue; + } + + amount = balance; + } + + if (transaction.type === TransactionType.Expense) { + totalExpense += amount; + } else if (transaction.type === TransactionType.Income) { + totalIncome += amount; + } else if (transaction.type === TransactionType.Transfer && totalAccountIdsCount > 0) { + if (allAccountIdsMap[transaction.sourceAccountId] && allAccountIdsMap[transaction.destinationAccountId]) { + // Do Nothing + } else if (transaction.sourceAccount && transaction.destinationAccount && allAccountIdsMap[transaction.sourceAccount.parentId] && allAccountIdsMap[transaction.destinationAccount.parentId]) { + // Do Nothing + } else if (transaction.sourceAccount && allAccountIdsMap[transaction.sourceAccount.parentId] && allAccountIdsMap[transaction.destinationAccountId]) { + // Do Nothing + } else if (transaction.destinationAccount && allAccountIdsMap[transaction.sourceAccountId] && allAccountIdsMap[transaction.destinationAccount.parentId]) { + // Do Nothing + } else if (allAccountIdsMap[transaction.sourceAccountId] || (transaction.sourceAccount && allAccountIdsMap[transaction.sourceAccount.parentId])) { + totalExpense += amount; + } else if (allAccountIdsMap[transaction.destinationAccountId] || (transaction.destinationAccount && allAccountIdsMap[transaction.destinationAccount.parentId])) { + totalIncome += amount; + } + } + } + + transactionMonthList.totalAmount.expense = Math.floor(totalExpense); + transactionMonthList.totalAmount.incompleteExpense = incomplete || hasUnCalculatedTotalExpense; + transactionMonthList.totalAmount.income = Math.floor(totalIncome); + transactionMonthList.totalAmount.incompleteIncome = incomplete || hasUnCalculatedTotalIncome; + } + + function fillTransactionObject(transaction: Transaction, currentUtcOffset: number): void { + if (!transaction) { + return; + } + + const transactionTime = parseDateFromUnixTime(transaction.time, transaction.utcOffset, currentUtcOffset); + transaction.setDisplayDate(getShortDate(transactionTime), getDay(transactionTime), getDayOfWeekName(transactionTime)); + + if (transaction.sourceAccountId) { + transaction.setSourceAccount(accountsStore.allAccountsMap[transaction.sourceAccountId]); + } + + if (transaction.destinationAccountId) { + transaction.setDestinationAccount(accountsStore.allAccountsMap[transaction.destinationAccountId]); + } + + if (transaction.categoryId) { + transaction.setCategory(transactionCategoriesStore.allTransactionCategoriesMap[transaction.categoryId]); + } + } + + function initTransactionDraft(): void { + if (settingsStore.appSettings.autoSaveTransactionDraft === 'enabled' || settingsStore.appSettings.autoSaveTransactionDraft === 'confirmation') { + transactionDraft.value = getUserTransactionDraft(); + } else { + transactionDraft.value = null; + } + } + + function isTransactionDraftModified(transaction?: Transaction): boolean { + if (!transaction) { + return false; + } + + if (transaction.sourceAmount !== 0) { + return true; + } + + if (transaction.type === TransactionType.Transfer && transaction.destinationAmount !== 0) { + return true; + } + + if (transaction.sourceAccountId && transaction.sourceAccountId !== '0' && transaction.sourceAccountId !== userStore.currentUserDefaultAccountId) { + return true; + } + + if (transaction.type === TransactionType.Transfer && transaction.destinationAccountId && transaction.destinationAccountId !== '0' && transaction.destinationAccountId !== userStore.currentUserDefaultAccountId) { + return true; + } + + const allCategories = transactionCategoriesStore.allTransactionCategories; + + if (allCategories) { + if (transaction.type === TransactionType.Expense) { + const defaultCategoryId = getFirstAvailableCategoryId(allCategories[CategoryType.Expense]); + + if (transaction.expenseCategoryId && transaction.expenseCategoryId !== '0' && transaction.expenseCategoryId !== defaultCategoryId) { + return true; + } + } else if (transaction.type === TransactionType.Income) { + const defaultCategoryId = getFirstAvailableCategoryId(allCategories[CategoryType.Income]); + + if (transaction.incomeCategoryId && transaction.incomeCategoryId !== '0' && transaction.incomeCategoryId !== defaultCategoryId) { + return true; + } + } else if (transaction.type === TransactionType.Transfer) { + const defaultCategoryId = getFirstAvailableCategoryId(allCategories[CategoryType.Transfer]); + + if (transaction.transferCategoryId && transaction.transferCategoryId !== '0' && transaction.transferCategoryId !== defaultCategoryId) { + return true; + } + } + } + + if (transaction.hideAmount) { + return true; + } + + if (transaction.tagIds && transaction.tagIds.length > 0) { + return true; + } + + if (transaction.pictures && transaction.pictures.length > 0) { + return true; + } + + if (transaction.comment && transaction.comment.trim()) { + return true; + } + + return false; + } + + function saveTransactionDraft(transaction?: Transaction): void { + if (settingsStore.appSettings.autoSaveTransactionDraft !== 'enabled' && settingsStore.appSettings.autoSaveTransactionDraft !== 'confirmation') { + clearTransactionDraft(); + return; + } + + if (transaction) { + if (!isTransactionDraftModified(transaction)) { + clearTransactionDraft(); + return; + } + + transactionDraft.value = transaction.toTransactionDraft(); + } + + updateUserTransactionDraft(transactionDraft.value); + } + + function clearTransactionDraft(): void { + transactionDraft.value = null; + clearUserTransactionDraft(); + } + + function generateNewTransactionModel(type: string): Transaction { + const now: number = getCurrentUnixTime(); + const currentTimezone: string = settingsStore.appSettings.timeZone; + + let defaultType: TransactionType = TransactionType.Expense; + + if (type === TransactionType.Income.toString()) { + defaultType = TransactionType.Income; + } else if (type === TransactionType.Transfer.toString()) { + defaultType = TransactionType.Transfer; + } + + return Transaction.createNewTransaction(defaultType, now, currentTimezone, getTimezoneOffsetMinutes(currentTimezone)); + } + + function setTransactionSuitableDestinationAmount(transaction: Transaction, oldValue: number, newValue: number): void { + if (transaction.type === TransactionType.Expense || transaction.type === TransactionType.Income) { + transaction.destinationAmount = newValue; + } else if (transaction.type === TransactionType.Transfer) { + const sourceAccount = accountsStore.allAccountsMap[transaction.sourceAccountId]; + const destinationAccount = accountsStore.allAccountsMap[transaction.destinationAccountId]; + + if (sourceAccount && destinationAccount && sourceAccount.currency !== destinationAccount.currency) { + const decimalNumberCount = getCurrencyFraction(destinationAccount.currency); + const exchangedOldValue = exchangeRatesStore.getExchangedAmount(oldValue, sourceAccount.currency, destinationAccount.currency); + const exchangedNewValue = exchangeRatesStore.getExchangedAmount(newValue, sourceAccount.currency, destinationAccount.currency); + + if (isNumber(decimalNumberCount) && isNumber(exchangedOldValue)) { + oldValue = Math.floor(exchangedOldValue); + oldValue = getAmountWithDecimalNumberCount(oldValue, decimalNumberCount); + } + + if (isNumber(decimalNumberCount) && isNumber(exchangedNewValue)) { + newValue = Math.floor(exchangedNewValue); + newValue = getAmountWithDecimalNumberCount(newValue, decimalNumberCount); + } else { + return; + } + } + + if ((!sourceAccount || !destinationAccount || transaction.destinationAmount === oldValue || transaction.destinationAmount === 0) && + (TRANSACTION_MIN_AMOUNT <= newValue && newValue <= TRANSACTION_MAX_AMOUNT)) { + transaction.destinationAmount = newValue; + } + } + } + + function updateTransactionListInvalidState(invalidState: boolean): void { + transactionListStateInvalid.value = invalidState; + } + + function resetTransactions(): void { + transactionsFilter.value.dateType = DateRange.All.type; + transactionsFilter.value.maxTime = 0; + transactionsFilter.value.minTime = 0; + transactionsFilter.value.type = 0; + transactionsFilter.value.categoryIds = ''; + transactionsFilter.value.accountIds = ''; + transactionsFilter.value.tagIds = ''; + transactionsFilter.value.tagFilterType = TransactionTagFilterType.Default.type; + transactionsFilter.value.amountFilter = ''; + transactionsFilter.value.keyword = ''; + transactions.value = []; + transactionsNextTimeId.value = 0; + transactionListStateInvalid.value = true; + } + + function clearTransactions(): void { + transactions.value = []; + transactionsNextTimeId.value = 0; + transactionListStateInvalid.value = true; + } + + function initTransactionListFilter(filter: TransactionListPartialFilter): void { + if (filter && isNumber(filter.dateType)) { + transactionsFilter.value.dateType = filter.dateType; + } else { + transactionsFilter.value.dateType = DateRange.All.type; + } + + if (filter && isNumber(filter.maxTime)) { + transactionsFilter.value.maxTime = filter.maxTime; + } else { + transactionsFilter.value.maxTime = 0; + } + + if (filter && isNumber(filter.minTime)) { + transactionsFilter.value.minTime = filter.minTime; + } else { + transactionsFilter.value.minTime = 0; + } + + if (filter && isNumber(filter.type)) { + transactionsFilter.value.type = filter.type; + } else { + transactionsFilter.value.type = 0; + } + + if (filter && isString(filter.categoryIds)) { + transactionsFilter.value.categoryIds = filter.categoryIds; + } else { + transactionsFilter.value.categoryIds = ''; + } + + if (filter && isString(filter.accountIds)) { + transactionsFilter.value.accountIds = filter.accountIds; + } else { + transactionsFilter.value.accountIds = ''; + } + + if (filter && isString(filter.tagIds)) { + transactionsFilter.value.tagIds = filter.tagIds; + } else { + transactionsFilter.value.tagIds = ''; + } + + if (filter && isNumber(filter.tagFilterType)) { + transactionsFilter.value.tagFilterType = filter.tagFilterType; + } else { + transactionsFilter.value.tagFilterType = TransactionTagFilterType.Default.type; + } + + if (filter && isString(filter.amountFilter)) { + transactionsFilter.value.amountFilter = filter.amountFilter; + } else { + transactionsFilter.value.amountFilter = ''; + } + + if (filter && isString(filter.keyword)) { + transactionsFilter.value.keyword = filter.keyword; + } else { + transactionsFilter.value.keyword = ''; + } + } + + function updateTransactionListFilter(filter: TransactionListPartialFilter): boolean { + let changed = false; + + if (filter && isNumber(filter.dateType) && transactionsFilter.value.dateType !== filter.dateType) { + transactionsFilter.value.dateType = filter.dateType; + changed = true; + } + + if (filter && isNumber(filter.maxTime) && transactionsFilter.value.maxTime !== filter.maxTime) { + transactionsFilter.value.maxTime = filter.maxTime; + changed = true; + } + + if (filter && isNumber(filter.minTime) && transactionsFilter.value.minTime !== filter.minTime) { + transactionsFilter.value.minTime = filter.minTime; + changed = true; + } + + if (filter && isNumber(filter.type) && transactionsFilter.value.type !== filter.type) { + transactionsFilter.value.type = filter.type; + changed = true; + } + + if (filter && isString(filter.categoryIds) && transactionsFilter.value.categoryIds !== filter.categoryIds) { + transactionsFilter.value.categoryIds = filter.categoryIds; + changed = true; + } + + if (filter && isString(filter.accountIds) && transactionsFilter.value.accountIds !== filter.accountIds) { + if (DateRange.isBillingCycle(transactionsFilter.value.dateType) && + (!accountsStore.getAccountStatementDate(filter.accountIds) || accountsStore.getAccountStatementDate(filter.accountIds) !== accountsStore.getAccountStatementDate(transactionsFilter.value.accountIds))) { + transactionsFilter.value.dateType = DateRange.Custom.type; + } + + transactionsFilter.value.accountIds = filter.accountIds; + changed = true; + } + + if (filter && isString(filter.tagIds) && transactionsFilter.value.tagIds !== filter.tagIds) { + transactionsFilter.value.tagIds = filter.tagIds; + changed = true; + } + + if (filter && isNumber(filter.tagFilterType) && transactionsFilter.value.tagFilterType !== filter.tagFilterType) { + transactionsFilter.value.tagFilterType = filter.tagFilterType; + changed = true; + } + + if (filter && isString(filter.amountFilter) && transactionsFilter.value.amountFilter !== filter.amountFilter) { + transactionsFilter.value.amountFilter = filter.amountFilter; + changed = true; + } + + if (filter && isString(filter.keyword) && transactionsFilter.value.keyword !== filter.keyword) { + transactionsFilter.value.keyword = filter.keyword; + changed = true; + } + + return changed; + } + + function getTransactionListPageParams(): string { + const querys: string[] = []; + + if (transactionsFilter.value.type) { + querys.push('type=' + transactionsFilter.value.type); + } + + if (transactionsFilter.value.accountIds) { + querys.push('accountIds=' + transactionsFilter.value.accountIds); + } + + if (transactionsFilter.value.categoryIds) { + querys.push('categoryIds=' + transactionsFilter.value.categoryIds); + } + + if (transactionsFilter.value.tagIds) { + querys.push('tagIds=' + transactionsFilter.value.tagIds); + } + + if (transactionsFilter.value.tagFilterType) { + querys.push('tagFilterType=' + transactionsFilter.value.tagFilterType); + } + + querys.push('dateType=' + transactionsFilter.value.dateType); + + if (DateRange.isBillingCycle(transactionsFilter.value.dateType) || transactionsFilter.value.dateType === DateRange.Custom.type) { + querys.push('maxTime=' + transactionsFilter.value.maxTime); + querys.push('minTime=' + transactionsFilter.value.minTime); + } + + if (transactionsFilter.value.amountFilter) { + querys.push('amountFilter=' + encodeURIComponent(transactionsFilter.value.amountFilter)); + } + + if (transactionsFilter.value.keyword) { + querys.push('keyword=' + encodeURIComponent(transactionsFilter.value.keyword)); + } + + return querys.join('&'); + } + + function loadTransactions({ reload, count, page, withCount, autoExpand, defaultCurrency }: { reload?: boolean, count?: number, page?: number, withCount?: boolean, autoExpand: boolean, defaultCurrency: string }): Promise { + let actualMaxTime = transactionsNextTimeId.value; + + if (reload && transactionsFilter.value.maxTime > 0) { + actualMaxTime = transactionsFilter.value.maxTime * 1000 + 999; + } else if (reload && transactionsFilter.value.maxTime <= 0) { + actualMaxTime = 0; + } + + return new Promise((resolve, reject) => { + services.getTransactions({ + maxTime: actualMaxTime, + minTime: transactionsFilter.value.minTime * 1000, + count: count || 50, + page: page || 1, + withCount: !!withCount, + type: transactionsFilter.value.type, + categoryIds: transactionsFilter.value.categoryIds, + accountIds: transactionsFilter.value.accountIds, + tagIds: transactionsFilter.value.tagIds, + tagFilterType: transactionsFilter.value.tagFilterType, + amountFilter: transactionsFilter.value.amountFilter, + keyword: transactionsFilter.value.keyword + }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + if (reload) { + loadTransactionList({ + transactionPageWrapper: EMPTY_TRANSACTION_RESULT, + reload: reload, + autoExpand: autoExpand, + defaultCurrency: defaultCurrency + }); + + if (!transactionListStateInvalid.value) { + updateTransactionListInvalidState(true); + } + } + + reject({ message: 'Unable to retrieve transaction list' }); + return; + } + + const transactionPageWrapper: TransactionPageWrapper = { + items: Transaction.ofMany(data.result.items), + totalCount: data.result.totalCount + }; + + loadTransactionList({ + transactionPageWrapper: transactionPageWrapper, + reload: !!reload, + autoExpand: autoExpand, + defaultCurrency: defaultCurrency, + nextTimeSequenceId: data.result.nextTimeSequenceId + }); + + if (reload) { + if (transactionListStateInvalid.value) { + updateTransactionListInvalidState(false); + } + } + + resolve(transactionPageWrapper); + }).catch(error => { + logger.error('failed to load transaction list', error); + + if (reload) { + loadTransactionList({ + transactionPageWrapper: EMPTY_TRANSACTION_RESULT, + reload: reload, + autoExpand: autoExpand, + defaultCurrency: defaultCurrency + }); + + if (!transactionListStateInvalid.value) { + updateTransactionListInvalidState(true); + } + } + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to retrieve transaction list' }); + } else { + reject(error); + } + }); + }); + } + + function loadMonthlyAllTransactions({ year, month, autoExpand, defaultCurrency }: { year: number, month: number, autoExpand: boolean, defaultCurrency: string }): Promise { + return new Promise((resolve, reject) => { + services.getAllTransactionsByMonth({ + year: year, + month: month, + type: transactionsFilter.value.type, + categoryIds: transactionsFilter.value.categoryIds, + accountIds: transactionsFilter.value.accountIds, + tagIds: transactionsFilter.value.tagIds, + tagFilterType: transactionsFilter.value.tagFilterType, + amountFilter: transactionsFilter.value.amountFilter, + keyword: transactionsFilter.value.keyword + }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + loadTransactionList({ + transactionPageWrapper: EMPTY_TRANSACTION_RESULT, + reload: true, + autoExpand: autoExpand, + defaultCurrency: defaultCurrency + }); + + if (!transactionListStateInvalid.value) { + updateTransactionListInvalidState(true); + } + + reject({ message: 'Unable to retrieve transaction list' }); + return; + } + + const transactionPageWrapper: TransactionPageWrapper = { + items: Transaction.ofMany(data.result.items), + totalCount: data.result.totalCount + }; + + loadTransactionList({ + transactionPageWrapper: transactionPageWrapper, + reload: true, + autoExpand: autoExpand, + defaultCurrency: defaultCurrency + }); + + if (transactionListStateInvalid.value) { + updateTransactionListInvalidState(false); + } + + resolve(transactionPageWrapper); + }).catch(error => { + logger.error('failed to load monthly all transaction list', error); + + loadTransactionList({ + transactionPageWrapper: EMPTY_TRANSACTION_RESULT, + reload: true, + autoExpand: autoExpand, + defaultCurrency: defaultCurrency + }); + + if (!transactionListStateInvalid.value) { + updateTransactionListInvalidState(true); + } + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to retrieve transaction list' }); + } else { + reject(error); + } + }); + }); + } + + function getTransaction({ transactionId, withPictures }: { transactionId: string, withPictures?: boolean }): Promise { + return new Promise((resolve, reject) => { + if (!isDefined(withPictures)) { + withPictures = true; + } + + services.getTransaction({ + id: transactionId, + withPictures: withPictures + }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to retrieve transaction' }); + return; + } + + const transaction = Transaction.of(data.result); + + resolve(transaction); + }).catch(error => { + logger.error('failed to load transaction info', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to retrieve transaction' }); + } else { + reject(error); + } + }); + }); + } + + function saveTransaction({ transaction, defaultCurrency, isEdit, clientSessionId }: { transaction: Transaction, defaultCurrency: string, isEdit: boolean, clientSessionId: string }): Promise { + return new Promise((resolve, reject) => { + const actualTime = getActualUnixTimeForStore(transaction.time, transaction.utcOffset, getBrowserTimezoneOffsetMinutes()); + let promise: ApiResponsePromise; + + if (transaction.type !== TransactionType.Expense && + transaction.type !== TransactionType.Income && + transaction.type !== TransactionType.Transfer) { + reject({ message: 'An error occurred' }); + return; + } + + if (!isEdit) { + promise = services.addTransaction(transaction.toCreateRequest(clientSessionId, actualTime)); + } else { + promise = services.modifyTransaction(transaction.toModifyRequest(actualTime)); + } + + promise.then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + if (!isEdit) { + reject({ message: 'Unable to add transaction' }); + } else { + reject({ message: 'Unable to save transaction' }); + } + } + + const transaction = Transaction.of(data.result); + + if (!isEdit) { + if (!transactionListStateInvalid.value) { + updateTransactionListInvalidState(true); + } + } else { + updateTransactionInTransactionList({ + transaction: transaction, + defaultCurrency: defaultCurrency + }); + } + + if (!accountsStore.accountListStateInvalid) { + accountsStore.updateAccountListInvalidState(true); + } + + if (!overviewStore.transactionOverviewStateInvalid) { + overviewStore.updateTransactionOverviewInvalidState(true); + } + + if (!statisticsStore.transactionStatisticsStateInvalid) { + statisticsStore.updateTransactionStatisticsInvalidState(true); + } + + resolve(transaction); + }).catch(error => { + logger.error('failed to save transaction', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + if (!isEdit) { + reject({ message: 'Unable to add transaction' }); + } else { + reject({ message: 'Unable to save transaction' }); + } + } else { + reject(error); + } + }); + }); + } + + function deleteTransaction({ transaction, defaultCurrency, beforeResolve }: { transaction: TransactionInfoResponse, defaultCurrency: string, beforeResolve?: BeforeResolveFunction }): Promise { + return new Promise((resolve, reject) => { + services.deleteTransaction({ + id: transaction.id + }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to delete this transaction' }); + return; + } + + if (beforeResolve) { + beforeResolve(() => { + removeTransactionFromTransactionList({ + transaction: transaction, + defaultCurrency: defaultCurrency + }); + }); + } else { + removeTransactionFromTransactionList({ + transaction: transaction, + defaultCurrency: defaultCurrency + }); + } + + if (!accountsStore.accountListStateInvalid) { + accountsStore.updateAccountListInvalidState(true); + } + + if (!overviewStore.transactionOverviewStateInvalid) { + overviewStore.updateTransactionOverviewInvalidState(true); + } + + if (!statisticsStore.transactionStatisticsStateInvalid) { + statisticsStore.updateTransactionStatisticsInvalidState(true); + } + + resolve(data.result); + }).catch(error => { + logger.error('failed to delete transaction', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to delete this transaction' }); + } else { + reject(error); + } + }); + }); + } + + function parseImportTransaction({ fileType, importFile }: { fileType: string, importFile: unknown }): Promise { + return new Promise((resolve, reject) => { + services.parseImportTransaction({ fileType, importFile }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to parse import file' }); + return; + } + + resolve(data.result); + }).catch(error => { + logger.error('Unable to parse import file', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to parse import file' }); + } else { + reject(error); + } + }); + }); + } + + function importTransactions({ transactions, clientSessionId }: { transactions: ImportTransaction[], clientSessionId: string }): Promise { + const submitTransactions: TransactionCreateRequest[] = []; + + if (transactions) { + for (const transaction of transactions) { + const submitTransaction = transaction.toCreateRequest(); + submitTransactions.push(submitTransaction); + } + } + + return new Promise((resolve, reject) => { + services.importTransactions({ + transactions: submitTransactions, + clientSessionId: clientSessionId + }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to import transactions' }); + return; + } + + resolve(data.result); + }).catch(error => { + logger.error('Unable to import transactions', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to import transactions' }); + } else { + reject(error); + } + }); + }); + } + + function uploadTransactionPicture({ pictureFile, clientSessionId }: { pictureFile: unknown, clientSessionId: string }): Promise { + return new Promise((resolve, reject) => { + services.uploadTransactionPicture({ pictureFile, clientSessionId }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to upload transaction picture' }); + return; + } + + resolve(data.result); + }).catch(error => { + logger.error('Unable to upload transaction picture', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to upload transaction picture' }); + } else { + reject(error); + } + }); + }); + } + + function removeUnusedTransactionPicture({ pictureInfo }: { pictureInfo: TransactionPictureInfoBasicResponse }): Promise { + return new Promise((resolve, reject) => { + services.removeUnusedTransactionPicture({ id: pictureInfo.pictureId }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to remove transaction picture' }); + return; + } + + resolve(data.result); + }).catch(error => { + logger.error('failed to remove transaction picture', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to remove transaction picture' }); + } else { + reject(error); + } + }); + }); + } + + function getTransactionPictureUrl(pictureInfo?: TransactionPictureInfoBasicResponse | null, disableBrowserCache?: boolean | string): string | null { + if (!pictureInfo || !pictureInfo.originalUrl) { + return null; + } + + return services.getTransactionPictureUrlWithToken(pictureInfo.originalUrl, disableBrowserCache); + } + + function collapseMonthInTransactionList({ month, collapse }: { month: TransactionMonthList, collapse: boolean }): void { + if (month) { + month.opened = !collapse; + } + } + + return { + // states + transactionDraft, + transactionsFilter, + transactions, + transactionsNextTimeId, + transactionListStateInvalid, + // computed states + allFilterCategoryIds, + allFilterAccountIds, + allFilterTagIds, + allFilterCategoryIdsCount, + allFilterAccountIdsCount, + allFilterTagIdsCount, + noTransaction, + hasMoreTransaction, + // functions + initTransactionDraft, + isTransactionDraftModified, + saveTransactionDraft, + clearTransactionDraft, + generateNewTransactionModel, + setTransactionSuitableDestinationAmount, + updateTransactionListInvalidState, + resetTransactions, + clearTransactions, + initTransactionListFilter, + updateTransactionListFilter, + getTransactionListPageParams, + loadTransactions, + loadMonthlyAllTransactions, + getTransaction, + saveTransaction, + deleteTransaction, + parseImportTransaction, + importTransactions, + uploadTransactionPicture, + removeUnusedTransactionPicture, + getTransactionPictureUrl, + collapseMonthInTransactionList + }; +}); diff --git a/src/stores/transactionTemplate.js b/src/stores/transactionTemplate.js index 6ac217e8..afc20998 100644 --- a/src/stores/transactionTemplate.js +++ b/src/stores/transactionTemplate.js @@ -238,11 +238,11 @@ export const useTransactionTemplatesStore = defineStore('transactionTemplates', } if (template.type === TransactionType.Expense) { - submitTemplate.categoryId = template.expenseCategory; + submitTemplate.categoryId = template.expenseCategoryId; } else if (template.type === TransactionType.Income) { - submitTemplate.categoryId = template.incomeCategory; + submitTemplate.categoryId = template.incomeCategoryId; } else if (template.type === TransactionType.Transfer) { - submitTemplate.categoryId = template.transferCategory; + submitTemplate.categoryId = template.transferCategoryId; submitTemplate.destinationAccountId = template.destinationAccountId; submitTemplate.destinationAmount = template.destinationAmount; } else { diff --git a/src/views/base/UnlockPageBase.ts b/src/views/base/UnlockPageBase.ts index 9ab3fd5f..4b202064 100644 --- a/src/views/base/UnlockPageBase.ts +++ b/src/views/base/UnlockPageBase.ts @@ -6,8 +6,7 @@ import { useRootStore } from '@/stores/index.ts'; import { useSettingsStore } from '@/stores/setting.ts'; import { useUserStore } from '@/stores/user.ts'; import { useTokensStore } from '@/stores/token.ts'; -// @ts-expect-error the above file is migrating to ts -import { useTransactionsStore } from '@/stores/transaction.js'; +import { useTransactionsStore } from '@/stores/transaction.ts'; import { useExchangeRatesStore } from '@/stores/exchangeRates.ts'; import { isWebAuthnSupported } from '@/lib/webauthn.ts'; diff --git a/src/views/base/settings/AppSettingsPageBase.ts b/src/views/base/settings/AppSettingsPageBase.ts index 74a2f07b..07393c78 100644 --- a/src/views/base/settings/AppSettingsPageBase.ts +++ b/src/views/base/settings/AppSettingsPageBase.ts @@ -3,8 +3,7 @@ import { computed } from 'vue'; import { useI18n } from '@/locales/helpers.ts'; import { useSettingsStore } from '@/stores/setting.ts'; -// @ts-expect-error the above file is migrating to ts -import { useTransactionsStore } from '@/stores/transaction.js'; +import { useTransactionsStore } from '@/stores/transaction.ts'; import { useOverviewStore } from '@/stores/overview.ts'; import { useStatisticsStore } from '@/stores/statistics.ts'; diff --git a/src/views/desktop/app/settings/tabs/AppLockSettingTab.vue b/src/views/desktop/app/settings/tabs/AppLockSettingTab.vue index 301b9457..32bc3868 100644 --- a/src/views/desktop/app/settings/tabs/AppLockSettingTab.vue +++ b/src/views/desktop/app/settings/tabs/AppLockSettingTab.vue @@ -71,7 +71,7 @@ import { useAppLockPageBase } from '@/views/base/settings/AppLockPageBase.ts'; import { useSettingsStore } from '@/stores/setting.ts'; import { useUserStore } from '@/stores/user.ts'; -import { useTransactionsStore } from '@/stores/transaction.js'; +import { useTransactionsStore } from '@/stores/transaction.ts'; import { registerWebAuthnCredential } from '@/lib/webauthn.ts'; import { diff --git a/src/views/desktop/common/cards/AccountFilterSettingsCard.vue b/src/views/desktop/common/cards/AccountFilterSettingsCard.vue index 003b35f5..ad402764 100644 --- a/src/views/desktop/common/cards/AccountFilterSettingsCard.vue +++ b/src/views/desktop/common/cards/AccountFilterSettingsCard.vue @@ -149,7 +149,7 @@ import { mapStores } from 'pinia'; import { useSettingsStore } from '@/stores/setting.ts'; import { useAccountsStore } from '@/stores/account.ts'; -import { useTransactionsStore } from '@/stores/transaction.js'; +import { useTransactionsStore } from '@/stores/transaction.ts'; import { useStatisticsStore } from '@/stores/statistics.ts'; import { AccountType, AccountCategory } from '@/core/account.ts'; diff --git a/src/views/desktop/common/cards/CategoryFilterSettingsCard.vue b/src/views/desktop/common/cards/CategoryFilterSettingsCard.vue index 53736371..acbbedc4 100644 --- a/src/views/desktop/common/cards/CategoryFilterSettingsCard.vue +++ b/src/views/desktop/common/cards/CategoryFilterSettingsCard.vue @@ -146,7 +146,7 @@ import { mapStores } from 'pinia'; import { useSettingsStore } from '@/stores/setting.ts'; import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts'; -import { useTransactionsStore } from '@/stores/transaction.js'; +import { useTransactionsStore } from '@/stores/transaction.ts'; import { useStatisticsStore } from '@/stores/statistics.ts'; import { CategoryType } from '@/core/category.ts'; diff --git a/src/views/desktop/common/cards/TransactionTagFilterSettingsCard.vue b/src/views/desktop/common/cards/TransactionTagFilterSettingsCard.vue index 3f618213..70f0f80f 100644 --- a/src/views/desktop/common/cards/TransactionTagFilterSettingsCard.vue +++ b/src/views/desktop/common/cards/TransactionTagFilterSettingsCard.vue @@ -133,7 +133,7 @@