support filtering transactions by amount in import transaction dialog

This commit is contained in:
MaysWind
2025-11-27 01:25:34 +08:00
parent 3fe51dce63
commit 17f604b6aa
2 changed files with 162 additions and 8 deletions

View File

@@ -440,26 +440,94 @@ export class AmountFilterType {
private static readonly allInstances: AmountFilterType[] = [];
private static readonly allInstancesByType: Record<string, AmountFilterType> = {};
public static readonly GreaterThan = new AmountFilterType('gt', 'Greater than', 1);
public static readonly LessThan = new AmountFilterType('lt', 'Less than', 1);
public static readonly EqualTo = new AmountFilterType('eq', 'Equal to', 1);
public static readonly NotEqualTo = new AmountFilterType('ne', 'Not equal to', 1);
public static readonly Between = new AmountFilterType('bt', 'Between', 2);
public static readonly NotBetween = new AmountFilterType('nb', 'Not between', 2);
public static readonly GreaterThan = new AmountFilterType('gt', 'Greater than', 1,
(amount: number, ...params: number[]) => {
return params && params.length > 0 && amount > (params[0] as number);
}
);
public static readonly LessThan = new AmountFilterType('lt', 'Less than', 1,
(amount: number, ...params: number[]) => {
return params && params.length > 0 && amount < (params[0] as number);
}
);
public static readonly EqualTo = new AmountFilterType('eq', 'Equal to', 1,
(amount: number, ...params: number[]) => {
return params && params.length > 0 && amount === (params[0] as number);
}
);
public static readonly NotEqualTo = new AmountFilterType('ne', 'Not equal to', 1,
(amount: number, ...params: number[]) => {
return params && params.length > 0 && amount !== (params[0] as number);
}
);
public static readonly Between = new AmountFilterType('bt', 'Between', 2,
(amount: number, ...params: number[]) => {
return params && params.length > 1 && amount >= (params[0] as number) && amount <= (params[1] as number);
}
);
public static readonly NotBetween = new AmountFilterType('nb', 'Not between', 2,
(amount: number, ...params: number[]) => {
return params && params.length > 1 && (amount < (params[0] as number) || amount > (params[1] as number));
}
);
public readonly type: string;
public readonly name: string;
public readonly paramCount: number;
private readonly matchFn: (amount: number, ...params: number[]) => boolean;
private constructor(type: string, name: string, paramCount: number) {
private constructor(type: string, name: string, paramCount: number, matchFn: (amount: number, ...params: number[]) => boolean) {
this.type = type;
this.name = name;
this.paramCount = paramCount;
this.matchFn = matchFn;
AmountFilterType.allInstances.push(this);
AmountFilterType.allInstancesByType[type] = this;
}
public toTextualFilter(...params: number[]): string {
if (this.paramCount === 1) {
return `${this.type}:${params[0] ?? ''}`;
} else if (this.paramCount === 2) {
return `${this.type}:${params[0] ?? ''}:${params[1] ?? ''}`;
} else {
return '';
}
}
public static match(filter: string, amount: number): boolean {
const parts = filter.split(':');
if (parts.length < 2) {
return false;
}
const filterType = AmountFilterType.valueOf(parts[0] as string);
if (!filterType) {
return false;
}
if (parts.length - 1 !== filterType.paramCount) {
return false;
}
const params: number[] = [];
for (let i = 1; i < parts.length; i++) {
const param = parseInt(parts[i] as string);
if (Number.isNaN(param)) {
return false;
}
params.push(param);
}
return filterType.matchFn(amount, ...params);
}
public static values(): AmountFilterType[] {
return AmountFilterType.allInstances;
}

View File

@@ -327,6 +327,35 @@
</template>
</v-data-table>
<v-dialog width="640" v-model="showCustomAmountFilterDialog">
<v-card class="pa-2 pa-sm-4 pa-md-4">
<template #title>
<div class="d-flex align-center justify-center">
<h4 class="text-h4">{{ tt('Filter Amount') }}</h4>
</div>
</template>
<v-card-text class="mb-md-4 w-100 d-flex justify-center">
<div class="ms-2 me-2 d-flex flex-column justify-center" v-if="currentAmountFilterType">
{{ tt(currentAmountFilterType.name) }}
</div>
<amount-input :currency="defaultCurrency"
v-model="currentAmountFilterValue1"/>
<div class="ms-2 me-2 d-flex flex-column justify-center" v-if="currentAmountFilterType && currentAmountFilterType.paramCount === 2">
~
</div>
<amount-input :currency="defaultCurrency"
v-model="currentAmountFilterValue2"
v-if="currentAmountFilterType && currentAmountFilterType.paramCount === 2"/>
</v-card-text>
<v-card-text class="overflow-y-visible">
<div class="w-100 d-flex justify-center gap-4">
<v-btn @click="showCustomAmountFilterDialog = false; filters.amount = currentAmountFilterType?.toTextualFilter(currentAmountFilterValue1, currentAmountFilterValue2) ?? null">{{ tt('OK') }}</v-btn>
<v-btn color="secondary" variant="tonal" @click="showCustomAmountFilterDialog = false">{{ tt('Cancel') }}</v-btn>
</div>
</v-card-text>
</v-card>
</v-dialog>
<v-dialog width="640" v-model="showCustomDescriptionDialog">
<v-card class="pa-2 pa-sm-4 pa-md-4">
<template #title>
@@ -382,7 +411,7 @@ import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts';
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
import { type NameValue, type NameNumeralValue, itemAndIndex, reversed, keys } from '@/core/base.ts';
import { type NumeralSystem } from '@/core/numeral.ts';
import { type NumeralSystem, AmountFilterType } from '@/core/numeral.ts';
import { CategoryType } from '@/core/category.ts';
import { TransactionType } from '@/core/transaction.ts';
@@ -435,6 +464,7 @@ interface ImportTransactionCheckDataFilter {
maxDatetime: number | null;
transactionType: TransactionType | null; // null for 'All Transaction Type'
category: string | null | undefined; // null for 'All Category', undefined for 'Invalid Category'
amount: string | null; // null for 'All Amount'
account: string | null | undefined; // null for 'All Account', undefined for 'Invalid Account'
tag: string | null | undefined; // null for 'All Tag', undefined for 'Invalid Tag'
description: string | null; // null for 'All Description'
@@ -486,6 +516,7 @@ const filters = ref<ImportTransactionCheckDataFilter>({
maxDatetime: null,
transactionType: null,
category: null,
amount: null,
account: null,
tag: null,
description: null
@@ -494,7 +525,11 @@ const filters = ref<ImportTransactionCheckDataFilter>({
const currentPage = ref<number>(1);
const countPerPage = ref<number>(10);
const showCustomDateRangeDialog = ref<boolean>(false);
const showCustomAmountFilterDialog = ref<boolean>(false);
const showCustomDescriptionDialog = ref<boolean>(false);
const currentAmountFilterType = ref<AmountFilterType | null>(null);
const currentAmountFilterValue1 = ref<number>(0);
const currentAmountFilterValue2 = ref<number>(0);
const currentDescriptionFilterValue = ref<string | null>(null);
const numeralSystem = computed<NumeralSystem>(() => getCurrentNumeralSystemType());
@@ -591,6 +626,49 @@ const filterMenus = computed<ImportTransactionCheckDataMenuGroup[]>(() => [
}))
]
},
{
title: tt('Amount'),
items: [
{
title: tt('All'),
appendIcon: !filters.value.amount ? mdiCheck : undefined,
onClick: () => filters.value.amount = null
},
...AmountFilterType.values().map(filterType => ({
title: tt(filterType.name),
appendIcon: filters.value.amount && filters.value.amount.startsWith(`${filterType.type}:`) ? mdiCheck : undefined,
onClick: () => {
let filterValue1: number = 0;
let filterValue2: number = 0;
if (filters.value.amount) {
const parts = filters.value.amount.split(':');
if (parts.length >= 2) {
filterValue1 = parseInt(parts[1] as string);
}
if (parts.length >= 3) {
filterValue2 = parseInt(parts[2] as string);
}
}
if (Number.isNaN(filterValue1) || !Number.isFinite(filterValue1)) {
filterValue1 = 0;
}
if (Number.isNaN(filterValue2) || !Number.isFinite(filterValue2)) {
filterValue2 = 0;
}
currentAmountFilterType.value = filterType;
currentAmountFilterValue1.value = filterValue1;
currentAmountFilterValue2.value = filterValue2;
showCustomAmountFilterDialog.value = true;
}
}))
]
},
{
title: tt('Account'),
items: [
@@ -1088,6 +1166,14 @@ function isTransactionDisplayed(transaction: ImportTransaction): boolean {
}
}
if (isString(filters.value.amount)) {
const match: boolean = AmountFilterType.match(filters.value.amount, transaction.sourceAmount);
if (!match) {
return false;
}
}
if (isString(filters.value.account)) {
if (filters.value.account === '' && (transaction.actualSourceAccountName !== '' || transaction.actualDestinationAccountName !== '')) {
return false;