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
@@ -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,