From 448fc760c088bca7d72d488d07443b05040de539 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Sat, 11 Apr 2026 16:49:11 +0800 Subject: [PATCH] support not in options for transaction type, transaction category and account filters --- src/core/explorer.ts | 2 + src/locales/de.json | 1 + src/locales/en.json | 1 + src/locales/es.json | 1 + src/locales/fr.json | 1 + src/locales/it.json | 1 + src/locales/ja.json | 1 + src/locales/kn.json | 1 + src/locales/ko.json | 1 + src/locales/nl.json | 1 + src/locales/pt_BR.json | 1 + src/locales/ru.json | 1 + src/locales/sl.json | 1 + src/locales/ta.json | 1 + src/locales/th.json | 1 + src/locales/tr.json | 1 + src/locales/uk.json | 1 + src/locales/vi.json | 1 + src/locales/zh_Hans.json | 1 + src/locales/zh_Hant.json | 1 + src/models/explorer.ts | 135 +++++++++++++++++++++++++++++---------- 21 files changed, 123 insertions(+), 33 deletions(-) diff --git a/src/core/explorer.ts b/src/core/explorer.ts index 2153f126..3085a39a 100644 --- a/src/core/explorer.ts +++ b/src/core/explorer.ts @@ -74,6 +74,7 @@ export class TransactionExplorerConditionField implements NameValue { export enum TransactionExplorerConditionOperatorType { In = 'in', + NotIn = 'notIn', GreaterThan = 'greaterThan', LessThan = 'lessThan', Equals = 'equals', @@ -103,6 +104,7 @@ export class TransactionExplorerConditionOperator implements NameValue { private static readonly allInstancesByValue: Record = {}; public static readonly In = new TransactionExplorerConditionOperator('In', TransactionExplorerConditionOperatorType.In); + public static readonly NotIn = new TransactionExplorerConditionOperator('Not in', TransactionExplorerConditionOperatorType.NotIn); public static readonly GreaterThan = new TransactionExplorerConditionOperator('Greater than', TransactionExplorerConditionOperatorType.GreaterThan); public static readonly LessThan = new TransactionExplorerConditionOperator('Less than', TransactionExplorerConditionOperatorType.LessThan); public static readonly Equals = new TransactionExplorerConditionOperator('Equal to', TransactionExplorerConditionOperatorType.Equals); diff --git a/src/locales/de.json b/src/locales/de.json index 7ef0bd08..54602333 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -1558,6 +1558,7 @@ "Between": "Zwischen", "Not between": "Nicht zwischen", "In": "In", + "Not in": "Not in", "Has any": "Hat eines", "Has all": "Hat alle", "Not has any": "Hat keines", diff --git a/src/locales/en.json b/src/locales/en.json index 49940fdd..b43d431a 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1558,6 +1558,7 @@ "Between": "Between", "Not between": "Not between", "In": "In", + "Not in": "Not in", "Has any": "Has any", "Has all": "Has all", "Not has any": "Not has any", diff --git a/src/locales/es.json b/src/locales/es.json index b1fbfc4c..6a3e85f4 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -1558,6 +1558,7 @@ "Between": "Entre", "Not between": "No entre", "In": "En", + "Not in": "Not in", "Has any": "Tiene cualquiera", "Has all": "Tiene todas", "Not has any": "No tiene cualquiera", diff --git a/src/locales/fr.json b/src/locales/fr.json index 1c6cb84c..74d090e1 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -1558,6 +1558,7 @@ "Between": "Entre", "Not between": "Pas entre", "In": "In", + "Not in": "Not in", "Has any": "Has any", "Has all": "Has all", "Not has any": "Not has any", diff --git a/src/locales/it.json b/src/locales/it.json index 25e96f15..79426f7e 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -1558,6 +1558,7 @@ "Between": "Tra", "Not between": "Non tra", "In": "In", + "Not in": "Not in", "Has any": "Has any", "Has all": "Has all", "Not has any": "Not has any", diff --git a/src/locales/ja.json b/src/locales/ja.json index d5ea9dd5..289133a0 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -1558,6 +1558,7 @@ "Between": "間", "Not between": "間ではない", "In": "In", + "Not in": "Not in", "Has any": "Has any", "Has all": "Has all", "Not has any": "Not has any", diff --git a/src/locales/kn.json b/src/locales/kn.json index 9c070f5c..98e61177 100644 --- a/src/locales/kn.json +++ b/src/locales/kn.json @@ -1558,6 +1558,7 @@ "Between": "ನಡುವೆ", "Not between": "ನಡುವಲ್ಲ", "In": "In", + "Not in": "Not in", "Has any": "Has any", "Has all": "Has all", "Not has any": "Not has any", diff --git a/src/locales/ko.json b/src/locales/ko.json index 2cc4b1c9..d1b0256a 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -1558,6 +1558,7 @@ "Between": "사이", "Not between": "사이 아님", "In": "In", + "Not in": "Not in", "Has any": "Has any", "Has all": "Has all", "Not has any": "Not has any", diff --git a/src/locales/nl.json b/src/locales/nl.json index 8bbc0189..8247c7d9 100644 --- a/src/locales/nl.json +++ b/src/locales/nl.json @@ -1558,6 +1558,7 @@ "Between": "Tussen", "Not between": "Niet tussen", "In": "In", + "Not in": "Not in", "Has any": "Has any", "Has all": "Has all", "Not has any": "Not has any", diff --git a/src/locales/pt_BR.json b/src/locales/pt_BR.json index 0262c98c..b043d85d 100644 --- a/src/locales/pt_BR.json +++ b/src/locales/pt_BR.json @@ -1558,6 +1558,7 @@ "Between": "Entre", "Not between": "Não entre", "In": "Em", + "Not in": "Not in", "Has any": "Contém algum", "Has all": "Contém todos", "Not has any": "Não contém nenhum", diff --git a/src/locales/ru.json b/src/locales/ru.json index 7165b8ae..1f08266c 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1558,6 +1558,7 @@ "Between": "Между", "Not between": "Не между", "In": "В", + "Not in": "Not in", "Has any": "Содержит любой", "Has all": "Содержит все", "Not has any": "Не содержит любой", diff --git a/src/locales/sl.json b/src/locales/sl.json index d0dbee99..c66f4432 100644 --- a/src/locales/sl.json +++ b/src/locales/sl.json @@ -1558,6 +1558,7 @@ "Between": "Med", "Not between": "Ni med", "In": "V", + "Not in": "Not in", "Has any": "Vsebuje katerokoli", "Has all": "Vsebuje vse", "Not has any": "Ne vsebuje nobene", diff --git a/src/locales/ta.json b/src/locales/ta.json index af5e1c58..4ffd678f 100644 --- a/src/locales/ta.json +++ b/src/locales/ta.json @@ -1558,6 +1558,7 @@ "Between": "இடையே", "Not between": "இடையில் இல்லை", "In": "In", + "Not in": "Not in", "Has any": "ஏதேனும் உள்ளது", "Has all": "அனைத்தும் உள்ளது", "Not has any": "ஏதும் இல்லை", diff --git a/src/locales/th.json b/src/locales/th.json index 6bc530d2..e05aba5a 100644 --- a/src/locales/th.json +++ b/src/locales/th.json @@ -1558,6 +1558,7 @@ "Between": "ระหว่าง", "Not between": "ไม่อยู่ระหว่าง", "In": "In", + "Not in": "Not in", "Has any": "Has any", "Has all": "Has all", "Not has any": "Not has any", diff --git a/src/locales/tr.json b/src/locales/tr.json index f6bbff2f..345455a4 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -1558,6 +1558,7 @@ "Between": "Arasında", "Not between": "Arasında değil", "In": "In", + "Not in": "Not in", "Has any": "Has any", "Has all": "Has all", "Not has any": "Not has any", diff --git a/src/locales/uk.json b/src/locales/uk.json index fe2325e6..500e8b6b 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -1558,6 +1558,7 @@ "Between": "Між", "Not between": "Не між", "In": "In", + "Not in": "Not in", "Has any": "Has any", "Has all": "Has all", "Not has any": "Not has any", diff --git a/src/locales/vi.json b/src/locales/vi.json index 9106b0a2..2e36240b 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -1558,6 +1558,7 @@ "Between": "Giữa", "Not between": "Không giữa", "In": "In", + "Not in": "Not in", "Has any": "Has any", "Has all": "Has all", "Not has any": "Not has any", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index 133641e4..cd3a7b37 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -1558,6 +1558,7 @@ "Between": "介于", "Not between": "不介于", "In": "在范围内", + "Not in": "不在范围内", "Has any": "包含任意", "Has all": "包含全部", "Not has any": "不包含任意", diff --git a/src/locales/zh_Hant.json b/src/locales/zh_Hant.json index 2b299c42..17c00257 100644 --- a/src/locales/zh_Hant.json +++ b/src/locales/zh_Hant.json @@ -1558,6 +1558,7 @@ "Between": "介於", "Not between": "不介於", "In": "包含於", + "Not in": "不包含於", "Has any": "包含任一", "Has all": "包含所有", "Not has any": "不包含任一", diff --git a/src/models/explorer.ts b/src/models/explorer.ts index 78c30c5a..af2a867d 100644 --- a/src/models/explorer.ts +++ b/src/models/explorer.ts @@ -301,16 +301,16 @@ export class TransactionExplorerQuery { switch (field) { case TransactionExplorerConditionField.TransactionType: - condition = new TransactionExplorerTransactionTypeCondition([ TransactionType.Expense, TransactionType.Income, TransactionType.Transfer ]); + condition = new TransactionExplorerTransactionTypeCondition(TransactionExplorerConditionOperatorType.In, [ TransactionType.Expense, TransactionType.Income, TransactionType.Transfer ]); break; case TransactionExplorerConditionField.TransactionCategory: - condition = new TransactionExplorerTransactionCategoryCondition([]); + condition = new TransactionExplorerTransactionCategoryCondition(TransactionExplorerConditionOperatorType.In, []); break; case TransactionExplorerConditionField.SourceAccount: - condition = new TransactionExplorerSourceAccountCondition([]); + condition = new TransactionExplorerSourceAccountCondition(TransactionExplorerConditionOperatorType.In, []); break; case TransactionExplorerConditionField.DestinationAccount: - condition = new TransactionExplorerDestinationAccountCondition([]); + condition = new TransactionExplorerDestinationAccountCondition(TransactionExplorerConditionOperatorType.In, []); break; case TransactionExplorerConditionField.SourceAmount: condition = new TransactionExplorerSourceAmountCondition(TransactionExplorerConditionOperatorType.Between, [0, 0]); @@ -331,7 +331,7 @@ export class TransactionExplorerQuery { condition = new TransactionExplorerDescriptionCondition(TransactionExplorerConditionOperatorType.Contains, ''); break; default: - condition = new TransactionExplorerTransactionTypeCondition([ TransactionType.Expense, TransactionType.Income, TransactionType.Transfer ]); + condition = new TransactionExplorerTransactionTypeCondition(TransactionExplorerConditionOperatorType.In, [ TransactionType.Expense, TransactionType.Income, TransactionType.Transfer ]); break; } @@ -737,23 +737,23 @@ export class TransactionExplorerConditionWithRelation { switch (conditionField) { case TransactionExplorerConditionField.TransactionType.value: - if (conditionOperator === TransactionExplorerConditionOperatorType.In && Array.isArray(conditionValue)) { - condition = new TransactionExplorerTransactionTypeCondition(conditionValue as number[]); + if (TransactionExplorerTransactionTypeCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue)) { + condition = new TransactionExplorerTransactionTypeCondition(conditionOperator as TransactionTypeConditionOperator, conditionValue as number[]); } break; case TransactionExplorerConditionField.TransactionCategory.value: - if (conditionOperator === TransactionExplorerConditionOperatorType.In && Array.isArray(conditionValue)) { - condition = new TransactionExplorerTransactionCategoryCondition(conditionValue as string[]); + if (TransactionExplorerTransactionCategoryCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue)) { + condition = new TransactionExplorerTransactionCategoryCondition(conditionOperator as TransactionCategoryConditionOperator, conditionValue as string[]); } break; case TransactionExplorerConditionField.SourceAccount.value: - if (conditionOperator === TransactionExplorerConditionOperatorType.In && Array.isArray(conditionValue)) { - condition = new TransactionExplorerSourceAccountCondition(conditionValue as string[]); + if (TransactionExplorerSourceAccountCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue)) { + condition = new TransactionExplorerSourceAccountCondition(conditionOperator as AccountConditionOperator, conditionValue as string[]); } break; case TransactionExplorerConditionField.DestinationAccount.value: - if (conditionOperator === TransactionExplorerConditionOperatorType.In && Array.isArray(conditionValue)) { - condition = new TransactionExplorerDestinationAccountCondition(conditionValue as string[]); + if (TransactionExplorerDestinationAccountCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue)) { + condition = new TransactionExplorerDestinationAccountCondition(conditionOperator as AccountConditionOperator, conditionValue as string[]); } break; case TransactionExplorerConditionField.SourceAmount.value: @@ -831,15 +831,20 @@ export class TransactionExplorerUndefinedCondition implements TransactionExplore } } +type TransactionTypeConditionOperator = TransactionExplorerConditionOperatorType.In | + TransactionExplorerConditionOperatorType.NotIn; + export class TransactionExplorerTransactionTypeCondition implements TransactionExplorerCondition { public static readonly supportedOperators: PartialRecord = { - [TransactionExplorerConditionOperatorType.In]: true + [TransactionExplorerConditionOperatorType.In]: true, + [TransactionExplorerConditionOperatorType.NotIn]: true }; public readonly field = TransactionExplorerConditionFieldType.TransactionType; - public readonly operator: TransactionExplorerConditionOperatorType.In = TransactionExplorerConditionOperatorType.In; + public readonly operator: TransactionTypeConditionOperator = TransactionExplorerConditionOperatorType.In; public value: number[]; - constructor(value: number[]) { + constructor(operator: TransactionTypeConditionOperator, value: number[]) { + this.operator = operator; this.value = value; } @@ -848,7 +853,13 @@ export class TransactionExplorerTransactionTypeCondition implements TransactionE } public match(transaction: TransactionInsightDataItem): boolean { - return this.value.includes(transaction.type); + if (this.operator === TransactionExplorerConditionOperatorType.In) { + return this.value.includes(transaction.type); + } else if (this.operator === TransactionExplorerConditionOperatorType.NotIn) { + return !this.value.includes(transaction.type); + } + + return false; } public toExpression(): string { @@ -863,19 +874,31 @@ export class TransactionExplorerTransactionTypeCondition implements TransactionE return type.toString(); } }).join(', '); - return `type IN (${textualTypes})`; + + if (this.operator === TransactionExplorerConditionOperatorType.In) { + return `type IN (${textualTypes})`; + } else if (this.operator === TransactionExplorerConditionOperatorType.NotIn) { + return `type NOT IN (${textualTypes})`; + } else { + return ''; + } } } +type TransactionCategoryConditionOperator = TransactionExplorerConditionOperatorType.In | + TransactionExplorerConditionOperatorType.NotIn; + export class TransactionExplorerTransactionCategoryCondition implements TransactionExplorerCondition { public static readonly supportedOperators: PartialRecord = { - [TransactionExplorerConditionOperatorType.In]: true + [TransactionExplorerConditionOperatorType.In]: true, + [TransactionExplorerConditionOperatorType.NotIn]: true }; public readonly field = TransactionExplorerConditionFieldType.TransactionCategory; - public readonly operator: TransactionExplorerConditionOperatorType.In = TransactionExplorerConditionOperatorType.In; + public readonly operator: TransactionCategoryConditionOperator = TransactionExplorerConditionOperatorType.In; public value: string[]; - constructor(value: string[]) { + constructor(operator: TransactionCategoryConditionOperator, value: string[]) { + this.operator = operator this.value = value; } @@ -884,7 +907,13 @@ export class TransactionExplorerTransactionCategoryCondition implements Transact } public match(transaction: TransactionInsightDataItem): boolean { - return this.value.includes(transaction.primaryCategory?.id ?? '') || this.value.includes(transaction.secondaryCategory?.id ?? transaction.categoryId); + if (this.operator === TransactionExplorerConditionOperatorType.In) { + return this.value.includes(transaction.primaryCategory?.id ?? '') || this.value.includes(transaction.secondaryCategory?.id ?? transaction.categoryId); + } else if (this.operator === TransactionExplorerConditionOperatorType.NotIn) { + return !this.value.includes(transaction.primaryCategory?.id ?? '') && !this.value.includes(transaction.secondaryCategory?.id ?? transaction.categoryId); + } + + return false; } public toExpression(allCategoriesMap: Record): string { @@ -901,19 +930,31 @@ export class TransactionExplorerTransactionCategoryCondition implements Transact return `'${id}'`; } }).filter(item => !!item).join(', '); - return `category IN (${textualCategories})`; + + if (this.operator === TransactionExplorerConditionOperatorType.In) { + return `category IN (${textualCategories})`; + } else if (this.operator === TransactionExplorerConditionOperatorType.NotIn) { + return `category NOT IN (${textualCategories})`; + } else { + return ''; + } } } +type AccountConditionOperator = TransactionExplorerConditionOperatorType.In | + TransactionExplorerConditionOperatorType.NotIn; + export class TransactionExplorerSourceAccountCondition implements TransactionExplorerCondition { public static readonly supportedOperators: PartialRecord = { - [TransactionExplorerConditionOperatorType.In]: true + [TransactionExplorerConditionOperatorType.In]: true, + [TransactionExplorerConditionOperatorType.NotIn]: true }; public readonly field = TransactionExplorerConditionFieldType.SourceAccount; - public readonly operator: TransactionExplorerConditionOperatorType.In = TransactionExplorerConditionOperatorType.In; + public readonly operator: AccountConditionOperator = TransactionExplorerConditionOperatorType.In; public value: string[]; - constructor(value: string[]) { + constructor(operator: AccountConditionOperator, value: string[]) { + this.operator = operator; this.value = value; } @@ -922,7 +963,13 @@ export class TransactionExplorerSourceAccountCondition implements TransactionExp } public match(transaction: TransactionInsightDataItem): boolean { - return this.value.includes(transaction.sourceAccountId); + if (this.operator === TransactionExplorerConditionOperatorType.In) { + return this.value.includes(transaction.sourceAccountId); + } else if (this.operator === TransactionExplorerConditionOperatorType.NotIn) { + return !this.value.includes(transaction.sourceAccountId); + } + + return false; } public toExpression(allCategoriesMap: Record, allAccountsMap: Record): string { @@ -939,19 +986,28 @@ export class TransactionExplorerSourceAccountCondition implements TransactionExp return `'${id}'`; } }).filter(item => !!item).join(', '); - return `source_account IN (${textualAccounts})`; + + if (this.operator === TransactionExplorerConditionOperatorType.In) { + return `source_account IN (${textualAccounts})`; + } else if (this.operator === TransactionExplorerConditionOperatorType.NotIn) { + return `source_account NOT IN (${textualAccounts})`; + } else { + return ''; + } } } export class TransactionExplorerDestinationAccountCondition implements TransactionExplorerCondition { public static readonly supportedOperators: PartialRecord = { - [TransactionExplorerConditionOperatorType.In]: true + [TransactionExplorerConditionOperatorType.In]: true, + [TransactionExplorerConditionOperatorType.NotIn]: true }; public readonly field = TransactionExplorerConditionFieldType.DestinationAccount; - public readonly operator: TransactionExplorerConditionOperatorType.In = TransactionExplorerConditionOperatorType.In; + public readonly operator: AccountConditionOperator = TransactionExplorerConditionOperatorType.In; public value: string[]; - constructor(value: string[]) { + constructor(operator: AccountConditionOperator, value: string[]) { + this.operator = operator; this.value = value; } @@ -960,7 +1016,13 @@ export class TransactionExplorerDestinationAccountCondition implements Transacti } public match(transaction: TransactionInsightDataItem): boolean { - return this.value.includes(transaction.destinationAccountId); + if (this.operator === TransactionExplorerConditionOperatorType.In) { + return this.value.includes(transaction.destinationAccountId); + } else if (this.operator === TransactionExplorerConditionOperatorType.NotIn) { + return !this.value.includes(transaction.destinationAccountId); + } + + return false; } public toExpression(allCategoriesMap: Record, allAccountsMap: Record): string { @@ -977,7 +1039,14 @@ export class TransactionExplorerDestinationAccountCondition implements Transacti return `'${id}'`; } }).filter(item => !!item).join(', '); - return `destination_account IN (${textualAccounts})`; + + if (this.operator === TransactionExplorerConditionOperatorType.In) { + return `destination_account IN (${textualAccounts})`; + } else if (this.operator === TransactionExplorerConditionOperatorType.NotIn) { + return `destination_account NOT IN (${textualAccounts})`; + } else { + return ''; + } } }