diff --git a/src/lib/category.ts b/src/lib/category.ts index f3c0f9a0..4e260506 100644 --- a/src/lib/category.ts +++ b/src/lib/category.ts @@ -72,7 +72,7 @@ export function getTransactionSecondaryCategoryName(categoryId: string | null | return ''; } -export function allTransactionCategoriesWithVisibleCount(allTransactionCategories: Record, allowCategoryTypes: Record): Record { +export function allTransactionCategoriesWithVisibleCount(allTransactionCategories: Record, allowCategoryTypes?: Record): Record { const ret: Record = {}; const hasAllowCategoryTypes = allowCategoryTypes && (allowCategoryTypes[CategoryType.Income] @@ -339,7 +339,7 @@ export function getLastShowingId(categories: TransactionCategory[], showHidden: return null; } -export function hasAnyAvailableCategory(allTransactionCategories: Record, showHidden: boolean): boolean { +export function containsAnyAvailableCategory(allTransactionCategories: Record, showHidden: boolean): boolean { for (const type in allTransactionCategories) { if (!Object.prototype.hasOwnProperty.call(allTransactionCategories, type)) { continue; @@ -361,7 +361,7 @@ export function hasAnyAvailableCategory(allTransactionCategories: Record, showHidden: boolean): Record { +export function containsAvailableCategory(allTransactionCategories: Record, showHidden: boolean): Record { const result: Record = {}; for (const type in allTransactionCategories) { @@ -381,7 +381,7 @@ export function hasAvailableCategory(allTransactionCategories: Record, category: TransactionCategory, value: boolean): void { +export function selectAllSubCategories(filterCategoryIds: Record, category: TransactionCategory, value: boolean): void { if (!category || !category.secondaryCategories || !category.secondaryCategories.length) { return; } diff --git a/src/views/base/settings/CategoryFilterSettingPageBase.ts b/src/views/base/settings/CategoryFilterSettingPageBase.ts new file mode 100644 index 00000000..5303cf4f --- /dev/null +++ b/src/views/base/settings/CategoryFilterSettingPageBase.ts @@ -0,0 +1,188 @@ +import { ref, computed } from 'vue'; + +import { useI18n } from '@/locales/helpers.ts'; + +import { useSettingsStore } from '@/stores/setting.ts'; +import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts'; +import { useTransactionsStore } from '@/stores/transaction.ts'; +import { useStatisticsStore } from '@/stores/statistics.ts'; + +import { CategoryType } from '@/core/category.ts'; +import type { TransactionCategory, TransactionCategoriesWithVisibleCount } from '@/models/transaction_category.ts'; + +import { + copyObjectTo, + arrayItemToObjectField +} from '@/lib/common.ts'; +import { + allTransactionCategoriesWithVisibleCount, + containsAnyAvailableCategory, + containsAvailableCategory, + selectAllSubCategories, + isCategoryOrSubCategoriesAllChecked +} from '@/lib/category.ts'; + +export function useCategoryFilterSettingPageBase(type?: string, allowCategoryTypesStr?: string) { + const { tt } = useI18n(); + + const settingsStore = useSettingsStore(); + const transactionCategoriesStore = useTransactionCategoriesStore(); + const transactionsStore = useTransactionsStore(); + const statisticsStore = useStatisticsStore(); + + const allowCategoryTypes: Record | undefined = allowCategoryTypesStr ? arrayItemToObjectField(allowCategoryTypesStr.split(','), true) : undefined; + + const loading = ref(true); + const showHidden = ref(false); + const filterCategoryIds = ref>({}); + + const title = computed(() => { + if (type === 'statisticsDefault') { + return 'Default Transaction Category Filter'; + } else { + return 'Filter Transaction Categories'; + } + }); + + const applyText = computed(() => { + if (type === 'statisticsDefault') { + return 'Save'; + } else { + return 'Apply'; + } + }); + + 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)); + + function isCategoryChecked(category: TransactionCategory, filterCategoryIds: Record): boolean { + return !filterCategoryIds[category.id]; + } + + function getCategoryTypeName(categoryType: CategoryType): string { + switch (categoryType) { + case CategoryType.Income: + return tt('Income Categories'); + case CategoryType.Expense: + return tt('Expense Categories'); + case CategoryType.Transfer: + return tt('Transfer Categories'); + default: + return tt('Transaction Categories'); + } + } + + function loadFilterCategoryIds(): boolean { + const allCategoryIds: Record = {}; + + for (const categoryId in transactionCategoriesStore.allTransactionCategoriesMap) { + if (!Object.prototype.hasOwnProperty.call(transactionCategoriesStore.allTransactionCategoriesMap, categoryId)) { + continue; + } + + const category = transactionCategoriesStore.allTransactionCategoriesMap[categoryId]; + + if (allowCategoryTypes && !allowCategoryTypes[category.type]) { + continue; + } + + if (type === 'transactionListCurrent' && transactionsStore.allFilterCategoryIdsCount > 0) { + allCategoryIds[category.id] = true; + } else { + allCategoryIds[category.id] = false; + } + } + + if (type === 'statisticsDefault') { + filterCategoryIds.value = copyObjectTo(settingsStore.appSettings.statistics.defaultTransactionCategoryFilter, allCategoryIds) as Record; + return true; + } else if (type === 'statisticsCurrent') { + filterCategoryIds.value = copyObjectTo(statisticsStore.transactionStatisticsFilter.filterCategoryIds, allCategoryIds) as Record; + return true; + } else if (type === 'transactionListCurrent') { + for (const categoryId in transactionsStore.allFilterCategoryIds) { + if (!Object.prototype.hasOwnProperty.call(transactionsStore.allFilterCategoryIds, categoryId)) { + continue; + } + + const category = transactionCategoriesStore.allTransactionCategoriesMap[categoryId]; + + if (category && (!category.subCategories || !category.subCategories.length)) { + allCategoryIds[category.id] = false; + } else if (category) { + selectAllSubCategories(allCategoryIds, category, false); + } + } + + filterCategoryIds.value = allCategoryIds; + return true; + } else { + return false; + } + } + + function saveFilterCategoryIds(): boolean { + const filteredCategoryIds: Record = {}; + let isAllSelected = true; + let finalCategoryIds = ''; + let changed = true; + + for (const categoryId in filterCategoryIds.value) { + if (!Object.prototype.hasOwnProperty.call(filterCategoryIds.value, categoryId)) { + continue; + } + + const category = transactionCategoriesStore.allTransactionCategoriesMap[categoryId]; + + if (!isCategoryOrSubCategoriesAllChecked(category, filterCategoryIds.value)) { + filteredCategoryIds[categoryId] = true; + isAllSelected = false; + } else { + if (finalCategoryIds.length > 0) { + finalCategoryIds += ','; + } + + finalCategoryIds += categoryId; + } + } + + if (type === 'statisticsDefault') { + settingsStore.setStatisticsDefaultTransactionCategoryFilter(filteredCategoryIds); + } else if (type === 'statisticsCurrent') { + changed = statisticsStore.updateTransactionStatisticsFilter({ + filterCategoryIds: filteredCategoryIds + }); + } else if (type === 'transactionListCurrent') { + changed = transactionsStore.updateTransactionListFilter({ + categoryIds: isAllSelected ? '' : finalCategoryIds + }); + + if (changed) { + transactionsStore.updateTransactionListInvalidState(true); + } + } + + return changed; + } + + return { + // states + loading, + showHidden, + filterCategoryIds, + // computed states + title, + applyText, + allTransactionCategories, + hasAnyAvailableCategory, + hasAnyVisibleCategory, + hasAvailableCategory, + // functions + isCategoryChecked, + getCategoryTypeName, + loadFilterCategoryIds, + saveFilterCategoryIds + }; +} diff --git a/src/views/desktop/common/cards/CategoryFilterSettingsCard.vue b/src/views/desktop/common/cards/CategoryFilterSettingsCard.vue index acbbedc4..fd8bdb1f 100644 --- a/src/views/desktop/common/cards/CategoryFilterSettingsCard.vue +++ b/src/views/desktop/common/cards/CategoryFilterSettingsCard.vue @@ -3,7 +3,7 @@