add search box in transaction category page / dialog
This commit is contained in:
+33
-73
@@ -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;
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user