From 2be329974e6d7969bdb79efe3f145fa8d72742a5 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Sun, 12 Jan 2025 23:47:56 +0800 Subject: [PATCH] migrate transaction category store to composition API and typescript --- src/lib/{category.js => category.ts} | 175 +++--- src/lib/services.ts | 4 +- src/lib/transaction.js | 2 +- src/models/transaction_category.ts | 132 +++++ src/stores/index.js | 2 +- src/stores/statistics.js | 4 +- src/stores/transaction.js | 4 +- src/stores/transactionCategory.js | 533 ------------------ src/stores/transactionCategory.ts | 518 +++++++++++++++++ src/views/desktop/SignupPage.vue | 2 +- src/views/desktop/categories/ListPage.vue | 4 +- .../categories/list/dialogs/EditDialog.vue | 13 +- .../categories/list/dialogs/PresetDialog.vue | 2 +- .../cards/CategoryFilterSettingsCard.vue | 16 +- .../desktop/statistics/TransactionPage.vue | 2 +- src/views/desktop/transactions/ListPage.vue | 4 +- .../list/dialogs/BatchReplaceDialog.vue | 4 +- .../transactions/list/dialogs/EditDialog.vue | 4 +- .../list/dialogs/ImportDialog.vue | 4 +- src/views/mobile/SignupPage.vue | 2 +- src/views/mobile/categories/AllPage.vue | 2 +- src/views/mobile/categories/EditPage.vue | 22 +- src/views/mobile/categories/ListPage.vue | 4 +- src/views/mobile/categories/PresetPage.vue | 2 +- .../settings/CategoryFilterSettingsPage.vue | 10 +- .../mobile/statistics/TransactionPage.vue | 2 +- src/views/mobile/transactions/EditPage.vue | 4 +- src/views/mobile/transactions/ListPage.vue | 4 +- 28 files changed, 818 insertions(+), 663 deletions(-) rename src/lib/{category.js => category.ts} (57%) delete mode 100644 src/stores/transactionCategory.js create mode 100644 src/stores/transactionCategory.ts diff --git a/src/lib/category.js b/src/lib/category.ts similarity index 57% rename from src/lib/category.js rename to src/lib/category.ts index d53116db..8ae5b5e2 100644 --- a/src/lib/category.js +++ b/src/lib/category.ts @@ -1,7 +1,8 @@ import { CategoryType } from '@/core/category.ts'; import { TransactionType } from '@/core/transaction.ts'; +import { type TransactionCategoriesWithVisibleCount, TransactionCategory } from '@/models/transaction_category.ts'; -export function setCategoryModelByAnotherCategory(category, category2) { +export function setCategoryModelByAnotherCategory(category: TransactionCategory, category2: TransactionCategory): void { category.id = category2.id; category.type = category2.type; category.parentId = category2.parentId; @@ -12,7 +13,7 @@ export function setCategoryModelByAnotherCategory(category, category2) { category.visible = !category2.hidden; } -export function transactionTypeToCategoryType(transactionType) { +export function transactionTypeToCategoryType(transactionType: TransactionType): CategoryType | null { if (transactionType === TransactionType.Income) { return CategoryType.Income; } else if (transactionType === TransactionType.Expense) { @@ -24,7 +25,7 @@ export function transactionTypeToCategoryType(transactionType) { } } -export function categoryTypeToTransactionType(categoryType) { +export function categoryTypeToTransactionType(categoryType: CategoryType): TransactionType | null { if (categoryType === CategoryType.Income) { return TransactionType.Income; } else if (categoryType === CategoryType.Expense) { @@ -36,14 +37,20 @@ export function categoryTypeToTransactionType(categoryType) { } } -export function getTransactionPrimaryCategoryName(categoryId, allCategories) { +export function getTransactionPrimaryCategoryName(categoryId: string, allCategories: TransactionCategory[]): string { if (!allCategories) { return ''; } for (let i = 0; i < allCategories.length; i++) { - for (let j = 0; j < allCategories[i].subCategories.length; j++) { - const subCategory = allCategories[i].subCategories[j]; + const subCategoryList = allCategories[i].secondaryCategories; + + if (!subCategoryList) { + continue; + } + + for (let j = 0; j < subCategoryList.length; j++) { + const subCategory = subCategoryList[j]; if (subCategory.id === categoryId) { return allCategories[i].name; } @@ -53,14 +60,20 @@ export function getTransactionPrimaryCategoryName(categoryId, allCategories) { return ''; } -export function getTransactionSecondaryCategoryName(categoryId, allCategories) { +export function getTransactionSecondaryCategoryName(categoryId: string, allCategories: TransactionCategory[]): string { if (!allCategories) { return ''; } for (let i = 0; i < allCategories.length; i++) { - for (let j = 0; j < allCategories[i].subCategories.length; j++) { - const subCategory = allCategories[i].subCategories[j]; + const subCategoryList = allCategories[i].secondaryCategories; + + if (!subCategoryList) { + continue; + } + + for (let j = 0; j < subCategoryList.length; j++) { + const subCategory = subCategoryList[j]; if (subCategory.id === categoryId) { return subCategory.name; } @@ -70,12 +83,12 @@ export function getTransactionSecondaryCategoryName(categoryId, allCategories) { return ''; } -export function allTransactionCategoriesWithVisibleCount(allTransactionCategories, allowCategoryTypes) { - const ret = {}; +export function allTransactionCategoriesWithVisibleCount(allTransactionCategories: Record, allowCategoryTypes: Record): Record { + const ret: Record = {}; const hasAllowCategoryTypes = allowCategoryTypes - && (allowCategoryTypes[CategoryType.Income.toString()] - || allowCategoryTypes[CategoryType.Expense.toString()] - || allowCategoryTypes[CategoryType.Transfer.toString()]); + && (allowCategoryTypes[CategoryType.Income] + || allowCategoryTypes[CategoryType.Expense] + || allowCategoryTypes[CategoryType.Transfer]); const allCategoryTypes = [ CategoryType.Income, CategoryType.Expense, CategoryType.Transfer ]; @@ -90,10 +103,10 @@ export function allTransactionCategoriesWithVisibleCount(allTransactionCategorie continue; } - const allCategories = allTransactionCategories[categoryType]; - const allSubCategories = {}; - const allVisibleSubCategoryCounts = {}; - const allFirstVisibleSubCategoryIndexes = {}; + const allCategories: TransactionCategory[] = allTransactionCategories[categoryType]; + const allSubCategories: Record = {}; + const allVisibleSubCategoryCounts: Record = {}; + const allFirstVisibleSubCategoryIndexes: Record = {}; let allVisibleCategoryCount = 0; let firstVisibleCategoryIndex = -1; @@ -108,12 +121,12 @@ export function allTransactionCategoriesWithVisibleCount(allTransactionCategorie } } - if (category.subCategories) { + if (category.secondaryCategories) { let visibleSubCategoryCount = 0; let firstVisibleSubCategoryIndex = -1; - for (let k = 0; k < category.subCategories.length; k++) { - const subCategory = category.subCategories[k]; + for (let k = 0; k < category.secondaryCategories.length; k++) { + const subCategory = category.secondaryCategories[k]; if (!subCategory.hidden) { visibleSubCategoryCount++; @@ -124,16 +137,16 @@ export function allTransactionCategoriesWithVisibleCount(allTransactionCategorie } } - if (category.subCategories.length > 0) { - allSubCategories[category.id] = category.subCategories; + if (category.secondaryCategories.length > 0) { + allSubCategories[category.id] = category.secondaryCategories; allVisibleSubCategoryCounts[category.id] = visibleSubCategoryCount; allFirstVisibleSubCategoryIndexes[category.id] = firstVisibleSubCategoryIndex; } } } - ret[categoryType.toString()] = { - type: categoryType.toString(), + ret[categoryType] = { + type: categoryType, allCategories: allCategories, allVisibleCategoryCount: allVisibleCategoryCount, firstVisibleCategoryIndex: firstVisibleCategoryIndex, @@ -146,9 +159,9 @@ export function allTransactionCategoriesWithVisibleCount(allTransactionCategorie return ret; } -export function allVisiblePrimaryTransactionCategoriesByType(allTransactionCategories, categoryType) { +export function allVisiblePrimaryTransactionCategoriesByType(allTransactionCategories: Record, categoryType: number): TransactionCategory[] { const allCategories = allTransactionCategories[categoryType]; - const visibleCategories = []; + const visibleCategories: TransactionCategory[] = []; if (!allCategories) { return visibleCategories; @@ -167,14 +180,14 @@ export function allVisiblePrimaryTransactionCategoriesByType(allTransactionCateg return visibleCategories; } -export function getFinalCategoryIdsByFilteredCategoryIds(allTransactionCategoriesMap, filteredCategoryIds) { +export function getFinalCategoryIdsByFilteredCategoryIds(allTransactionCategoriesMap: Record, filteredCategoryIds: Record): string { let finalCategoryIds = ''; if (!allTransactionCategoriesMap) { return finalCategoryIds; } - for (let categoryId in allTransactionCategoriesMap) { + for (const categoryId in allTransactionCategoriesMap) { if (!Object.prototype.hasOwnProperty.call(allTransactionCategoriesMap, categoryId)) { continue; } @@ -195,7 +208,7 @@ export function getFinalCategoryIdsByFilteredCategoryIds(allTransactionCategorie return finalCategoryIds; } -export function isSubCategoryIdAvailable(categories, categoryId) { +export function isSubCategoryIdAvailable(categories: TransactionCategory[], categoryId: string): boolean { if (!categories || !categories.length) { return false; } @@ -207,8 +220,14 @@ export function isSubCategoryIdAvailable(categories, categoryId) { continue; } - for (let j = 0; j < primaryCategory.subCategories.length; j++) { - const secondaryCategory = primaryCategory.subCategories[j]; + const subCategoryList = primaryCategory.secondaryCategories; + + if (!subCategoryList) { + continue; + } + + for (let j = 0; j < subCategoryList.length; j++) { + const secondaryCategory = subCategoryList[j]; if (secondaryCategory.hidden) { continue; @@ -223,7 +242,7 @@ export function isSubCategoryIdAvailable(categories, categoryId) { return false; } -export function getFirstAvailableCategoryId(categories) { +export function getFirstAvailableCategoryId(categories: TransactionCategory[]): string { if (!categories || !categories.length) { return ''; } @@ -235,8 +254,14 @@ export function getFirstAvailableCategoryId(categories) { continue; } - for (let j = 0; j < primaryCategory.subCategories.length; j++) { - const secondaryCategory = primaryCategory.subCategories[j]; + const subCategoryList = primaryCategory.secondaryCategories; + + if (!subCategoryList) { + continue; + } + + for (let j = 0; j < subCategoryList.length; j++) { + const secondaryCategory = subCategoryList[j]; if (secondaryCategory.hidden) { continue; @@ -249,7 +274,7 @@ export function getFirstAvailableCategoryId(categories) { return ''; } -export function getFirstAvailableSubCategoryId(categories, categoryId) { +export function getFirstAvailableSubCategoryId(categories: TransactionCategory[], categoryId: string): string { if (!categories || !categories.length) { return ''; } @@ -261,8 +286,14 @@ export function getFirstAvailableSubCategoryId(categories, categoryId) { continue; } - for (let j = 0; j < primaryCategory.subCategories.length; j++) { - const secondaryCategory = primaryCategory.subCategories[j]; + const subCategoryList = primaryCategory.secondaryCategories; + + if (!subCategoryList) { + return ''; + } + + for (let j = 0; j < subCategoryList.length; j++) { + const secondaryCategory = subCategoryList[j]; if (secondaryCategory.hidden) { continue; @@ -277,7 +308,7 @@ export function getFirstAvailableSubCategoryId(categories, categoryId) { return ''; } -export function isNoAvailableCategory(categories, showHidden) { +export function isNoAvailableCategory(categories: TransactionCategory[], showHidden: boolean): boolean { for (let i = 0; i < categories.length; i++) { if (showHidden || !categories[i].hidden) { return false; @@ -287,7 +318,7 @@ export function isNoAvailableCategory(categories, showHidden) { return true; } -export function getAvailableCategoryCount(categories, showHidden) { +export function getAvailableCategoryCount(categories: TransactionCategory[], showHidden: boolean): number { let count = 0; for (let i = 0; i < categories.length; i++) { @@ -299,7 +330,7 @@ export function getAvailableCategoryCount(categories, showHidden) { return count; } -export function getFirstShowingId(categories, showHidden) { +export function getFirstShowingId(categories: TransactionCategory[], showHidden: boolean): string | null { for (let i = 0; i < categories.length; i++) { if (showHidden || !categories[i].hidden) { return categories[i].id; @@ -309,7 +340,7 @@ export function getFirstShowingId(categories, showHidden) { return null; } -export function getLastShowingId(categories, showHidden) { +export function getLastShowingId(categories: TransactionCategory[], showHidden: boolean): string | null { for (let i = categories.length - 1; i >= 0; i--) { if (showHidden || !categories[i].hidden) { return categories[i].id; @@ -319,8 +350,8 @@ export function getLastShowingId(categories, showHidden) { return null; } -export function hasAnyAvailableCategory(allTransactionCategories, showHidden) { - for (let type in allTransactionCategories) { +export function hasAnyAvailableCategory(allTransactionCategories: Record, showHidden: boolean): boolean { + for (const type in allTransactionCategories) { if (!Object.prototype.hasOwnProperty.call(allTransactionCategories, type)) { continue; } @@ -341,10 +372,10 @@ export function hasAnyAvailableCategory(allTransactionCategories, showHidden) { return false; } -export function hasAvailableCategory(allTransactionCategories, showHidden) { - const result = {}; +export function hasAvailableCategory(allTransactionCategories: Record, showHidden: boolean): Record { + const result: Record = {}; - for (let type in allTransactionCategories) { + for (const type in allTransactionCategories) { if (!Object.prototype.hasOwnProperty.call(allTransactionCategories, type)) { continue; } @@ -361,19 +392,19 @@ export function hasAvailableCategory(allTransactionCategories, showHidden) { return result; } -export function selectSubCategories(filterCategoryIds, category, value) { - if (!category || !category.subCategories || !category.subCategories.length) { +export function selectSubCategories(filterCategoryIds: Record, category: TransactionCategory, value: boolean): void { + if (!category || !category.secondaryCategories || !category.secondaryCategories.length) { return; } - for (let i = 0; i < category.subCategories.length; i++) { - const subCategory = category.subCategories[i]; + for (let i = 0; i < category.secondaryCategories.length; i++) { + const subCategory = category.secondaryCategories[i]; filterCategoryIds[subCategory.id] = value; } } -export function selectAll(filterCategoryIds, allTransactionCategoriesMap) { - for (let categoryId in filterCategoryIds) { +export function selectAll(filterCategoryIds: Record, allTransactionCategoriesMap: Record): void { + for (const categoryId in filterCategoryIds) { if (!Object.prototype.hasOwnProperty.call(filterCategoryIds, categoryId)) { continue; } @@ -386,8 +417,8 @@ export function selectAll(filterCategoryIds, allTransactionCategoriesMap) { } } -export function selectNone(filterCategoryIds, allTransactionCategoriesMap) { - for (let categoryId in filterCategoryIds) { +export function selectNone(filterCategoryIds: Record, allTransactionCategoriesMap: Record): void { + for (const categoryId in filterCategoryIds) { if (!Object.prototype.hasOwnProperty.call(filterCategoryIds, categoryId)) { continue; } @@ -400,8 +431,8 @@ export function selectNone(filterCategoryIds, allTransactionCategoriesMap) { } } -export function selectInvert(filterCategoryIds, allTransactionCategoriesMap) { - for (let categoryId in filterCategoryIds) { +export function selectInvert(filterCategoryIds: Record, allTransactionCategoriesMap: Record): void { + for (const categoryId in filterCategoryIds) { if (!Object.prototype.hasOwnProperty.call(filterCategoryIds, categoryId)) { continue; } @@ -414,13 +445,13 @@ export function selectInvert(filterCategoryIds, allTransactionCategoriesMap) { } } -export function isCategoryOrSubCategoriesAllChecked(category, filterCategoryIds) { - if (!category.subCategories) { +export function isCategoryOrSubCategoriesAllChecked(category: TransactionCategory, filterCategoryIds: Record): boolean { + if (!category.secondaryCategories || category.secondaryCategories.length < 1) { return !filterCategoryIds[category.id]; } - for (let i = 0; i < category.subCategories.length; i++) { - const subCategory = category.subCategories[i]; + for (let i = 0; i < category.secondaryCategories.length; i++) { + const subCategory = category.secondaryCategories[i]; if (filterCategoryIds[subCategory.id]) { return false; } @@ -429,9 +460,13 @@ export function isCategoryOrSubCategoriesAllChecked(category, filterCategoryIds) return true; } -export function isSubCategoriesAllChecked(category, filterCategoryIds) { - for (let i = 0; i < category.subCategories.length; i++) { - const subCategory = category.subCategories[i]; +export function isSubCategoriesAllChecked(category: TransactionCategory, filterCategoryIds: Record): boolean { + if (!category.secondaryCategories || category.secondaryCategories.length < 1) { + return false; + } + + for (let i = 0; i < category.secondaryCategories.length; i++) { + const subCategory = category.secondaryCategories[i]; if (filterCategoryIds[subCategory.id]) { return false; } @@ -440,15 +475,19 @@ export function isSubCategoriesAllChecked(category, filterCategoryIds) { return true; } -export function isSubCategoriesHasButNotAllChecked(category, filterCategoryIds) { +export function isSubCategoriesHasButNotAllChecked(category: TransactionCategory, filterCategoryIds: Record): boolean { let checkedCount = 0; - for (let i = 0; i < category.subCategories.length; i++) { - const subCategory = category.subCategories[i]; + if (!category.secondaryCategories || category.secondaryCategories.length < 1) { + return false; + } + + for (let i = 0; i < category.secondaryCategories.length; i++) { + const subCategory = category.secondaryCategories[i]; if (!filterCategoryIds[subCategory.id]) { checkedCount++; } } - return checkedCount > 0 && checkedCount < category.subCategories.length; + return checkedCount > 0 && checkedCount < category.secondaryCategories.length; } diff --git a/src/lib/services.ts b/src/lib/services.ts index 50da8f09..775ef165 100644 --- a/src/lib/services.ts +++ b/src/lib/services.ts @@ -445,8 +445,8 @@ export default { removeUnusedTransactionPicture: (req: TransactionPictureUnusedDeleteRequest): ApiResponsePromise => { return axios.post>('v1/transaction/pictures/remove_unused.json', req); }, - getAllTransactionCategories: (): ApiResponsePromise => { - return axios.get>('v1/transaction/categories/list.json'); + getAllTransactionCategories: (): ApiResponsePromise> => { + return axios.get>>('v1/transaction/categories/list.json'); }, getTransactionCategory: (req: { id: string }): ApiResponsePromise => { return axios.get>('v1/transaction/categories/get.json?id=' + req.id); diff --git a/src/lib/transaction.js b/src/lib/transaction.js index ab699521..448576d1 100644 --- a/src/lib/transaction.js +++ b/src/lib/transaction.js @@ -12,7 +12,7 @@ import { isSubCategoryIdAvailable, getFirstAvailableCategoryId, getFirstAvailableSubCategoryId -} from './category.js'; +} from './category.ts'; function getDisplayAmount(amount, currency, hideAmount, formatAmountWithCurrencyFunc) { if (hideAmount) { diff --git a/src/models/transaction_category.ts b/src/models/transaction_category.ts index c89f0ed6..111d8ce6 100644 --- a/src/models/transaction_category.ts +++ b/src/models/transaction_category.ts @@ -1,3 +1,125 @@ +import { CategoryType } from '@/core/category.ts'; +import { DEFAULT_CATEGORY_ICON_ID } from '@/consts/icon.ts'; +import { DEFAULT_CATEGORY_COLOR } from '@/consts/color.ts'; + +export class TransactionCategory implements TransactionCategoryInfoResponse { + public id: string; + public name: string; + public parentId: string; + public type: number; + public icon: string; + public color: string; + public comment: string; + public displayOrder: number; + public visible: boolean; + public secondaryCategories?: TransactionCategory[]; + + private constructor(id: string, name: string, parentId: string, type: number, icon: string, color: string, comment: string, displayOrder: number, visible: boolean, secondaryCategories?: TransactionCategory[]) { + this.id = id; + this.name = name; + this.parentId = parentId; + this.type = type; + this.icon = icon; + this.color = color; + this.comment = comment; + this.displayOrder = displayOrder; + this.visible = visible; + + if (secondaryCategories) { + this.secondaryCategories = secondaryCategories; + } else if (!secondaryCategories && (!parentId || parentId === '0')) { + this.secondaryCategories = []; + } + } + + get hidden(): boolean { + return !this.visible; + } + + get subCategories(): TransactionCategoryInfoResponse[] | undefined { + if (typeof this.secondaryCategories === 'undefined') { + return undefined; + } + + const ret: TransactionCategoryInfoResponse[] = []; + + if (this.secondaryCategories) { + for (const subCategory of this.secondaryCategories) { + ret.push(subCategory); + } + } + + return ret; + } + + public toCreateRequest(clientSessionId: string): TransactionCategoryCreateRequest { + return { + name: this.name, + type: this.type, + parentId: this.parentId, + icon: this.icon, + color: this.color, + comment: this.comment, + clientSessionId: clientSessionId + }; + } + + public toModifyRequest(): TransactionCategoryModifyRequest { + return { + id: this.id, + name: this.name, + parentId: this.parentId, + icon: this.icon, + color: this.color, + comment: this.comment, + hidden: !this.visible + }; + } + + public static of(categoryResponse: TransactionCategoryInfoResponse): TransactionCategory { + return new TransactionCategory( + categoryResponse.id, + categoryResponse.name, + categoryResponse.parentId, + categoryResponse.type, + categoryResponse.icon, + categoryResponse.color, + categoryResponse.comment, + categoryResponse.displayOrder, + !categoryResponse.hidden, + categoryResponse.subCategories ? TransactionCategory.ofMany(categoryResponse.subCategories) : undefined + ); + } + + public static ofMany(categoryResponses: TransactionCategoryInfoResponse[]): TransactionCategory[] { + const tags: TransactionCategory[] = []; + + for (const tagResponse of categoryResponses) { + tags.push(TransactionCategory.of(tagResponse)); + } + + return tags; + } + + public static ofMap(categoriesByType: Record): Record { + const ret: Record = {}; + + for (const categoryType in categoriesByType) { + if (!Object.prototype.hasOwnProperty.call(categoriesByType, categoryType)) { + continue; + } + + ret[categoryType] = TransactionCategory.ofMany(categoriesByType[categoryType]); + } + + return ret; + } + + public static createNewCategory(type?: CategoryType, parentId?: string): TransactionCategory { + return new TransactionCategory('', '', parentId || '0', type || CategoryType.Income, DEFAULT_CATEGORY_ICON_ID, DEFAULT_CATEGORY_COLOR, '', 0, true); + } +} + export interface TransactionCategoryCreateRequest { readonly name: string; readonly type: number; @@ -61,3 +183,13 @@ export interface TransactionCategoryInfoResponse { readonly hidden: boolean; readonly subCategories?: TransactionCategoryInfoResponse[]; } + +export interface TransactionCategoriesWithVisibleCount { + readonly type: number; + readonly allCategories: TransactionCategory[]; + readonly allVisibleCategoryCount: number; + readonly firstVisibleCategoryIndex: number; + readonly allSubCategories: Record; + readonly allVisibleSubCategoryCounts: Record; + readonly allFirstVisibleSubCategoryIndexes: Record; +} diff --git a/src/stores/index.js b/src/stores/index.js index 3063b312..8e0186b4 100644 --- a/src/stores/index.js +++ b/src/stores/index.js @@ -3,7 +3,7 @@ import { defineStore } from 'pinia'; import { useSettingsStore } from './setting.ts'; import { useUserStore } from './user.ts'; import { useAccountsStore } from './account.js'; -import { useTransactionCategoriesStore } from './transactionCategory.js'; +import { useTransactionCategoriesStore } from './transactionCategory.ts'; import { useTransactionTagsStore } from './transactionTag.ts'; import { useTransactionTemplatesStore } from './transactionTemplate.js'; import { useTransactionsStore } from './transaction.js'; diff --git a/src/stores/statistics.js b/src/stores/statistics.js index dbf526b8..aaf3f591 100644 --- a/src/stores/statistics.js +++ b/src/stores/statistics.js @@ -3,7 +3,7 @@ import { defineStore } from 'pinia'; import { useSettingsStore } from './setting.ts'; import { useUserStore } from './user.ts'; import { useAccountsStore } from './account.js'; -import { useTransactionCategoriesStore } from './transactionCategory.js'; +import { useTransactionCategoriesStore } from './transactionCategory.ts'; import { useExchangeRatesStore } from './exchangeRates.ts'; import { DateRangeScene, DateRange } from '@/core/datetime'; @@ -43,7 +43,7 @@ import { } from '@/lib/account.js'; import { getFinalCategoryIdsByFilteredCategoryIds -} from '@/lib/category.js'; +} from '@/lib/category.ts'; import { sortStatisticsItems } from '@/lib/statistics.ts'; diff --git a/src/stores/transaction.js b/src/stores/transaction.js index 8ff9f5d2..97a7ce84 100644 --- a/src/stores/transaction.js +++ b/src/stores/transaction.js @@ -3,7 +3,7 @@ import { defineStore } from 'pinia'; import { useSettingsStore } from './setting.ts'; import { useUserStore } from './user.ts'; import { useAccountsStore } from './account.js'; -import { useTransactionCategoriesStore } from './transactionCategory.js'; +import { useTransactionCategoriesStore } from './transactionCategory.ts'; import { useOverviewStore } from './overview.ts'; import { useStatisticsStore } from './statistics.js'; import { useExchangeRatesStore } from './exchangeRates.ts'; @@ -39,7 +39,7 @@ import { } from '@/lib/datetime.ts'; import { getAmountWithDecimalNumberCount } from '@/lib/numeral.ts'; import { getCurrencyFraction } from '@/lib/currency.ts'; -import { getFirstAvailableCategoryId } from '@/lib/category.js'; +import { getFirstAvailableCategoryId } from '@/lib/category.ts'; const emptyTransactionResult = { items: [], diff --git a/src/stores/transactionCategory.js b/src/stores/transactionCategory.js deleted file mode 100644 index f5836372..00000000 --- a/src/stores/transactionCategory.js +++ /dev/null @@ -1,533 +0,0 @@ -import { defineStore } from 'pinia'; - -import { CategoryType } from '@/core/category.ts'; -import { DEFAULT_CATEGORY_ICON_ID } from '@/consts/icon.ts'; -import { DEFAULT_CATEGORY_COLOR } from '@/consts/color.ts'; -import { isEquals } from '@/lib/common.ts'; -import services from '@/lib/services.ts'; -import logger from '@/lib/logger.ts'; - -function loadTransactionCategoryList(state, allCategories) { - state.allTransactionCategories = allCategories; - state.allTransactionCategoriesMap = {}; - - for (let categoryType in allCategories) { - if (!Object.prototype.hasOwnProperty.call(allCategories, categoryType)) { - continue; - } - - const categories = allCategories[categoryType]; - - for (let i = 0; i < categories.length; i++) { - const category = categories[i]; - state.allTransactionCategoriesMap[category.id] = category; - - for (let j = 0; j < category.subCategories.length; j++) { - const subCategory = category.subCategories[j]; - state.allTransactionCategoriesMap[subCategory.id] = subCategory; - } - } - } -} - -function addCategoryToTransactionCategoryList(state, category) { - let categoryList = null; - - if (!category.parentId || category.parentId === '0') { - categoryList = state.allTransactionCategories[category.type]; - } else if (state.allTransactionCategoriesMap[category.parentId]) { - categoryList = state.allTransactionCategoriesMap[category.parentId].subCategories; - } - - if (categoryList) { - categoryList.push(category); - } - - state.allTransactionCategoriesMap[category.id] = category; -} - -function updateCategoryInTransactionCategoryList(state, category, oldCategory) { - if (oldCategory && category.parentId !== oldCategory.parentId) { - return false; - } - - let categoryList = null; - - if (!category.parentId || category.parentId === '0') { - categoryList = state.allTransactionCategories[category.type]; - } else if (state.allTransactionCategoriesMap[category.parentId]) { - categoryList = state.allTransactionCategoriesMap[category.parentId].subCategories; - } - - if (categoryList) { - for (let i = 0; i < categoryList.length; i++) { - if (categoryList[i].id === category.id) { - if (!category.parentId || category.parentId === '0') { - category.subCategories = categoryList[i].subCategories; - } - - categoryList.splice(i, 1, category); - break; - } - } - } - - state.allTransactionCategoriesMap[category.id] = category; - return true; -} - -function updateCategoryDisplayOrderInCategoryList(state, { category, from, to }) { - let categoryList = null; - - if (!category.parentId || category.parentId === '0') { - categoryList = state.allTransactionCategories[category.type]; - } else if (state.allTransactionCategoriesMap[category.parentId]) { - categoryList = state.allTransactionCategoriesMap[category.parentId].subCategories; - } - - if (categoryList) { - categoryList.splice(to, 0, categoryList.splice(from, 1)[0]); - } -} - -function updateCategoryVisibilityInTransactionCategoryList(state, { category, hidden }) { - if (state.allTransactionCategoriesMap[category.id]) { - state.allTransactionCategoriesMap[category.id].hidden = hidden; - } -} - -function removeCategoryFromTransactionCategoryList(state, category) { - let categoryList = null; - - if (!category.parentId || category.parentId === '0') { - categoryList = state.allTransactionCategories[category.type]; - } else if (state.allTransactionCategoriesMap[category.parentId]) { - categoryList = state.allTransactionCategoriesMap[category.parentId].subCategories; - } - - if (categoryList) { - for (let i = 0; i < categoryList.length; i++) { - if (categoryList[i].id === category.id) { - categoryList.splice(i, 1); - break; - } - } - } - - if (state.allTransactionCategoriesMap[category.id] && state.allTransactionCategoriesMap[category.id].subCategories) { - const subCategories = state.allTransactionCategoriesMap[category.id].subCategories; - - for (let i = 0; i < subCategories.length; i++) { - const subCategory = subCategories[i]; - if (state.allTransactionCategoriesMap[subCategory.id]) { - delete state.allTransactionCategoriesMap[subCategory.id]; - } - } - } - - if (state.allTransactionCategoriesMap[category.id]) { - delete state.allTransactionCategoriesMap[category.id]; - } -} - -export const useTransactionCategoriesStore = defineStore('transactionCategories', { - state: () => ({ - allTransactionCategories: {}, - allTransactionCategoriesMap: {}, - transactionCategoryListStateInvalid: true, - }), - actions: { - generateNewTransactionCategoryModel(type, parentId) { - return { - type: type || CategoryType.Income, - name: '', - parentId: parentId || '0', - icon: DEFAULT_CATEGORY_ICON_ID, - color: DEFAULT_CATEGORY_COLOR, - comment: '', - visible: true - }; - }, - updateTransactionCategoryListInvalidState(invalidState) { - this.transactionCategoryListStateInvalid = invalidState; - }, - resetTransactionCategories() { - this.allTransactionCategories = {}; - this.allTransactionCategoriesMap = {}; - this.transactionCategoryListStateInvalid = true; - }, - loadAllCategories({ force }) { - const self = this; - - if (!force && !self.transactionCategoryListStateInvalid) { - return new Promise((resolve) => { - resolve(self.allTransactionCategories); - }); - } - - return new Promise((resolve, reject) => { - services.getAllTransactionCategories().then(response => { - const data = response.data; - - if (!data || !data.success || !data.result) { - reject({ message: 'Unable to retrieve category list' }); - return; - } - - if (!data.result[CategoryType.Income]) { - data.result[CategoryType.Income] = []; - } - - if (!data.result[CategoryType.Expense]) { - data.result[CategoryType.Expense] = []; - } - - if (!data.result[CategoryType.Transfer]) { - data.result[CategoryType.Transfer] = []; - } - - for (let categoryType in data.result) { - if (!Object.prototype.hasOwnProperty.call(data.result, categoryType)) { - continue; - } - - const categories = data.result[categoryType]; - - for (let i = 0; i < categories.length; i++) { - const category = categories[i]; - - if (!category.subCategories) { - category.subCategories = []; - } - } - } - - if (self.transactionCategoryListStateInvalid) { - self.updateTransactionCategoryListInvalidState(false); - } - - if (force && data.result && isEquals(self.allTransactionCategories, data.result)) { - reject({ message: 'Category list is up to date' }); - return; - } - - loadTransactionCategoryList(self, data.result); - - resolve(data.result); - }).catch(error => { - if (force) { - logger.error('failed to force load category list', error); - } else { - logger.error('failed to load category list', 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 category list' }); - } else { - reject(error); - } - }); - }); - }, - getCategory({ categoryId }) { - return new Promise((resolve, reject) => { - services.getTransactionCategory({ - id: categoryId - }).then(response => { - const data = response.data; - - if (!data || !data.success || !data.result) { - reject({ message: 'Unable to retrieve category' }); - return; - } - - resolve(data.result); - }).catch(error => { - logger.error('failed to load category 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 category' }); - } else { - reject(error); - } - }); - }); - }, - saveCategory({ category, isEdit, clientSessionId }) { - const self = this; - - const submitCategory = { - type: category.type, - name: category.name, - parentId: category.parentId, - icon: category.icon, - color: category.color, - comment: category.comment - }; - - if (clientSessionId) { - submitCategory.clientSessionId = clientSessionId; - } - - if (isEdit) { - submitCategory.id = category.id; - submitCategory.hidden = !category.visible; - } - - return new Promise((resolve, reject) => { - let promise = null; - - if (!submitCategory.id) { - promise = services.addTransactionCategory(submitCategory); - } else { - promise = services.modifyTransactionCategory(submitCategory); - } - - promise.then(response => { - const data = response.data; - - if (!data || !data.success || !data.result) { - if (!submitCategory.id) { - reject({ message: 'Unable to add category' }); - } else { - reject({ message: 'Unable to save category' }); - } - return; - } - - if (!data.result.subCategories) { - data.result.subCategories = []; - } - - if (!submitCategory.id) { - addCategoryToTransactionCategoryList(self, data.result); - } else { - const result = updateCategoryInTransactionCategoryList(self, data.result, self.allTransactionCategoriesMap[submitCategory.id]); - - if (!result && !self.transactionCategoryListStateInvalid) { - self.updateTransactionCategoryListInvalidState(true); - } - } - - resolve(data.result); - }).catch(error => { - logger.error('failed to save category', error); - - if (error.response && error.response.data && error.response.data.errorMessage) { - reject({ error: error.response.data }); - } else if (!error.processed) { - if (!submitCategory.id) { - reject({ message: 'Unable to add category' }); - } else { - reject({ message: 'Unable to save category' }); - } - } else { - reject(error); - } - }); - }); - }, - addCategories({ categories }) { - const self = this; - - return new Promise((resolve, reject) => { - services.addTransactionCategoryBatch({ - categories: categories - }).then(response => { - const data = response.data; - - if (!data || !data.success || !data.result) { - reject({ message: 'Unable to add preset categories' }); - return; - } - - if (!self.transactionCategoryListStateInvalid) { - self.updateTransactionCategoryListInvalidState(true); - } - - resolve(data.result); - }).catch(error => { - logger.error('failed to add preset categories', error); - - if (error.response && error.response.data && error.response.data.errorMessage) { - reject({ error: error.response.data }); - } else if (!error.processed) { - reject({ message: 'Unable to add preset categories' }); - } else { - reject(error); - } - }); - }); - }, - changeCategoryDisplayOrder({ categoryId, from, to }) { - const self = this; - const category = self.allTransactionCategoriesMap[categoryId]; - - return new Promise((resolve, reject) => { - if (!category) { - reject({ message: 'Unable to move category' }); - return; - } - - if (!category.parentId || category.parentId === '0') { - if (!self.allTransactionCategories[category.type] || - !self.allTransactionCategories[category.type][to]) { - reject({ message: 'Unable to move category' }); - return; - } - } else { - if (!self.allTransactionCategoriesMap[category.parentId].subCategories || - !self.allTransactionCategoriesMap[category.parentId].subCategories[to]) { - reject({ message: 'Unable to move category' }); - return; - } - } - - if (!self.transactionCategoryListStateInvalid) { - self.updateTransactionCategoryListInvalidState(true); - } - - updateCategoryDisplayOrderInCategoryList(self, { - category: category, - from: from, - to: to - }); - - resolve(); - }); - }, - updateCategoryDisplayOrders({ type, parentId }) { - const self = this; - const newDisplayOrders = []; - - let categoryList = null; - - if (!parentId || parentId === '0') { - categoryList = self.allTransactionCategories[type]; - } else if (self.allTransactionCategoriesMap[parentId]) { - categoryList = self.allTransactionCategoriesMap[parentId].subCategories; - } - - if (categoryList) { - for (let i = 0; i < categoryList.length; i++) { - newDisplayOrders.push({ - id: categoryList[i].id, - displayOrder: i + 1 - }); - } - } - - return new Promise((resolve, reject) => { - services.moveTransactionCategory({ - newDisplayOrders: newDisplayOrders - }).then(response => { - const data = response.data; - - if (!data || !data.success || !data.result) { - reject({ message: 'Unable to move category' }); - return; - } - - if (self.transactionCategoryListStateInvalid) { - self.updateTransactionCategoryListInvalidState(false); - } - - resolve(data.result); - }).catch(error => { - logger.error('failed to save categories display order', error); - - if (error.response && error.response.data && error.response.data.errorMessage) { - reject({ error: error.response.data }); - } else if (!error.processed) { - reject({ message: 'Unable to move category' }); - } else { - reject(error); - } - }); - }); - }, - hideCategory({ category, hidden }) { - const self = this; - - return new Promise((resolve, reject) => { - services.hideTransactionCategory({ - id: category.id, - hidden: hidden - }).then(response => { - const data = response.data; - - if (!data || !data.success || !data.result) { - if (hidden) { - reject({ message: 'Unable to hide this category' }); - } else { - reject({ message: 'Unable to unhide this category' }); - } - - return; - } - - updateCategoryVisibilityInTransactionCategoryList(self, { - category: category, - hidden: hidden - }); - - resolve(data.result); - }).catch(error => { - logger.error('failed to change category visibility', error); - - if (error.response && error.response.data && error.response.data.errorMessage) { - reject({ error: error.response.data }); - } else if (!error.processed) { - if (hidden) { - reject({ message: 'Unable to hide this category' }); - } else { - reject({ message: 'Unable to unhide this category' }); - } - } else { - reject(error); - } - }); - }); - }, - deleteCategory({ category, beforeResolve }) { - const self = this; - - return new Promise((resolve, reject) => { - services.deleteTransactionCategory({ - id: category.id - }).then(response => { - const data = response.data; - - if (!data || !data.success || !data.result) { - reject({ message: 'Unable to delete this category' }); - return; - } - - if (beforeResolve) { - beforeResolve(() => { - removeCategoryFromTransactionCategoryList(self, category); - }); - } else { - removeCategoryFromTransactionCategoryList(self, category); - } - - resolve(data.result); - }).catch(error => { - logger.error('failed to delete category', 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 category' }); - } else { - reject(error); - } - }); - }); - } - } -}); diff --git a/src/stores/transactionCategory.ts b/src/stores/transactionCategory.ts new file mode 100644 index 00000000..fd18f52f --- /dev/null +++ b/src/stores/transactionCategory.ts @@ -0,0 +1,518 @@ +import { ref } from 'vue'; +import { defineStore } from 'pinia'; + +import type { BeforeResolveFunction } from '@/core/base.ts'; + +import { CategoryType } from '@/core/category.ts'; + +import { + type TransactionCategoryInfoResponse, + type TransactionCategoryCreateBatchRequest, + type TransactionCategoryNewDisplayOrderRequest, + TransactionCategory, +} from '@/models/transaction_category.ts'; + +import { isEquals } from '@/lib/common.ts'; +import services, { type ApiResponsePromise } from '@/lib/services.ts'; +import logger from '@/lib/logger.ts'; + +export const useTransactionCategoriesStore = defineStore('transactionCategories', () =>{ + const allTransactionCategories = ref>({}); + const allTransactionCategoriesMap = ref>({}); + const transactionCategoryListStateInvalid = ref(true); + + function loadTransactionCategoryList(allCategories: Record): void { + allTransactionCategories.value = allCategories; + allTransactionCategoriesMap.value = {}; + + for (const categoryType in allCategories) { + if (!Object.prototype.hasOwnProperty.call(allCategories, categoryType)) { + continue; + } + + const categories = allCategories[categoryType]; + + for (let i = 0; i < categories.length; i++) { + const category = categories[i]; + allTransactionCategoriesMap.value[category.id] = category; + + if (!category.secondaryCategories) { + continue; + } + + for (let j = 0; j < category.secondaryCategories.length; j++) { + const subCategory = category.secondaryCategories[j]; + allTransactionCategoriesMap.value[subCategory.id] = subCategory; + } + } + } + } + + function addCategoryToTransactionCategoryList(category: TransactionCategory): void { + let categoryList: TransactionCategory[] | undefined = undefined; + + if (!category.parentId || category.parentId === '0') { + categoryList = allTransactionCategories.value[category.type]; + } else if (allTransactionCategoriesMap.value[category.parentId]) { + categoryList = allTransactionCategoriesMap.value[category.parentId].secondaryCategories; + } + + if (categoryList) { + categoryList.push(category); + } + + allTransactionCategoriesMap.value[category.id] = category; + } + + function updateCategoryInTransactionCategoryList(category: TransactionCategory, oldCategory: TransactionCategory): boolean { + if (oldCategory && category.parentId !== oldCategory.parentId) { + return false; + } + + let categoryList: TransactionCategory[] | undefined = undefined; + + if (!category.parentId || category.parentId === '0') { + categoryList = allTransactionCategories.value[category.type]; + } else if (allTransactionCategoriesMap.value[category.parentId]) { + categoryList = allTransactionCategoriesMap.value[category.parentId].secondaryCategories; + } + + if (categoryList) { + for (let i = 0; i < categoryList.length; i++) { + if (categoryList[i].id === category.id) { + if (!category.parentId || category.parentId === '0') { + category.secondaryCategories = categoryList[i].secondaryCategories; + } + + categoryList.splice(i, 1, category); + break; + } + } + } + + allTransactionCategoriesMap.value[category.id] = category; + return true; + } + + function updateCategoryDisplayOrderInCategoryList(params: { category: TransactionCategory, from: number, to: number }): void { + let categoryList: TransactionCategory[] | undefined = undefined; + + if (!params.category.parentId || params.category.parentId === '0') { + categoryList = allTransactionCategories.value[params.category.type]; + } else if (allTransactionCategoriesMap.value[params.category.parentId]) { + categoryList = allTransactionCategoriesMap.value[params.category.parentId].secondaryCategories; + } + + if (categoryList) { + categoryList.splice(params.to, 0, categoryList.splice(params.from, 1)[0]); + } + } + + function updateCategoryVisibilityInTransactionCategoryList(params: { category: TransactionCategory, hidden: boolean }): void { + if (allTransactionCategoriesMap.value[params.category.id]) { + allTransactionCategoriesMap.value[params.category.id].visible = !params.hidden; + } + } + + function removeCategoryFromTransactionCategoryList(category: TransactionCategory): void { + let categoryList: TransactionCategory[] | undefined = undefined; + + if (!category.parentId || category.parentId === '0') { + categoryList = allTransactionCategories.value[category.type]; + } else if (allTransactionCategoriesMap.value[category.parentId]) { + categoryList = allTransactionCategoriesMap.value[category.parentId].secondaryCategories; + } + + if (categoryList) { + for (let i = 0; i < categoryList.length; i++) { + if (categoryList[i].id === category.id) { + categoryList.splice(i, 1); + break; + } + } + } + + if (allTransactionCategoriesMap.value[category.id] && allTransactionCategoriesMap.value[category.id].secondaryCategories) { + const subCategoryList = allTransactionCategoriesMap.value[category.id].secondaryCategories; + + if (subCategoryList) { + for (let i = 0; i < subCategoryList.length; i++) { + const subCategory = subCategoryList[i]; + if (allTransactionCategoriesMap.value[subCategory.id]) { + delete allTransactionCategoriesMap.value[subCategory.id]; + } + } + } + } + + if (allTransactionCategoriesMap.value[category.id]) { + delete allTransactionCategoriesMap.value[category.id]; + } + } + + function updateTransactionCategoryListInvalidState(invalidState: boolean): void { + transactionCategoryListStateInvalid.value = invalidState; + } + + function resetTransactionCategories(): void { + allTransactionCategories.value = {}; + allTransactionCategoriesMap.value = {}; + transactionCategoryListStateInvalid.value = true; + } + + function loadAllCategories(params: { force?: boolean }): Promise> { + if (!params.force && !transactionCategoryListStateInvalid.value) { + return new Promise((resolve) => { + resolve(allTransactionCategories.value); + }); + } + + return new Promise((resolve, reject) => { + services.getAllTransactionCategories().then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to retrieve category list' }); + return; + } + + if (!data.result[CategoryType.Income]) { + data.result[CategoryType.Income] = []; + } + + if (!data.result[CategoryType.Expense]) { + data.result[CategoryType.Expense] = []; + } + + if (!data.result[CategoryType.Transfer]) { + data.result[CategoryType.Transfer] = []; + } + + if (transactionCategoryListStateInvalid.value) { + updateTransactionCategoryListInvalidState(false); + } + + const transactionCategories = TransactionCategory.ofMap(data.result); + + if (params.force && data.result && isEquals(allTransactionCategories.value, transactionCategories)) { + reject({ message: 'Category list is up to date' }); + return; + } + + loadTransactionCategoryList(transactionCategories); + + resolve(transactionCategories); + }).catch(error => { + if (params.force) { + logger.error('failed to force load category list', error); + } else { + logger.error('failed to load category list', 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 category list' }); + } else { + reject(error); + } + }); + }); + } + + function getCategory(params: { categoryId: string }): Promise { + return new Promise((resolve, reject) => { + services.getTransactionCategory({ + id: params.categoryId + }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to retrieve category' }); + return; + } + + const transactionCategory = TransactionCategory.of(data.result); + + resolve(transactionCategory); + }).catch(error => { + logger.error('failed to load category 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 category' }); + } else { + reject(error); + } + }); + }); + } + + function saveCategory(params: { category: TransactionCategory, isEdit: boolean, clientSessionId: string }): Promise { + return new Promise((resolve, reject) => { + let promise: ApiResponsePromise; + + if (!params.isEdit) { + promise = services.addTransactionCategory(params.category.toCreateRequest(params.clientSessionId)); + } else { + promise = services.modifyTransactionCategory(params.category.toModifyRequest()); + } + + promise.then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + if (!params.isEdit) { + reject({ message: 'Unable to add category' }); + } else { + reject({ message: 'Unable to save category' }); + } + return; + } + + const transactionCategory = TransactionCategory.of(data.result); + + if (!params.isEdit) { + addCategoryToTransactionCategoryList(transactionCategory); + } else { + const result = updateCategoryInTransactionCategoryList(transactionCategory, allTransactionCategoriesMap.value[params.category.id]); + + if (!result && !transactionCategoryListStateInvalid.value) { + updateTransactionCategoryListInvalidState(true); + } + } + + resolve(transactionCategory); + }).catch(error => { + logger.error('failed to save category', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + if (!params.isEdit) { + reject({ message: 'Unable to add category' }); + } else { + reject({ message: 'Unable to save category' }); + } + } else { + reject(error); + } + }); + }); + } + + function addCategories(req: TransactionCategoryCreateBatchRequest): Promise> { + return new Promise((resolve, reject) => { + services.addTransactionCategoryBatch(req).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to add preset categories' }); + return; + } + + if (!transactionCategoryListStateInvalid.value) { + updateTransactionCategoryListInvalidState(true); + } + + const transactionCategories = TransactionCategory.ofMap(data.result); + + resolve(transactionCategories); + }).catch(error => { + logger.error('failed to add preset categories', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to add preset categories' }); + } else { + reject(error); + } + }); + }); + } + + function changeCategoryDisplayOrder(params: { categoryId: string, from: number, to: number }): Promise { + const category = allTransactionCategoriesMap.value[params.categoryId]; + + return new Promise((resolve, reject) => { + if (!category) { + reject({ message: 'Unable to move category' }); + return; + } + + if (!category.parentId || category.parentId === '0') { + if (!allTransactionCategories.value[category.type] || + !allTransactionCategories.value[category.type][params.to]) { + reject({ message: 'Unable to move category' }); + return; + } + } else { + const subCategoryList = allTransactionCategoriesMap.value[category.parentId].secondaryCategories; + + if (!subCategoryList || !subCategoryList[params.to]) { + reject({ message: 'Unable to move category' }); + return; + } + } + + if (!transactionCategoryListStateInvalid.value) { + updateTransactionCategoryListInvalidState(true); + } + + updateCategoryDisplayOrderInCategoryList({ + category: category, + from: params.from, + to: params.to + }); + + resolve(); + }); + } + + function updateCategoryDisplayOrders(params: { type: CategoryType, parentId: string }): Promise { + const newDisplayOrders: TransactionCategoryNewDisplayOrderRequest[] = []; + + let categoryList: TransactionCategory[] | undefined = undefined; + + if (!params.parentId || params.parentId === '0') { + categoryList = allTransactionCategories.value[params.type]; + } else if (allTransactionCategoriesMap.value[params.parentId]) { + categoryList = allTransactionCategoriesMap.value[params.parentId].secondaryCategories; + } + + if (categoryList) { + for (let i = 0; i < categoryList.length; i++) { + newDisplayOrders.push({ + id: categoryList[i].id, + displayOrder: i + 1 + }); + } + } + + return new Promise((resolve, reject) => { + services.moveTransactionCategory({ + newDisplayOrders: newDisplayOrders + }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to move category' }); + return; + } + + if (transactionCategoryListStateInvalid.value) { + updateTransactionCategoryListInvalidState(false); + } + + resolve(data.result); + }).catch(error => { + logger.error('failed to save categories display order', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to move category' }); + } else { + reject(error); + } + }); + }); + } + + function hideCategory(params: { category: TransactionCategory, hidden: boolean }): Promise { + return new Promise((resolve, reject) => { + services.hideTransactionCategory({ + id: params.category.id, + hidden: params.hidden + }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + if (params.hidden) { + reject({ message: 'Unable to hide this category' }); + } else { + reject({ message: 'Unable to unhide this category' }); + } + + return; + } + + updateCategoryVisibilityInTransactionCategoryList({ + category: params.category, + hidden: params.hidden + }); + + resolve(data.result); + }).catch(error => { + logger.error('failed to change category visibility', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + if (params.hidden) { + reject({ message: 'Unable to hide this category' }); + } else { + reject({ message: 'Unable to unhide this category' }); + } + } else { + reject(error); + } + }); + }); + } + + function deleteCategory(params: { category: TransactionCategory, beforeResolve: BeforeResolveFunction }): Promise { + return new Promise((resolve, reject) => { + services.deleteTransactionCategory({ + id: params.category.id + }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to delete this category' }); + return; + } + + if (params.beforeResolve) { + params.beforeResolve(() => { + removeCategoryFromTransactionCategoryList(params.category); + }); + } else { + removeCategoryFromTransactionCategoryList(params.category); + } + + resolve(data.result); + }).catch(error => { + logger.error('failed to delete category', 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 category' }); + } else { + reject(error); + } + }); + }); + } + + return { + // states + allTransactionCategories, + allTransactionCategoriesMap, + transactionCategoryListStateInvalid, + // functions + updateTransactionCategoryListInvalidState, + resetTransactionCategories, + loadAllCategories, + getCategory, + saveCategory, + addCategories, + changeCategoryDisplayOrder, + updateCategoryDisplayOrders, + hideCategory, + deleteCategory + }; +}); diff --git a/src/views/desktop/SignupPage.vue b/src/views/desktop/SignupPage.vue index 0eaa0c0b..19da55d8 100644 --- a/src/views/desktop/SignupPage.vue +++ b/src/views/desktop/SignupPage.vue @@ -241,7 +241,7 @@ import { mapStores } from 'pinia'; import { useRootStore } from '@/stores/index.js'; import { useSettingsStore } from '@/stores/setting.ts'; import { useUserStore } from '@/stores/user.ts'; -import { useTransactionCategoriesStore } from '@/stores/transactionCategory.js'; +import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts'; import { useExchangeRatesStore } from '@/stores/exchangeRates.ts'; import { APPLICATION_LOGO_PATH } from '@/consts/asset.ts'; diff --git a/src/views/desktop/categories/ListPage.vue b/src/views/desktop/categories/ListPage.vue index f4cb5dcf..323720db 100644 --- a/src/views/desktop/categories/ListPage.vue +++ b/src/views/desktop/categories/ListPage.vue @@ -198,13 +198,13 @@ import PresetDialog from './list/dialogs/PresetDialog.vue'; import { useDisplay } from 'vuetify'; import { mapStores } from 'pinia'; -import { useTransactionCategoriesStore } from '@/stores/transactionCategory.js'; +import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts'; import { CategoryType } from '@/core/category.ts'; import { isNoAvailableCategory, getAvailableCategoryCount -} from '@/lib/category.js'; +} from '@/lib/category.ts'; import { getNavSideBarOuterHeight } from '@/lib/ui/desktop.ts'; import { diff --git a/src/views/desktop/categories/list/dialogs/EditDialog.vue b/src/views/desktop/categories/list/dialogs/EditDialog.vue index ea390e2f..65f08f84 100644 --- a/src/views/desktop/categories/list/dialogs/EditDialog.vue +++ b/src/views/desktop/categories/list/dialogs/EditDialog.vue @@ -95,16 +95,18 @@