From b1cefa5a344b2f5f74a1e7b0c0c89170c1e2d48c Mon Sep 17 00:00:00 2001 From: MaysWind Date: Sun, 14 Dec 2025 01:05:42 +0800 Subject: [PATCH] add search box in transaction category page / dialog --- src/lib/category.ts | 106 ++++------- src/lib/transaction.ts | 8 +- src/models/transaction_category.ts | 24 +-- src/stores/transaction.ts | 8 +- src/stores/transactionCategory.ts | 52 ++++-- .../settings/CategoryFilterSettingPageBase.ts | 44 +++-- .../transactions/TransactionEditPageBase.ts | 12 +- .../cards/CategoryFilterSettingsCard.vue | 164 +++++++----------- .../dialogs/BatchReplaceAllTypesDialog.vue | 12 +- .../import/dialogs/BatchReplaceDialog.vue | 12 +- .../tabs/ImportTransactionCheckDataTab.vue | 12 +- .../transactions/list/dialogs/EditDialog.vue | 18 +- .../settings/CategoryFilterSettingsPage.vue | 81 +++++---- src/views/mobile/transactions/EditPage.vue | 24 +-- 14 files changed, 279 insertions(+), 298 deletions(-) diff --git a/src/lib/category.ts b/src/lib/category.ts index 6ce2ddeb..22a376d9 100644 --- a/src/lib/category.ts +++ b/src/lib/category.ts @@ -1,8 +1,7 @@ -import { itemAndIndex, reversed, entries, keys, values } from '@/core/base.ts'; +import { reversed, keys, values } from '@/core/base.ts'; import { type LocalizedPresetCategory, CategoryType } from '@/core/category.ts'; import { TransactionType } from '@/core/transaction.ts'; import { - type TransactionCategoriesWithVisibleCount, type TransactionCategoryCreateRequest, type TransactionCategoryCreateWithSubCategories, TransactionCategory @@ -133,17 +132,20 @@ export function getTransactionSecondaryCategoryName(categoryId: string | null | return ''; } -export function allTransactionCategoriesWithVisibleCount(allTransactionCategories: Record, allowCategoryTypes?: Record): Record { - const ret: Record = {}; +export function filterTransactionCategories(allTransactionCategories: Record, allowCategoryTypes?: Record, allowCategoryName?: string, showHidden?: boolean): Record { + const ret: Record = {}; const hasAllowCategoryTypes = allowCategoryTypes && (allowCategoryTypes[CategoryType.Income] || allowCategoryTypes[CategoryType.Expense] || allowCategoryTypes[CategoryType.Transfer]); const allCategoryTypes = [ CategoryType.Income, CategoryType.Expense, CategoryType.Transfer ]; + const lowercaseFilterContent = allowCategoryName ? allowCategoryName.toLowerCase() : ''; for (const categoryType of allCategoryTypes) { - if (!allTransactionCategories[categoryType]) { + const allCategories = allTransactionCategories[categoryType]; + + if (!allCategories || allCategories.length < 1) { continue; } @@ -151,53 +153,41 @@ export function allTransactionCategoriesWithVisibleCount(allTransactionCategorie continue; } - const allCategories: TransactionCategory[] = allTransactionCategories[categoryType]; - const allSubCategories: Record = {}; - const allVisibleSubCategoryCounts: Record = {}; - const allFirstVisibleSubCategoryIndexes: Record = {}; - let allVisibleCategoryCount = 0; - let firstVisibleCategoryIndex = -1; + const allFilteredCategories: TransactionCategory[] = []; - for (const [category, cagtegoryIndex] of itemAndIndex(allCategories)) { - if (!category.hidden) { - allVisibleCategoryCount++; - - if (firstVisibleCategoryIndex === -1) { - firstVisibleCategoryIndex = cagtegoryIndex; - } + for (const category of allCategories) { + if (!showHidden && category.hidden) { + continue; } + const categoryMatchesName = !lowercaseFilterContent || category.name.toLowerCase().includes(lowercaseFilterContent); + const filteredSubCategories: TransactionCategory[] = []; + if (category.subCategories) { - let visibleSubCategoryCount = 0; - let firstVisibleSubCategoryIndex = -1; - - for (const [subCategory, subCategoryIndex] of itemAndIndex(category.subCategories)) { - if (!subCategory.hidden) { - visibleSubCategoryCount++; - - if (firstVisibleSubCategoryIndex === -1) { - firstVisibleSubCategoryIndex = subCategoryIndex; - } + for (const subCategory of category.subCategories) { + if (!showHidden && subCategory.hidden) { + continue; } - } - if (category.subCategories.length > 0) { - allSubCategories[category.id] = category.subCategories; - allVisibleSubCategoryCounts[category.id] = visibleSubCategoryCount; - allFirstVisibleSubCategoryIndexes[category.id] = firstVisibleSubCategoryIndex; + if (!categoryMatchesName && lowercaseFilterContent && !subCategory.name.toLowerCase().includes(lowercaseFilterContent)) { + continue; + } + + const filteredSubCategory = subCategory.clone(); + filteredSubCategories.push(filteredSubCategory); } } + + if (!categoryMatchesName && filteredSubCategories.length < 1) { + continue; + } + + const filteredCategory = category.clone(); + filteredCategory.subCategories = filteredSubCategories; + allFilteredCategories.push(filteredCategory); } - ret[`${categoryType}`] = { - type: categoryType, - allCategories: allCategories, - allVisibleCategoryCount: allVisibleCategoryCount, - firstVisibleCategoryIndex: firstVisibleCategoryIndex, - allSubCategories: allSubCategories, - allVisibleSubCategoryCounts: allVisibleSubCategoryCounts, - allFirstVisibleSubCategoryIndexes: allFirstVisibleSubCategoryIndexes - }; + ret[`${categoryType}`] = allFilteredCategories; } return ret; @@ -274,7 +264,7 @@ export function isSubCategoryIdAvailable(categories: TransactionCategory[], cate return false; } -export function getFirstAvailableCategoryId(categories?: TransactionCategory[]): string { +export function getFirstVisibleCategoryId(categories?: TransactionCategory[]): string { if (!categories || !categories.length) { return ''; } @@ -374,36 +364,6 @@ export function getLastShowingId(categories: TransactionCategory[], showHidden: return null; } -export function containsAnyAvailableCategory(allTransactionCategories: Record, showHidden: boolean): boolean { - for (const categoryType of values(allTransactionCategories)) { - if (showHidden) { - if (categoryType.allCategories && categoryType.allCategories.length > 0) { - return true; - } - } else { - if (categoryType.allVisibleCategoryCount > 0) { - return true; - } - } - } - - return false; -} - -export function containsAvailableCategory(allTransactionCategories: Record, showHidden: boolean): Record { - const result: Record = {}; - - for (const [type, categoryType] of entries(allTransactionCategories)) { - if (showHidden) { - result[parseInt(type)] = categoryType.allCategories && categoryType.allCategories.length > 0; - } else { - result[parseInt(type)] = categoryType.allVisibleCategoryCount > 0; - } - } - - return result; -} - export function selectAllSubCategories(filterCategoryIds: Record, value: boolean, category?: TransactionCategory): void { if (!category || !category.subCategories || !category.subCategories.length) { return; diff --git a/src/lib/transaction.ts b/src/lib/transaction.ts index 2a9714ca..e13d564f 100644 --- a/src/lib/transaction.ts +++ b/src/lib/transaction.ts @@ -17,7 +17,7 @@ import { import { categoryTypeToTransactionType, isSubCategoryIdAvailable, - getFirstAvailableCategoryId, + getFirstVisibleCategoryId, getFirstAvailableSubCategoryId } from './category.ts'; @@ -66,7 +66,7 @@ export function setTransactionModelByTransaction(transaction: Transaction, trans } if (!transaction.expenseCategoryId) { - transaction.expenseCategoryId = getFirstAvailableCategoryId(allCategories[CategoryType.Expense]); + transaction.expenseCategoryId = getFirstVisibleCategoryId(allCategories[CategoryType.Expense]); } } @@ -81,7 +81,7 @@ export function setTransactionModelByTransaction(transaction: Transaction, trans } if (!transaction.incomeCategoryId) { - transaction.incomeCategoryId = getFirstAvailableCategoryId(allCategories[CategoryType.Income]); + transaction.incomeCategoryId = getFirstVisibleCategoryId(allCategories[CategoryType.Income]); } } @@ -96,7 +96,7 @@ export function setTransactionModelByTransaction(transaction: Transaction, trans } if (!transaction.transferCategoryId) { - transaction.transferCategoryId = getFirstAvailableCategoryId(allCategories[CategoryType.Transfer]); + transaction.transferCategoryId = getFirstVisibleCategoryId(allCategories[CategoryType.Transfer]); } } diff --git a/src/models/transaction_category.ts b/src/models/transaction_category.ts index e2e1e07a..579d0a55 100644 --- a/src/models/transaction_category.ts +++ b/src/models/transaction_category.ts @@ -81,6 +81,20 @@ export class TransactionCategory implements TransactionCategoryInfoResponse { this.visible = other.visible; } + public clone(): TransactionCategory { + return new TransactionCategory( + this.id, + this.name, + this.parentId, + this.type, + this.icon, + this.color, + this.comment, + this.displayOrder, + this.visible + ); + } + public toCreateRequest(clientSessionId: string): TransactionCategoryCreateRequest { return { name: this.name, @@ -217,13 +231,3 @@ 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/transaction.ts b/src/stores/transaction.ts index d0145c60..5c805261 100644 --- a/src/stores/transaction.ts +++ b/src/stores/transaction.ts @@ -61,7 +61,7 @@ import { } from '@/lib/datetime.ts'; import { getAmountWithDecimalNumberCount } from '@/lib/numeral.ts'; import { getCurrencyFraction } from '@/lib/currency.ts'; -import { getFirstAvailableCategoryId } from '@/lib/category.ts'; +import { getFirstVisibleCategoryId } from '@/lib/category.ts'; import services, { type ApiResponsePromise } from '@/lib/services.ts'; import logger from '@/lib/logger.ts'; @@ -499,19 +499,19 @@ export const useTransactionsStore = defineStore('transactions', () => { if (allCategories) { if (transaction.type === TransactionType.Expense) { - const defaultCategoryId = getFirstAvailableCategoryId(allCategories[CategoryType.Expense]); + const defaultCategoryId = getFirstVisibleCategoryId(allCategories[CategoryType.Expense]); if (transaction.expenseCategoryId && transaction.expenseCategoryId !== '0' && transaction.expenseCategoryId !== defaultCategoryId && transaction.expenseCategoryId !== initCategoryId) { return true; } } else if (transaction.type === TransactionType.Income) { - const defaultCategoryId = getFirstAvailableCategoryId(allCategories[CategoryType.Income]); + const defaultCategoryId = getFirstVisibleCategoryId(allCategories[CategoryType.Income]); if (transaction.incomeCategoryId && transaction.incomeCategoryId !== '0' && transaction.incomeCategoryId !== defaultCategoryId && transaction.incomeCategoryId !== initCategoryId) { return true; } } else if (transaction.type === TransactionType.Transfer) { - const defaultCategoryId = getFirstAvailableCategoryId(allCategories[CategoryType.Transfer]); + const defaultCategoryId = getFirstVisibleCategoryId(allCategories[CategoryType.Transfer]); if (transaction.transferCategoryId && transaction.transferCategoryId !== '0' && transaction.transferCategoryId !== defaultCategoryId && transaction.transferCategoryId !== initCategoryId) { return true; diff --git a/src/stores/transactionCategory.ts b/src/stores/transactionCategory.ts index f7e9cfb1..0cd37c51 100644 --- a/src/stores/transactionCategory.ts +++ b/src/stores/transactionCategory.ts @@ -14,7 +14,7 @@ import { } from '@/models/transaction_category.ts'; import { isEquals } from '@/lib/common.ts'; -import { getFirstAvailableCategoryId } from '@/lib/category.ts'; +import { getFirstVisibleCategoryId } from '@/lib/category.ts'; import services, { type ApiResponsePromise } from '@/lib/services.ts'; import logger from '@/lib/logger.ts'; @@ -23,31 +23,55 @@ export const useTransactionCategoriesStore = defineStore('transactionCategories' const allTransactionCategoriesMap = ref>({}); const transactionCategoryListStateInvalid = ref(true); - const hasAvailableExpenseCategories = computed(() => { + const allAvailablePrimaryCategoriesCount = computed(() => { + let count = 0; + + for (const categories of values(allTransactionCategories.value)) { + count += categories.length; + } + + return count; + }); + + const allAvailableSecondaryCategoriesCount = computed(() => { + let count = 0; + + for (const categories of values(allTransactionCategories.value)) { + for (const category of categories) { + if (category.subCategories) { + count += category.subCategories.length; + } + } + } + + return count; + }); + + const hasVisibleExpenseCategories = computed(() => { if (!allTransactionCategories.value || !allTransactionCategories.value[CategoryType.Expense] || !allTransactionCategories.value[CategoryType.Expense].length) { return false; } - const firstAvailableCategoryId = getFirstAvailableCategoryId(allTransactionCategories.value[CategoryType.Expense]); - return firstAvailableCategoryId !== ''; + const firstVisibleCategoryId = getFirstVisibleCategoryId(allTransactionCategories.value[CategoryType.Expense]); + return firstVisibleCategoryId !== ''; }); - const hasAvailableIncomeCategories = computed(() => { + const hasVisibleIncomeCategories = computed(() => { if (!allTransactionCategories.value || !allTransactionCategories.value[CategoryType.Income] || !allTransactionCategories.value[CategoryType.Income].length) { return false; } - const firstAvailableCategoryId = getFirstAvailableCategoryId(allTransactionCategories.value[CategoryType.Income]); - return firstAvailableCategoryId !== ''; + const firstVisibleCategoryId = getFirstVisibleCategoryId(allTransactionCategories.value[CategoryType.Income]); + return firstVisibleCategoryId !== ''; }); - const hasAvailableTransferCategories = computed(() => { + const hasVisibleTransferCategories = computed(() => { if (!allTransactionCategories.value || !allTransactionCategories.value[CategoryType.Transfer] || !allTransactionCategories.value[CategoryType.Transfer].length) { return false; } - const firstAvailableCategoryId = getFirstAvailableCategoryId(allTransactionCategories.value[CategoryType.Transfer]); - return firstAvailableCategoryId !== ''; + const firstVisibleCategoryId = getFirstVisibleCategoryId(allTransactionCategories.value[CategoryType.Transfer]); + return firstVisibleCategoryId !== ''; }); function loadTransactionCategoryList(allCategories: Record): void { @@ -548,9 +572,11 @@ export const useTransactionCategoriesStore = defineStore('transactionCategories' allTransactionCategoriesMap, transactionCategoryListStateInvalid, // computed states - hasAvailableExpenseCategories, - hasAvailableIncomeCategories, - hasAvailableTransferCategories, + allAvailablePrimaryCategoriesCount, + allAvailableSecondaryCategoriesCount, + hasVisibleExpenseCategories, + hasVisibleIncomeCategories, + hasVisibleTransferCategories, // functions updateTransactionCategoryListInvalidState, resetTransactionCategories, diff --git a/src/views/base/settings/CategoryFilterSettingPageBase.ts b/src/views/base/settings/CategoryFilterSettingPageBase.ts index 0130a2da..89094a54 100644 --- a/src/views/base/settings/CategoryFilterSettingPageBase.ts +++ b/src/views/base/settings/CategoryFilterSettingPageBase.ts @@ -10,15 +10,13 @@ import { useOverviewStore } from '@/stores/overview.ts'; import { keys, keysIfValueEquals, values } from '@/core/base.ts'; import { CategoryType } from '@/core/category.ts'; -import type { TransactionCategory, TransactionCategoriesWithVisibleCount } from '@/models/transaction_category.ts'; +import type { TransactionCategory } from '@/models/transaction_category.ts'; import { arrayItemToObjectField } from '@/lib/common.ts'; import { - allTransactionCategoriesWithVisibleCount, - containsAnyAvailableCategory, - containsAvailableCategory, + filterTransactionCategories, selectAllSubCategories, isCategoryOrSubCategoriesAllChecked } from '@/lib/category.ts'; @@ -38,6 +36,7 @@ export function useCategoryFilterSettingPageBase(type?: CategoryFilterType, allo const loading = ref(true); const showHidden = ref(false); + const filterContent = ref(''); const filterCategoryIds = ref>({}); const title = computed(() => { @@ -56,10 +55,34 @@ export function useCategoryFilterSettingPageBase(type?: CategoryFilterType, allo } }); - const allTransactionCategories = computed>(() => allTransactionCategoriesWithVisibleCount(transactionCategoriesStore.allTransactionCategories, allowCategoryTypes)); - const hasAnyAvailableCategory = computed(() => containsAnyAvailableCategory(allTransactionCategories.value, true)); - const hasAnyVisibleCategory = computed(() => containsAnyAvailableCategory(allTransactionCategories.value, showHidden.value)); - const hasAvailableCategory = computed>(() => containsAvailableCategory(allTransactionCategories.value, showHidden.value)); + const allVisibleTransactionCategories = computed>(() => filterTransactionCategories(transactionCategoriesStore.allTransactionCategories, allowCategoryTypes, filterContent.value, showHidden.value)); + const allVisibleTransactionCategoryMap = computed>(() => { + const categoryMap: Record = {}; + + for (const categories of values(allVisibleTransactionCategories.value)) { + for (const category of categories) { + categoryMap[category.id] = category; + + if (category.subCategories) { + for (const subCategory of category.subCategories) { + categoryMap[subCategory.id] = subCategory; + } + } + } + } + + return categoryMap; + }); + const hasAnyAvailableCategory = computed(() => transactionCategoriesStore.allAvailablePrimaryCategoriesCount > 0 || transactionCategoriesStore.allAvailableSecondaryCategoriesCount > 0); + const hasAnyVisibleCategory = computed(() => { + for (const categories of values(allVisibleTransactionCategories.value)) { + if (categories.length > 0) { + return true; + } + } + + return false; + }); function isCategoryChecked(category: TransactionCategory, filterCategoryIds: Record): boolean { return !filterCategoryIds[category.id]; @@ -171,14 +194,15 @@ export function useCategoryFilterSettingPageBase(type?: CategoryFilterType, allo // states loading, showHidden, + filterContent, filterCategoryIds, // computed states title, applyText, - allTransactionCategories, + allVisibleTransactionCategories, + allVisibleTransactionCategoryMap, hasAnyAvailableCategory, hasAnyVisibleCategory, - hasAvailableCategory, // functions isCategoryChecked, getCategoryTypeName, diff --git a/src/views/base/transactions/TransactionEditPageBase.ts b/src/views/base/transactions/TransactionEditPageBase.ts index 2abc3bdd..ff179e87 100644 --- a/src/views/base/transactions/TransactionEditPageBase.ts +++ b/src/views/base/transactions/TransactionEditPageBase.ts @@ -110,9 +110,9 @@ export function useTransactionEditPageBase(type: TransactionEditPageType, initMo const allTagsMap = computed>(() => transactionTagsStore.allTransactionTagsMap); const firstVisibleAccountId = computed(() => allVisibleAccounts.value && allVisibleAccounts.value[0] ? allVisibleAccounts.value[0].id : undefined); - const hasAvailableExpenseCategories = computed(() => transactionCategoriesStore.hasAvailableExpenseCategories); - const hasAvailableIncomeCategories = computed(() => transactionCategoriesStore.hasAvailableIncomeCategories); - const hasAvailableTransferCategories = computed(() => transactionCategoriesStore.hasAvailableTransferCategories); + const hasVisibleExpenseCategories = computed(() => transactionCategoriesStore.hasVisibleExpenseCategories); + const hasVisibleIncomeCategories = computed(() => transactionCategoriesStore.hasVisibleIncomeCategories); + const hasVisibleTransferCategories = computed(() => transactionCategoriesStore.hasVisibleTransferCategories); const canAddTransactionPicture = computed(() => { if (type !== TransactionEditPageType.Transaction || (mode.value !== TransactionEditPageMode.Add && mode.value !== TransactionEditPageMode.Edit)) { @@ -438,9 +438,9 @@ export function useTransactionEditPageBase(type: TransactionEditPageType, initMo allTags, allTagsMap, firstVisibleAccountId, - hasAvailableExpenseCategories, - hasAvailableIncomeCategories, - hasAvailableTransferCategories, + hasVisibleExpenseCategories, + hasVisibleIncomeCategories, + hasVisibleTransferCategories, canAddTransactionPicture, title, saveButtonTitle, diff --git a/src/views/desktop/common/cards/CategoryFilterSettingsCard.vue b/src/views/desktop/common/cards/CategoryFilterSettingsCard.vue index a2927fd9..94f4460c 100644 --- a/src/views/desktop/common/cards/CategoryFilterSettingsCard.vue +++ b/src/views/desktop/common/cards/CategoryFilterSettingsCard.vue @@ -1,79 +1,48 @@