add search box in transaction category page / dialog

This commit is contained in:
MaysWind
2025-12-14 01:05:42 +08:00
parent a12038e40c
commit b1cefa5a34
14 changed files with 279 additions and 298 deletions
+33 -73
View File
@@ -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<number, TransactionCategory[]>, allowCategoryTypes?: Record<number, boolean>): Record<number, TransactionCategoriesWithVisibleCount> {
const ret: Record<string, TransactionCategoriesWithVisibleCount> = {};
export function filterTransactionCategories(allTransactionCategories: Record<number, TransactionCategory[]>, allowCategoryTypes?: Record<number, boolean>, allowCategoryName?: string, showHidden?: boolean): Record<number, TransactionCategory[]> {
const ret: Record<string, TransactionCategory[]> = {};
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<string, TransactionCategory[]> = {};
const allVisibleSubCategoryCounts: Record<string, number> = {};
const allFirstVisibleSubCategoryIndexes: Record<string, number> = {};
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<string, TransactionCategoriesWithVisibleCount>, 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<string, TransactionCategoriesWithVisibleCount>, showHidden: boolean): Record<number, boolean> {
const result: Record<number, boolean> = {};
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<string, boolean>, value: boolean, category?: TransactionCategory): void {
if (!category || !category.subCategories || !category.subCategories.length) {
return;
+4 -4
View File
@@ -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]);
}
}
+14 -10
View File
@@ -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<string, TransactionCategory[]>;
readonly allVisibleSubCategoryCounts: Record<string, number>;
readonly allFirstVisibleSubCategoryIndexes: Record<string, number>;
}
+4 -4
View File
@@ -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;
+39 -13
View File
@@ -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<Record<string, TransactionCategory>>({});
const transactionCategoryListStateInvalid = ref<boolean>(true);
const hasAvailableExpenseCategories = computed<boolean>(() => {
const allAvailablePrimaryCategoriesCount = computed<number>(() => {
let count = 0;
for (const categories of values(allTransactionCategories.value)) {
count += categories.length;
}
return count;
});
const allAvailableSecondaryCategoriesCount = computed<number>(() => {
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<boolean>(() => {
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<boolean>(() => {
const hasVisibleIncomeCategories = computed<boolean>(() => {
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<boolean>(() => {
const hasVisibleTransferCategories = computed<boolean>(() => {
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<number, TransactionCategory[]>): 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,
@@ -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<boolean>(true);
const showHidden = ref<boolean>(false);
const filterContent = ref<string>('');
const filterCategoryIds = ref<Record<string, boolean>>({});
const title = computed<string>(() => {
@@ -56,10 +55,34 @@ export function useCategoryFilterSettingPageBase(type?: CategoryFilterType, allo
}
});
const allTransactionCategories = computed<Record<string, TransactionCategoriesWithVisibleCount>>(() => allTransactionCategoriesWithVisibleCount(transactionCategoriesStore.allTransactionCategories, allowCategoryTypes));
const hasAnyAvailableCategory = computed<boolean>(() => containsAnyAvailableCategory(allTransactionCategories.value, true));
const hasAnyVisibleCategory = computed<boolean>(() => containsAnyAvailableCategory(allTransactionCategories.value, showHidden.value));
const hasAvailableCategory = computed<Record<number, boolean>>(() => containsAvailableCategory(allTransactionCategories.value, showHidden.value));
const allVisibleTransactionCategories = computed<Record<string, TransactionCategory[]>>(() => filterTransactionCategories(transactionCategoriesStore.allTransactionCategories, allowCategoryTypes, filterContent.value, showHidden.value));
const allVisibleTransactionCategoryMap = computed<Record<string, TransactionCategory>>(() => {
const categoryMap: Record<string, TransactionCategory> = {};
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<boolean>(() => transactionCategoriesStore.allAvailablePrimaryCategoriesCount > 0 || transactionCategoriesStore.allAvailableSecondaryCategoriesCount > 0);
const hasAnyVisibleCategory = computed<boolean>(() => {
for (const categories of values(allVisibleTransactionCategories.value)) {
if (categories.length > 0) {
return true;
}
}
return false;
});
function isCategoryChecked(category: TransactionCategory, filterCategoryIds: Record<string, boolean>): 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,
@@ -110,9 +110,9 @@ export function useTransactionEditPageBase(type: TransactionEditPageType, initMo
const allTagsMap = computed<Record<string, TransactionTag>>(() => transactionTagsStore.allTransactionTagsMap);
const firstVisibleAccountId = computed<string | undefined>(() => allVisibleAccounts.value && allVisibleAccounts.value[0] ? allVisibleAccounts.value[0].id : undefined);
const hasAvailableExpenseCategories = computed<boolean>(() => transactionCategoriesStore.hasAvailableExpenseCategories);
const hasAvailableIncomeCategories = computed<boolean>(() => transactionCategoriesStore.hasAvailableIncomeCategories);
const hasAvailableTransferCategories = computed<boolean>(() => transactionCategoriesStore.hasAvailableTransferCategories);
const hasVisibleExpenseCategories = computed<boolean>(() => transactionCategoriesStore.hasVisibleExpenseCategories);
const hasVisibleIncomeCategories = computed<boolean>(() => transactionCategoriesStore.hasVisibleIncomeCategories);
const hasVisibleTransferCategories = computed<boolean>(() => transactionCategoriesStore.hasVisibleTransferCategories);
const canAddTransactionPicture = computed<boolean>(() => {
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,
@@ -1,79 +1,48 @@
<template>
<v-card :class="{ 'pa-sm-1 pa-md-2': dialogMode }">
<template #title>
<div class="d-flex align-center justify-center" v-if="dialogMode">
<div class="w-100 text-center">
<h4 class="text-h4">{{ tt(title) }}</h4>
</div>
<v-btn density="comfortable" color="default" variant="text" class="ms-2"
:disabled="loading || !hasAnyAvailableCategory" :icon="true">
<v-icon :icon="mdiDotsVertical" />
<v-menu activator="parent">
<v-list>
<v-list-item :prepend-icon="mdiSelectAll"
:title="tt('Select All')"
:disabled="!hasAnyVisibleCategory"
@click="selectAllCategories"></v-list-item>
<v-list-item :prepend-icon="mdiSelect"
:title="tt('Select None')"
:disabled="!hasAnyVisibleCategory"
@click="selectNoneCategories"></v-list-item>
<v-list-item :prepend-icon="mdiSelectInverse"
:title="tt('Invert Selection')"
:disabled="!hasAnyVisibleCategory"
@click="selectInvertCategories"></v-list-item>
<v-divider class="my-2"/>
<v-list-item :prepend-icon="mdiSelectAll"
:title="tt('Select All Visible')"
:disabled="!hasAnyVisibleCategory"
@click="selectAllVisibleCategories"></v-list-item>
<v-divider class="my-2"/>
<v-list-item :prepend-icon="mdiEyeOutline"
:title="tt('Show Hidden Transaction Categories')"
v-if="!showHidden" @click="showHidden = true"></v-list-item>
<v-list-item :prepend-icon="mdiEyeOffOutline"
:title="tt('Hide Hidden Transaction Categories')"
v-if="showHidden" @click="showHidden = false"></v-list-item>
</v-list>
</v-menu>
</v-btn>
</div>
<div class="d-flex align-center" v-else-if="!dialogMode">
<span>{{ tt(title) }}</span>
<v-spacer/>
<v-btn density="comfortable" color="default" variant="text" class="ms-2"
:disabled="loading" :icon="true">
<v-icon :icon="mdiDotsVertical" />
<v-menu activator="parent">
<v-list>
<v-list-item :prepend-icon="mdiSelectAll"
:title="tt('Select All')"
:disabled="!hasAnyVisibleCategory"
@click="selectAllCategories"></v-list-item>
<v-list-item :prepend-icon="mdiSelect"
:title="tt('Select None')"
:disabled="!hasAnyVisibleCategory"
@click="selectNoneCategories"></v-list-item>
<v-list-item :prepend-icon="mdiSelectInverse"
:title="tt('Invert Selection')"
:disabled="!hasAnyVisibleCategory"
@click="selectInvertCategories"></v-list-item>
<v-divider class="my-2"/>
<v-list-item :prepend-icon="mdiSelectAll"
:title="tt('Select All Visible')"
:disabled="!hasAnyVisibleCategory"
@click="selectAllVisibleCategories"></v-list-item>
<v-divider class="my-2"/>
<v-list-item :prepend-icon="mdiEyeOutline"
:title="tt('Show Hidden Transaction Categories')"
v-if="!showHidden" @click="showHidden = true"></v-list-item>
<v-list-item :prepend-icon="mdiEyeOffOutline"
:title="tt('Hide Hidden Transaction Categories')"
v-if="showHidden" @click="showHidden = false"></v-list-item>
</v-list>
</v-menu>
</v-btn>
</div>
<v-row>
<v-col cols="6">
<div :class="{ 'text-h4': dialogMode, 'text-wrap': true }">
{{ tt(title) }}
</div>
</v-col>
<v-col cols="6" class="d-flex align-center">
<v-spacer v-if="!dialogMode"/>
<v-text-field density="compact" :disabled="loading || !hasAnyAvailableCategory"
:prepend-inner-icon="mdiMagnify"
:placeholder="tt('Find category')"
v-model="filterContent"
v-if="dialogMode"></v-text-field>
<v-btn density="comfortable" color="default" variant="text" class="ms-2"
:disabled="loading || !hasAnyAvailableCategory" :icon="true">
<v-icon :icon="mdiDotsVertical" />
<v-menu activator="parent">
<v-list>
<v-list-item :prepend-icon="mdiSelectAll"
:title="tt('Select All')"
:disabled="!hasAnyVisibleCategory"
@click="selectAllCategories"></v-list-item>
<v-list-item :prepend-icon="mdiSelect"
:title="tt('Select None')"
:disabled="!hasAnyVisibleCategory"
@click="selectNoneCategories"></v-list-item>
<v-list-item :prepend-icon="mdiSelectInverse"
:title="tt('Invert Selection')"
:disabled="!hasAnyVisibleCategory"
@click="selectInvertCategories"></v-list-item>
<v-divider class="my-2"/>
<v-list-item :prepend-icon="mdiEyeOutline"
:title="tt('Show Hidden Transaction Categories')"
v-if="!showHidden" @click="showHidden = true"></v-list-item>
<v-list-item :prepend-icon="mdiEyeOffOutline"
:title="tt('Hide Hidden Transaction Categories')"
v-if="showHidden" @click="showHidden = false"></v-list-item>
</v-list>
</v-menu>
</v-btn>
</v-col>
</v-row>
</template>
<div v-if="loading">
@@ -83,22 +52,22 @@
<v-card-text :class="{ 'flex-grow-1 overflow-y-auto': dialogMode }" v-else-if="!loading">
<v-expansion-panels class="category-types" multiple v-model="expandCategoryTypes">
<v-expansion-panel :key="transactionType.type"
:value="transactionType.type"
<v-expansion-panel :key="categoryType"
:value="parseInt(categoryType) as CategoryType"
class="border"
v-for="transactionType in allTransactionCategories">
v-for="(categories, categoryType) in allVisibleTransactionCategories">
<v-expansion-panel-title class="expand-panel-title-with-bg py-0">
<span class="ms-3">{{ getCategoryTypeName(transactionType.type) }}</span>
<span class="ms-3">{{ getCategoryTypeName(parseInt(categoryType)) }}</span>
</v-expansion-panel-title>
<v-expansion-panel-text>
<v-list rounded density="comfortable" class="pa-0">
<div class="ms-5 py-3" v-if="!hasAvailableCategory[transactionType.type]">{{ tt('No available category') }}</div>
<div class="ms-5 py-3" v-if="!categories || !categories.length">{{ tt('No available category') }}</div>
<template :key="category.id"
v-for="(category, idx) in transactionType.allCategories">
<v-divider v-if="showHidden ? idx > 0 : (!category.hidden ? idx > transactionType.firstVisibleCategoryIndex : false)"/>
v-for="(category, idx) in categories">
<v-divider v-if="idx > 0"/>
<v-list-item v-if="showHidden || !category.hidden">
<v-list-item>
<template #prepend>
<v-checkbox :model-value="isSubCategoriesAllChecked(category, filterCategoryIds)"
:indeterminate="isSubCategoriesHasButNotAllChecked(category, filterCategoryIds)"
@@ -112,15 +81,15 @@
</template>
</v-list-item>
<v-divider v-if="(showHidden || !category.hidden) && ((showHidden && transactionType.allSubCategories[category.id]) || transactionType.allVisibleSubCategoryCounts[category.id])"/>
<v-divider v-if="category.subCategories && category.subCategories.length"/>
<v-list rounded density="comfortable" class="pa-0 ms-4"
v-if="(showHidden || !category.hidden) && ((showHidden && transactionType.allSubCategories[category.id]) || transactionType.allVisibleSubCategoryCounts[category.id])">
v-if="category.subCategories && category.subCategories.length">
<template :key="subCategory.id"
v-for="(subCategory, subIdx) in transactionType.allSubCategories[category.id]">
<v-divider v-if="showHidden ? subIdx > 0 : (!subCategory.hidden ? subIdx > (transactionType.allFirstVisibleSubCategoryIndexes[category.id] as number) : false)"/>
v-for="(subCategory, subIdx) in category.subCategories">
<v-divider v-if="subIdx > 0"/>
<v-list-item v-if="showHidden || !subCategory.hidden">
<v-list-item>
<template #prepend>
<v-checkbox :model-value="isCategoryChecked(subCategory, filterCategoryIds)"
@update:model-value="updateCategorySelected(subCategory, $event)">
@@ -143,7 +112,7 @@
<v-card-text class="overflow-y-visible" v-if="dialogMode">
<div class="w-100 d-flex justify-center flex-wrap mt-sm-1 mt-md-2 gap-4">
<v-btn :disabled="!hasAnyVisibleCategory" @click="save">{{ tt(applyText) }}</v-btn>
<v-btn :disabled="!hasAnyAvailableCategory" @click="save">{{ tt(applyText) }}</v-btn>
<v-btn color="secondary" variant="tonal" @click="cancel">{{ tt('Cancel') }}</v-btn>
</div>
</v-card-text>
@@ -170,7 +139,6 @@ import type { TransactionCategory } from '@/models/transaction_category.ts';
import {
selectAllSubCategories,
selectAllVisible,
selectAll,
selectNone,
selectInvert,
@@ -179,6 +147,7 @@ import {
} from '@/lib/category.ts';
import {
mdiMagnify,
mdiSelectAll,
mdiSelect,
mdiSelectInverse,
@@ -205,13 +174,14 @@ const { tt } = useI18n();
const {
loading,
showHidden,
filterContent,
filterCategoryIds,
title,
applyText,
allTransactionCategories,
allVisibleTransactionCategories,
allVisibleTransactionCategoryMap,
hasAnyAvailableCategory,
hasAnyVisibleCategory,
hasAvailableCategory,
isCategoryChecked,
getCategoryTypeName,
loadFilterCategoryIds,
@@ -267,7 +237,7 @@ function updateAllSubCategoriesSelected(category: TransactionCategory, value: bo
}
function selectAllCategories(): void {
selectAll(filterCategoryIds.value, transactionCategoriesStore.allTransactionCategoriesMap);
selectAll(filterCategoryIds.value, allVisibleTransactionCategoryMap.value);
if (props.autoSave) {
save();
@@ -275,7 +245,7 @@ function selectAllCategories(): void {
}
function selectNoneCategories(): void {
selectNone(filterCategoryIds.value, transactionCategoriesStore.allTransactionCategoriesMap);
selectNone(filterCategoryIds.value, allVisibleTransactionCategoryMap.value);
if (props.autoSave) {
save();
@@ -283,15 +253,7 @@ function selectNoneCategories(): void {
}
function selectInvertCategories(): void {
selectInvert(filterCategoryIds.value, transactionCategoriesStore.allTransactionCategoriesMap);
if (props.autoSave) {
save();
}
}
function selectAllVisibleCategories(): void {
selectAllVisible(filterCategoryIds.value, transactionCategoriesStore.allTransactionCategoriesMap);
selectInvert(filterCategoryIds.value, allVisibleTransactionCategoryMap.value);
if (props.autoSave) {
save();
@@ -104,7 +104,7 @@
secondary-key-field="id" secondary-value-field="id" secondary-title-field="name"
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
secondary-hidden-field="hidden"
:disabled="loading || !hasAvailableExpenseCategories"
:disabled="loading || !hasVisibleExpenseCategories"
:enable-filter="true" :filter-placeholder="tt('Find category')" :filter-no-items-text="tt('No available category')"
:show-selection-primary-text="true"
:custom-selection-primary-text="getTransactionPrimaryCategoryName(newRule.targetId, allCategories[CategoryType.Expense])"
@@ -120,7 +120,7 @@
secondary-key-field="id" secondary-value-field="id" secondary-title-field="name"
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
secondary-hidden-field="hidden"
:disabled="loading || !hasAvailableIncomeCategories"
:disabled="loading || !hasVisibleIncomeCategories"
:enable-filter="true" :filter-placeholder="tt('Find category')" :filter-no-items-text="tt('No available category')"
:show-selection-primary-text="true"
:custom-selection-primary-text="getTransactionPrimaryCategoryName(newRule.targetId, allCategories[CategoryType.Income])"
@@ -136,7 +136,7 @@
secondary-key-field="id" secondary-value-field="id" secondary-title-field="name"
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
secondary-hidden-field="hidden"
:disabled="loading || !hasAvailableTransferCategories"
:disabled="loading || !hasVisibleTransferCategories"
:enable-filter="true" :filter-placeholder="tt('Find category')" :filter-no-items-text="tt('No available category')"
:show-selection-primary-text="true"
:custom-selection-primary-text="getTransactionPrimaryCategoryName(newRule.targetId, allCategories[CategoryType.Transfer])"
@@ -285,9 +285,9 @@ const allVisibleCategorizedAccounts = computed<CategorizedAccountWithDisplayBala
const allCategories = computed<Record<number, TransactionCategory[]>>(() => transactionCategoriesStore.allTransactionCategories);
const allTags = computed<TransactionTag[]>(() => transactionTagsStore.allTransactionTags);
const hasAvailableExpenseCategories = computed<boolean>(() => transactionCategoriesStore.hasAvailableExpenseCategories);
const hasAvailableIncomeCategories = computed<boolean>(() => transactionCategoriesStore.hasAvailableIncomeCategories);
const hasAvailableTransferCategories = computed<boolean>(() => transactionCategoriesStore.hasAvailableTransferCategories);
const hasVisibleExpenseCategories = computed<boolean>(() => transactionCategoriesStore.hasVisibleExpenseCategories);
const hasVisibleIncomeCategories = computed<boolean>(() => transactionCategoriesStore.hasVisibleIncomeCategories);
const hasVisibleTransferCategories = computed<boolean>(() => transactionCategoriesStore.hasVisibleTransferCategories);
const sourceItems = computed<NameValue[]>(() => {
switch (newRule.value.dataType) {
@@ -48,7 +48,7 @@
secondary-key-field="id" secondary-value-field="id" secondary-title-field="name"
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
secondary-hidden-field="hidden"
:disabled="loading || !hasAvailableExpenseCategories"
:disabled="loading || !hasVisibleExpenseCategories"
:enable-filter="true" :filter-placeholder="tt('Find category')" :filter-no-items-text="tt('No available category')"
:show-selection-primary-text="true"
:custom-selection-primary-text="getTransactionPrimaryCategoryName(targetItem, allCategories[CategoryType.Expense])"
@@ -65,7 +65,7 @@
secondary-key-field="id" secondary-value-field="id" secondary-title-field="name"
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
secondary-hidden-field="hidden"
:disabled="loading || !hasAvailableIncomeCategories"
:disabled="loading || !hasVisibleIncomeCategories"
:enable-filter="true" :filter-placeholder="tt('Find category')" :filter-no-items-text="tt('No available category')"
:show-selection-primary-text="true"
:custom-selection-primary-text="getTransactionPrimaryCategoryName(targetItem, allCategories[CategoryType.Income])"
@@ -82,7 +82,7 @@
secondary-key-field="id" secondary-value-field="id" secondary-title-field="name"
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
secondary-hidden-field="hidden"
:disabled="loading || !hasAvailableTransferCategories"
:disabled="loading || !hasVisibleTransferCategories"
:enable-filter="true" :filter-placeholder="tt('Find category')" :filter-no-items-text="tt('No available category')"
:show-selection-primary-text="true"
:custom-selection-primary-text="getTransactionPrimaryCategoryName(targetItem, allCategories[CategoryType.Transfer])"
@@ -275,9 +275,9 @@ const allVisibleCategorizedAccounts = computed<CategorizedAccountWithDisplayBala
const allCategories = computed<Record<number, TransactionCategory[]>>(() => transactionCategoriesStore.allTransactionCategories);
const allTags = computed<TransactionTag[]>(() => transactionTagsStore.allTransactionTags);
const hasAvailableExpenseCategories = computed<boolean>(() => transactionCategoriesStore.hasAvailableExpenseCategories);
const hasAvailableIncomeCategories = computed<boolean>(() => transactionCategoriesStore.hasAvailableIncomeCategories);
const hasAvailableTransferCategories = computed<boolean>(() => transactionCategoriesStore.hasAvailableTransferCategories);
const hasVisibleExpenseCategories = computed<boolean>(() => transactionCategoriesStore.hasVisibleExpenseCategories);
const hasVisibleIncomeCategories = computed<boolean>(() => transactionCategoriesStore.hasVisibleIncomeCategories);
const hasVisibleTransferCategories = computed<boolean>(() => transactionCategoriesStore.hasVisibleTransferCategories);
function getAccountDisplayName(accountId?: string): string {
if (accountId) {
@@ -113,7 +113,7 @@
secondary-key-field="id" secondary-value-field="id" secondary-title-field="name"
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
secondary-hidden-field="hidden"
:disabled="!!disabled || !hasAvailableExpenseCategories"
:disabled="!!disabled || !hasVisibleExpenseCategories"
:enable-filter="true" :filter-placeholder="tt('Find category')" :filter-no-items-text="tt('No available category')"
:show-selection-primary-text="true"
:custom-selection-primary-text="getTransactionPrimaryCategoryName(item.categoryId, allCategories[CategoryType.Expense])"
@@ -131,7 +131,7 @@
secondary-key-field="id" secondary-value-field="id" secondary-title-field="name"
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
secondary-hidden-field="hidden"
:disabled="!!disabled || !hasAvailableIncomeCategories"
:disabled="!!disabled || !hasVisibleIncomeCategories"
:enable-filter="true" :filter-placeholder="tt('Find category')" :filter-no-items-text="tt('No available category')"
:show-selection-primary-text="true"
:custom-selection-primary-text="getTransactionPrimaryCategoryName(item.categoryId, allCategories[CategoryType.Income])"
@@ -149,7 +149,7 @@
secondary-key-field="id" secondary-value-field="id" secondary-title-field="name"
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
secondary-hidden-field="hidden"
:disabled="!!disabled || !hasAvailableTransferCategories"
:disabled="!!disabled || !hasVisibleTransferCategories"
:enable-filter="true" :filter-placeholder="tt('Find category')" :filter-no-items-text="tt('No available category')"
:show-selection-primary-text="true"
:custom-selection-primary-text="getTransactionPrimaryCategoryName(item.categoryId, allCategories[CategoryType.Transfer])"
@@ -551,9 +551,9 @@ const allCategoriesMap = computed<Record<string, TransactionCategory>>(() => tra
const allTags = computed<TransactionTag[]>(() => transactionTagsStore.allTransactionTags);
const allTagsMap = computed<Record<string, TransactionTag>>(() => transactionTagsStore.allTransactionTagsMap);
const hasAvailableExpenseCategories = computed<boolean>(() => transactionCategoriesStore.hasAvailableExpenseCategories);
const hasAvailableIncomeCategories = computed<boolean>(() => transactionCategoriesStore.hasAvailableIncomeCategories);
const hasAvailableTransferCategories = computed<boolean>(() => transactionCategoriesStore.hasAvailableTransferCategories);
const hasVisibleExpenseCategories = computed<boolean>(() => transactionCategoriesStore.hasVisibleExpenseCategories);
const hasVisibleIncomeCategories = computed<boolean>(() => transactionCategoriesStore.hasVisibleIncomeCategories);
const hasVisibleTransferCategories = computed<boolean>(() => transactionCategoriesStore.hasVisibleTransferCategories);
const isEditing = computed<boolean>(() => !!editingTransaction.value);
const canImport = computed<boolean>(() => selectedImportTransactionCount.value > 0 && selectedInvalidTransactionCount.value < 1);
@@ -124,7 +124,7 @@
v-model="transaction.destinationAmount"/>
</v-col>
<v-col cols="12" md="12" v-if="transaction.type === TransactionType.Expense">
<v-tooltip :disabled="hasAvailableExpenseCategories" :text="hasAvailableExpenseCategories ? '' : tt('No secondary expense categories are available')">
<v-tooltip :disabled="hasVisibleExpenseCategories" :text="hasVisibleExpenseCategories ? '' : tt('No secondary expense categories are available')">
<template v-slot:activator="{ props }">
<div v-bind="props" class="d-block">
<two-column-select primary-key-field="id" primary-value-field="id" primary-title-field="name"
@@ -134,7 +134,7 @@
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
secondary-hidden-field="hidden"
:readonly="mode === TransactionEditPageMode.View"
:disabled="loading || submitting || !hasAvailableExpenseCategories"
:disabled="loading || submitting || !hasVisibleExpenseCategories"
:enable-filter="true" :filter-placeholder="tt('Find category')" :filter-no-items-text="tt('No available category')"
:show-selection-primary-text="true"
:custom-selection-primary-text="getTransactionPrimaryCategoryName(transaction.expenseCategoryId, allCategories[CategoryType.Expense])"
@@ -148,7 +148,7 @@
</v-tooltip>
</v-col>
<v-col cols="12" md="12" v-if="transaction.type === TransactionType.Income">
<v-tooltip :disabled="hasAvailableIncomeCategories" :text="hasAvailableIncomeCategories ? '' : tt('No secondary income categories are available')">
<v-tooltip :disabled="hasVisibleIncomeCategories" :text="hasVisibleIncomeCategories ? '' : tt('No secondary income categories are available')">
<template v-slot:activator="{ props }">
<div v-bind="props" class="d-block">
<two-column-select primary-key-field="id" primary-value-field="id" primary-title-field="name"
@@ -158,7 +158,7 @@
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
secondary-hidden-field="hidden"
:readonly="mode === TransactionEditPageMode.View"
:disabled="loading || submitting || !hasAvailableIncomeCategories"
:disabled="loading || submitting || !hasVisibleIncomeCategories"
:enable-filter="true" :filter-placeholder="tt('Find category')" :filter-no-items-text="tt('No available category')"
:show-selection-primary-text="true"
:custom-selection-primary-text="getTransactionPrimaryCategoryName(transaction.incomeCategoryId, allCategories[CategoryType.Income])"
@@ -172,7 +172,7 @@
</v-tooltip>
</v-col>
<v-col cols="12" md="12" v-if="transaction.type === TransactionType.Transfer">
<v-tooltip :disabled="hasAvailableTransferCategories" :text="hasAvailableTransferCategories ? '' : tt('No secondary transfer categories are available')">
<v-tooltip :disabled="hasVisibleTransferCategories" :text="hasVisibleTransferCategories ? '' : tt('No secondary transfer categories are available')">
<template v-slot:activator="{ props }">
<div v-bind="props" class="d-block">
<two-column-select primary-key-field="id" primary-value-field="id" primary-title-field="name"
@@ -182,7 +182,7 @@
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
secondary-hidden-field="hidden"
:readonly="mode === TransactionEditPageMode.View"
:disabled="loading || submitting || !hasAvailableTransferCategories"
:disabled="loading || submitting || !hasVisibleTransferCategories"
:enable-filter="true" :filter-placeholder="tt('Find category')" :filter-no-items-text="tt('No available category')"
:show-selection-primary-text="true"
:custom-selection-primary-text="getTransactionPrimaryCategoryName(transaction.transferCategoryId, allCategories[CategoryType.Transfer])"
@@ -621,9 +621,9 @@ const {
allTags,
allTagsMap,
firstVisibleAccountId,
hasAvailableExpenseCategories,
hasAvailableIncomeCategories,
hasAvailableTransferCategories,
hasVisibleExpenseCategories,
hasVisibleIncomeCategories,
hasVisibleTransferCategories,
canAddTransactionPicture,
title,
saveButtonTitle,
@@ -1,12 +1,22 @@
<template>
<f7-page @page:afterin="onPageAfterIn">
<f7-page with-subnavbar @page:beforein="onPageBeforeIn" @page:afterin="onPageAfterIn">
<f7-navbar>
<f7-nav-left :back-link="tt('Back')"></f7-nav-left>
<f7-nav-title :title="tt(title)"></f7-nav-title>
<f7-nav-right class="navbar-compact-icons">
<f7-link icon-f7="ellipsis" :class="{ 'disabled': !hasAnyAvailableCategory }" @click="showMoreActionSheet = true"></f7-link>
<f7-link icon-f7="checkmark_alt" :class="{ 'disabled': !hasAnyVisibleCategory }" @click="save"></f7-link>
<f7-link icon-f7="checkmark_alt" :class="{ 'disabled': !hasAnyAvailableCategory }" @click="save"></f7-link>
</f7-nav-right>
<f7-subnavbar :inner="false">
<f7-searchbar
custom-searchs
:value="filterContent"
:placeholder="tt('Find category')"
:disable-button-text="tt('Cancel')"
@change="filterContent = $event.target.value"
></f7-searchbar>
</f7-subnavbar>
</f7-navbar>
<div class="skeleton-text" v-if="loading">
@@ -49,38 +59,37 @@
</div>
<f7-block class="combination-list-wrapper margin-vertical"
:key="categoryType.type"
v-for="categoryType in allTransactionCategories"
:key="categoryType"
v-for="(categories, categoryType) in allVisibleTransactionCategories"
v-else-if="!loading">
<f7-accordion-item :opened="collapseStates[categoryType.type]!.opened"
@accordion:open="collapseStates[categoryType.type]!.opened = true"
@accordion:close="collapseStates[categoryType.type]!.opened = false">
<f7-accordion-item :opened="collapseStates[categoryType]!.opened"
@accordion:open="collapseStates[categoryType]!.opened = true"
@accordion:close="collapseStates[categoryType]!.opened = false">
<f7-block-title>
<f7-accordion-toggle>
<f7-list strong inset dividers
class="combination-list-header"
:class="collapseStates[categoryType.type]!.opened ? 'combination-list-opened' : 'combination-list-closed'">
:class="collapseStates[categoryType]!.opened ? 'combination-list-opened' : 'combination-list-closed'">
<f7-list-item group-title>
<small>{{ getCategoryTypeName(categoryType.type) }}</small>
<f7-icon class="combination-list-chevron-icon" :f7="collapseStates[categoryType.type]!.opened ? 'chevron_up' : 'chevron_down'"></f7-icon>
<small>{{ getCategoryTypeName(parseInt(categoryType)) }}</small>
<f7-icon class="combination-list-chevron-icon" :f7="collapseStates[categoryType]!.opened ? 'chevron_up' : 'chevron_down'"></f7-icon>
</f7-list-item>
</f7-list>
</f7-accordion-toggle>
</f7-block-title>
<f7-accordion-content :style="{ height: collapseStates[categoryType.type]!.opened ? 'auto' : '' }">
<f7-list strong inset dividers accordion-list class="combination-list-content" v-if="!hasAvailableCategory[categoryType.type]">
<f7-accordion-content :style="{ height: collapseStates[categoryType]!.opened ? 'auto' : '' }">
<f7-list strong inset dividers accordion-list class="combination-list-content" v-if="!categories || !categories.length">
<f7-list-item :title="tt('No available category')"></f7-list-item>
</f7-list>
<f7-list strong inset dividers accordion-list class="combination-list-content" v-else-if="hasAvailableCategory[categoryType.type]">
<f7-list strong inset dividers accordion-list class="combination-list-content" v-else-if="categories && categories.length">
<f7-list-item checkbox
:class="{ 'has-child-list-item': (showHidden && categoryType.allSubCategories[category.id]) || categoryType.allVisibleSubCategoryCounts[category.id] }"
:class="{ 'has-child-list-item': category.subCategories && category.subCategories.length > 0 }"
:title="category.name"
:value="category.id"
:checked="isSubCategoriesAllChecked(category, filterCategoryIds)"
:indeterminate="isSubCategoriesHasButNotAllChecked(category, filterCategoryIds)"
:key="category.id"
v-for="category in categoryType.allCategories"
v-show="showHidden || !category.hidden"
v-for="category in categories"
@change="updateAllSubCategoriesSelected">
<template #media>
<ItemIcon icon-type="category" :icon-id="category.icon" :color="category.color">
@@ -92,14 +101,13 @@
<template #root>
<ul class="padding-inline-start"
v-if="(showHidden && categoryType.allSubCategories[category.id]) || categoryType.allVisibleSubCategoryCounts[category.id]">
v-if="category.subCategories && category.subCategories.length > 0">
<f7-list-item checkbox
:title="subCategory.name"
:value="subCategory.id"
:checked="isCategoryChecked(subCategory, filterCategoryIds)"
:key="subCategory.id"
v-for="subCategory in categoryType.allSubCategories[category.id]"
v-show="showHidden || !subCategory.hidden"
v-for="subCategory in category.subCategories"
@change="updateCategorySelected">
<template #media>
<ItemIcon icon-type="category" :icon-id="subCategory.icon" :color="subCategory.color">
@@ -123,9 +131,6 @@
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleCategory }" @click="selectNoneCategories">{{ tt('Select None') }}</f7-actions-button>
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleCategory }" @click="selectInvertCategories">{{ tt('Invert Selection') }}</f7-actions-button>
</f7-actions-group>
<f7-actions-group>
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleCategory }" @click="selectAllVisibleCategories">{{ tt('Select All Visible') }}</f7-actions-button>
</f7-actions-group>
<f7-actions-group>
<f7-actions-button v-if="!showHidden" @click="showHidden = true">{{ tt('Show Hidden Transaction Categories') }}</f7-actions-button>
<f7-actions-button v-if="showHidden" @click="showHidden = false">{{ tt('Hide Hidden Transaction Categories') }}</f7-actions-button>
@@ -154,7 +159,6 @@ import { CategoryType } from '@/core/category.ts';
import {
selectAllSubCategories,
selectAllVisible,
selectAll,
selectNone,
selectInvert,
@@ -179,12 +183,13 @@ const { showToast, routeBackOnError } = useI18nUIComponents();
const {
loading,
showHidden,
filterContent,
filterCategoryIds,
title,
allTransactionCategories,
allVisibleTransactionCategories,
allVisibleTransactionCategoryMap,
hasAnyAvailableCategory,
hasAnyVisibleCategory,
hasAvailableCategory,
isCategoryChecked,
getCategoryTypeName,
loadFilterCategoryIds,
@@ -196,14 +201,14 @@ const transactionCategoriesStore = useTransactionCategoriesStore();
const loadingError = ref<unknown | null>(null);
const showMoreActionSheet = ref<boolean>(false);
const collapseStates = ref<Record<number, CollapseState>>({
[CategoryType.Income]: {
const collapseStates = ref<Record<string, CollapseState>>({
[CategoryType.Income.toString()]: {
opened: true
},
[CategoryType.Expense]: {
[CategoryType.Expense.toString()]: {
opened: true
},
[CategoryType.Transfer]: {
[CategoryType.Transfer.toString()]: {
opened: true
}
});
@@ -231,7 +236,7 @@ function init(): void {
function updateCategorySelected(e: Event): void {
const target = e.target as HTMLInputElement;
const categoryId = target.value;
const category = transactionCategoriesStore.allTransactionCategoriesMap[categoryId];
const category = allVisibleTransactionCategoryMap.value[categoryId];
if (!category) {
return;
@@ -243,25 +248,21 @@ function updateCategorySelected(e: Event): void {
function updateAllSubCategoriesSelected(e: Event): void {
const target = e.target as HTMLInputElement;
const categoryId = target.value;
const category = transactionCategoriesStore.allTransactionCategoriesMap[categoryId];
const category = allVisibleTransactionCategoryMap.value[categoryId];
selectAllSubCategories(filterCategoryIds.value, !target.checked, category);
}
function selectAllCategories(): void {
selectAll(filterCategoryIds.value, transactionCategoriesStore.allTransactionCategoriesMap);
selectAll(filterCategoryIds.value, allVisibleTransactionCategoryMap.value);
}
function selectNoneCategories(): void {
selectNone(filterCategoryIds.value, transactionCategoriesStore.allTransactionCategoriesMap);
selectNone(filterCategoryIds.value, allVisibleTransactionCategoryMap.value);
}
function selectInvertCategories(): void {
selectInvert(filterCategoryIds.value, transactionCategoriesStore.allTransactionCategoriesMap);
}
function selectAllVisibleCategories(): void {
selectAllVisible(filterCategoryIds.value, transactionCategoriesStore.allTransactionCategoriesMap);
selectInvert(filterCategoryIds.value, allVisibleTransactionCategoryMap.value);
}
function save(): void {
@@ -269,6 +270,10 @@ function save(): void {
props.f7router.back();
}
function onPageBeforeIn(): void {
filterContent.value = '';
}
function onPageAfterIn(): void {
routeBackOnError(props.f7router, loadingError);
}
+12 -12
View File
@@ -97,18 +97,18 @@
class="list-item-with-header-and-title list-item-title-hide-overflow"
key="expenseCategorySelection"
link="#" no-chevron
:class="{ 'disabled': !hasAvailableExpenseCategories, 'readonly': mode === TransactionEditPageMode.View }"
:class="{ 'disabled': !hasVisibleExpenseCategories, 'readonly': mode === TransactionEditPageMode.View }"
:header="tt('Category')"
@click="showCategorySheet = true"
v-if="transaction.type === TransactionType.Expense"
>
<template #title>
<div class="list-item-custom-title" v-if="hasAvailableExpenseCategories">
<div class="list-item-custom-title" v-if="hasVisibleExpenseCategories">
<span>{{ getTransactionPrimaryCategoryName(transaction.expenseCategoryId, allCategories[CategoryType.Expense]) }}</span>
<f7-icon class="category-separate-icon icon-with-direction" f7="chevron_right"></f7-icon>
<span>{{ getTransactionSecondaryCategoryName(transaction.expenseCategoryId, allCategories[CategoryType.Expense]) }}</span>
</div>
<div class="list-item-custom-title" v-else-if="!hasAvailableExpenseCategories">
<div class="list-item-custom-title" v-else-if="!hasVisibleExpenseCategories">
<span>{{ tt('None') }}</span>
</div>
</template>
@@ -129,18 +129,18 @@
class="list-item-with-header-and-title list-item-title-hide-overflow"
key="incomeCategorySelection"
link="#" no-chevron
:class="{ 'disabled': !hasAvailableIncomeCategories, 'readonly': mode === TransactionEditPageMode.View }"
:class="{ 'disabled': !hasVisibleIncomeCategories, 'readonly': mode === TransactionEditPageMode.View }"
:header="tt('Category')"
@click="showCategorySheet = true"
v-if="transaction.type === TransactionType.Income"
>
<template #title>
<div class="list-item-custom-title" v-if="hasAvailableIncomeCategories">
<div class="list-item-custom-title" v-if="hasVisibleIncomeCategories">
<span>{{ getTransactionPrimaryCategoryName(transaction.incomeCategoryId, allCategories[CategoryType.Income]) }}</span>
<f7-icon class="category-separate-icon icon-with-direction" f7="chevron_right"></f7-icon>
<span>{{ getTransactionSecondaryCategoryName(transaction.incomeCategoryId, allCategories[CategoryType.Income]) }}</span>
</div>
<div class="list-item-custom-title" v-else-if="!hasAvailableIncomeCategories">
<div class="list-item-custom-title" v-else-if="!hasVisibleIncomeCategories">
<span>{{ tt('None') }}</span>
</div>
</template>
@@ -161,18 +161,18 @@
class="list-item-with-header-and-title list-item-title-hide-overflow"
key="transferCategorySelection"
link="#" no-chevron
:class="{ 'disabled': !hasAvailableTransferCategories, 'readonly': mode === TransactionEditPageMode.View }"
:class="{ 'disabled': !hasVisibleTransferCategories, 'readonly': mode === TransactionEditPageMode.View }"
:header="tt('Category')"
@click="showCategorySheet = true"
v-if="transaction.type === TransactionType.Transfer"
>
<template #title>
<div class="list-item-custom-title" v-if="hasAvailableTransferCategories">
<div class="list-item-custom-title" v-if="hasVisibleTransferCategories">
<span>{{ getTransactionPrimaryCategoryName(transaction.transferCategoryId, allCategories[CategoryType.Transfer]) }}</span>
<f7-icon class="category-separate-icon icon-with-direction" f7="chevron_right"></f7-icon>
<span>{{ getTransactionSecondaryCategoryName(transaction.transferCategoryId, allCategories[CategoryType.Transfer]) }}</span>
</div>
<div class="list-item-custom-title" v-else-if="!hasAvailableTransferCategories">
<div class="list-item-custom-title" v-else-if="!hasVisibleTransferCategories">
<span>{{ tt('None') }}</span>
</div>
</template>
@@ -571,9 +571,9 @@ const {
allTags,
allTagsMap,
firstVisibleAccountId,
hasAvailableExpenseCategories,
hasAvailableIncomeCategories,
hasAvailableTransferCategories,
hasVisibleExpenseCategories,
hasVisibleIncomeCategories,
hasVisibleTransferCategories,
canAddTransactionPicture,
title,
saveButtonTitle,