add insights & explore page
This commit is contained in:
@@ -383,6 +383,7 @@ func startWebServer(c *core.CliContext) error {
|
||||
apiV1Route.GET("/transactions/count.json", bindApi(api.Transactions.TransactionCountHandler))
|
||||
apiV1Route.GET("/transactions/list.json", bindApi(api.Transactions.TransactionListHandler))
|
||||
apiV1Route.GET("/transactions/list/by_month.json", bindApi(api.Transactions.TransactionMonthListHandler))
|
||||
apiV1Route.GET("/transactions/list/all.json", bindApi(api.Transactions.TransactionListAllHandler))
|
||||
apiV1Route.GET("/transactions/reconciliation_statements.json", bindApi(api.Transactions.TransactionReconciliationStatementHandler))
|
||||
apiV1Route.GET("/transactions/statistics.json", bindApi(api.Transactions.TransactionStatisticsHandler))
|
||||
apiV1Route.GET("/transactions/statistics/trends.json", bindApi(api.Transactions.TransactionStatisticsTrendsHandler))
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -288,6 +289,88 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.WebContext) (any,
|
||||
return transactionResps, nil
|
||||
}
|
||||
|
||||
// TransactionListAllHandler returns all transaction list of current user
|
||||
func (a *TransactionsApi) TransactionListAllHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
var transactionAllListReq models.TransactionAllListRequest
|
||||
err := c.ShouldBindQuery(&transactionAllListReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transactions.TransactionListAllHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
utcOffset, err := c.GetClientTimezoneOffset()
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transactions.TransactionListAllHandler] cannot get client timezone offset, because %s", err.Error())
|
||||
return nil, errs.ErrClientTimezoneOffsetInvalid
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
user, err := a.users.GetUserById(c, uid)
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.Errorf(c, "[transactions.TransactionListAllHandler] failed to get user, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
allAccountIds, err := a.accounts.GetAccountOrSubAccountIds(c, transactionAllListReq.AccountIds, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transactions.TransactionListAllHandler] get account error, because %s", err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
allCategoryIds, err := a.transactionCategories.GetCategoryOrSubCategoryIds(c, transactionAllListReq.CategoryIds, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transactions.TransactionListAllHandler] get transaction category error, because %s", err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
noTags := transactionAllListReq.TagFilter == models.TransactionNoTagFilterValue
|
||||
var tagFilters []*models.TransactionTagFilter
|
||||
|
||||
if !noTags {
|
||||
tagFilters, err = models.ParseTransactionTagFilter(transactionAllListReq.TagFilter)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transactions.TransactionListAllHandler] parse transaction tag filters error, because %s", err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
}
|
||||
|
||||
maxTransactionTime := int64(math.MaxInt64)
|
||||
minTransactionTime := int64(0)
|
||||
|
||||
if transactionAllListReq.EndTime > 0 {
|
||||
maxTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(transactionAllListReq.EndTime)
|
||||
}
|
||||
|
||||
if transactionAllListReq.StartTime > 0 {
|
||||
minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(transactionAllListReq.StartTime)
|
||||
}
|
||||
|
||||
allTransactions, err := a.transactions.GetAllSpecifiedTransactions(c, uid, maxTransactionTime, minTransactionTime, transactionAllListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionAllListReq.AmountFilter, transactionAllListReq.Keyword, pageCountForDataExport, true)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transactions.TransactionListAllHandler] failed to get all transactions for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
transactionResult, err := a.getTransactionResponseListResult(c, user, allTransactions, utcOffset, transactionAllListReq.WithPictures, transactionAllListReq.TrimAccount, transactionAllListReq.TrimCategory, transactionAllListReq.TrimTag)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transactions.TransactionListAllHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
return transactionResult, nil
|
||||
}
|
||||
|
||||
// TransactionReconciliationStatementHandler returns transaction reconciliation statement list of current user
|
||||
func (a *TransactionsApi) TransactionReconciliationStatementHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
var reconciliationStatementRequest models.TransactionReconciliationStatementRequest
|
||||
|
||||
@@ -254,6 +254,22 @@ type TransactionListInMonthByPageRequest struct {
|
||||
TrimTag bool `form:"trim_tag"`
|
||||
}
|
||||
|
||||
// TransactionAllListRequest represents all parameters of all transaction listing request
|
||||
type TransactionAllListRequest struct {
|
||||
Type TransactionType `form:"type" binding:"min=0,max=4"`
|
||||
CategoryIds string `form:"category_ids"`
|
||||
AccountIds string `form:"account_ids"`
|
||||
TagFilter string `form:"tag_filter" binding:"validTagFilter"`
|
||||
AmountFilter string `form:"amount_filter" binding:"validAmountFilter"`
|
||||
Keyword string `form:"keyword"`
|
||||
StartTime int64 `form:"start_time" binding:"min=0"`
|
||||
EndTime int64 `form:"end_time" binding:"min=0"`
|
||||
WithPictures bool `form:"with_pictures"`
|
||||
TrimAccount bool `form:"trim_account"`
|
||||
TrimCategory bool `form:"trim_category"`
|
||||
TrimTag bool `form:"trim_tag"`
|
||||
}
|
||||
|
||||
// TransactionReconciliationStatementRequest represents all parameters of transaction reconciliation statement request
|
||||
type TransactionReconciliationStatementRequest struct {
|
||||
AccountId int64 `form:"account_id,string" binding:"required,min=1"`
|
||||
|
||||
@@ -29,6 +29,9 @@ var ALL_ALLOWED_CLOUD_SYNC_APP_SETTING_KEY_TYPES = map[string]UserApplicationClo
|
||||
"autoSaveTransactionDraft": USER_APPLICATION_CLOUD_SETTING_TYPE_STRING,
|
||||
"autoGetCurrentGeoLocation": USER_APPLICATION_CLOUD_SETTING_TYPE_BOOLEAN,
|
||||
"alwaysShowTransactionPicturesInMobileTransactionEditPage": USER_APPLICATION_CLOUD_SETTING_TYPE_BOOLEAN,
|
||||
// Insights & Explore Page
|
||||
"insightsExploreDefaultDateRangeType": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER,
|
||||
"timezoneUsedForInsightsExplorePage": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER,
|
||||
// Account List Page
|
||||
"totalAmountExcludeAccountIds": USER_APPLICATION_CLOUD_SETTING_TYPE_STRING_BOOLEAN_MAP,
|
||||
// Exchange Rates Data Page
|
||||
|
||||
+4
-4
@@ -21,7 +21,7 @@ export function* reversedItemAndIndex<T>(arr: T[]): Iterable<[T, number]> {
|
||||
}
|
||||
}
|
||||
|
||||
export function* entries<K extends string | number | symbol, V>(obj: Record<K, V>): Iterable<[string, V]> {
|
||||
export function* entries<K extends string | number | symbol, V>(obj: Record<K, V> | PartialRecord<K, V>): Iterable<[string, V]> {
|
||||
for (const key in obj) {
|
||||
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
continue;
|
||||
@@ -31,7 +31,7 @@ export function* entries<K extends string | number | symbol, V>(obj: Record<K, V
|
||||
}
|
||||
}
|
||||
|
||||
export function* keys<K extends string | number | symbol, V>(obj: Record<K, V>): Iterable<string> {
|
||||
export function* keys<K extends string | number | symbol, V>(obj: Record<K, V> | PartialRecord<K, V>): Iterable<string> {
|
||||
for (const key in obj) {
|
||||
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
continue;
|
||||
@@ -41,7 +41,7 @@ export function* keys<K extends string | number | symbol, V>(obj: Record<K, V>):
|
||||
}
|
||||
}
|
||||
|
||||
export function* keysIfValueEquals<K extends string | number | symbol, V>(obj: Record<K, V>, value: V): Iterable<string> {
|
||||
export function* keysIfValueEquals<K extends string | number | symbol, V>(obj: Record<K, V> | PartialRecord<K, V>, value: V): Iterable<string> {
|
||||
for (const key in obj) {
|
||||
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
continue;
|
||||
@@ -55,7 +55,7 @@ export function* keysIfValueEquals<K extends string | number | symbol, V>(obj: R
|
||||
}
|
||||
}
|
||||
|
||||
export function* values<K extends string | number | symbol, V>(obj: Record<K, V>): Iterable<V> {
|
||||
export function* values<K extends string | number | symbol, V>(obj: Record<K, V> | PartialRecord<K, V>): Iterable<V> {
|
||||
for (const key in obj) {
|
||||
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
continue;
|
||||
|
||||
+22
-21
@@ -589,7 +589,8 @@ export class ShortTimeFormat implements TimeFormat {
|
||||
export enum DateRangeScene {
|
||||
Normal = 0,
|
||||
TrendAnalysis = 1,
|
||||
AssetTrends = 2
|
||||
AssetTrends = 2,
|
||||
InsightsExplore = 3
|
||||
}
|
||||
|
||||
export class DateRange implements TypeAndName {
|
||||
@@ -597,38 +598,38 @@ export class DateRange implements TypeAndName {
|
||||
private static readonly allInstancesByType: Record<number, DateRange> = {};
|
||||
|
||||
// All date range
|
||||
public static readonly All = new DateRange(0, 'All', false, false, DateRangeScene.Normal, DateRangeScene.TrendAnalysis, DateRangeScene.AssetTrends);
|
||||
public static readonly All = new DateRange(0, 'All', false, false, DateRangeScene.Normal, DateRangeScene.TrendAnalysis, DateRangeScene.AssetTrends, DateRangeScene.InsightsExplore);
|
||||
|
||||
// Date ranges for normal scene only
|
||||
public static readonly Today = new DateRange(1, 'Today', false, false, DateRangeScene.Normal);
|
||||
public static readonly Yesterday = new DateRange(2, 'Yesterday', false, false, DateRangeScene.Normal);
|
||||
public static readonly LastSevenDays = new DateRange(3, 'Recent 7 days', false, false, DateRangeScene.Normal, DateRangeScene.AssetTrends);
|
||||
public static readonly LastThirtyDays = new DateRange(4, 'Recent 30 days', false, false, DateRangeScene.Normal, DateRangeScene.AssetTrends);
|
||||
public static readonly ThisWeek = new DateRange(5, 'This week', false, false, DateRangeScene.Normal, DateRangeScene.AssetTrends);
|
||||
public static readonly LastWeek = new DateRange(6, 'Last week', false, false, DateRangeScene.Normal, DateRangeScene.AssetTrends);
|
||||
public static readonly ThisMonth = new DateRange(7, 'This month', false, false, DateRangeScene.Normal, DateRangeScene.AssetTrends);
|
||||
public static readonly LastMonth = new DateRange(8, 'Last month', false, false, DateRangeScene.Normal, DateRangeScene.AssetTrends);
|
||||
public static readonly Today = new DateRange(1, 'Today', false, false, DateRangeScene.Normal, DateRangeScene.InsightsExplore);
|
||||
public static readonly Yesterday = new DateRange(2, 'Yesterday', false, false, DateRangeScene.Normal, DateRangeScene.InsightsExplore);
|
||||
public static readonly LastSevenDays = new DateRange(3, 'Recent 7 days', false, false, DateRangeScene.Normal, DateRangeScene.AssetTrends, DateRangeScene.InsightsExplore);
|
||||
public static readonly LastThirtyDays = new DateRange(4, 'Recent 30 days', false, false, DateRangeScene.Normal, DateRangeScene.AssetTrends, DateRangeScene.InsightsExplore);
|
||||
public static readonly ThisWeek = new DateRange(5, 'This week', false, false, DateRangeScene.Normal, DateRangeScene.AssetTrends, DateRangeScene.InsightsExplore);
|
||||
public static readonly LastWeek = new DateRange(6, 'Last week', false, false, DateRangeScene.Normal, DateRangeScene.AssetTrends, DateRangeScene.InsightsExplore);
|
||||
public static readonly ThisMonth = new DateRange(7, 'This month', false, false, DateRangeScene.Normal, DateRangeScene.AssetTrends, DateRangeScene.InsightsExplore);
|
||||
public static readonly LastMonth = new DateRange(8, 'Last month', false, false, DateRangeScene.Normal, DateRangeScene.AssetTrends, DateRangeScene.InsightsExplore);
|
||||
|
||||
// Date ranges for normal and trend analysis scene
|
||||
public static readonly ThisYear = new DateRange(9, 'This year', false, false, DateRangeScene.Normal, DateRangeScene.TrendAnalysis, DateRangeScene.AssetTrends);
|
||||
public static readonly LastYear = new DateRange(10, 'Last year', false, false, DateRangeScene.Normal, DateRangeScene.TrendAnalysis, DateRangeScene.AssetTrends);
|
||||
public static readonly ThisFiscalYear = new DateRange(11, 'This fiscal year', false, true, DateRangeScene.Normal, DateRangeScene.TrendAnalysis, DateRangeScene.AssetTrends);
|
||||
public static readonly LastFiscalYear = new DateRange(12, 'Last fiscal year', false, true, DateRangeScene.Normal, DateRangeScene.TrendAnalysis, DateRangeScene.AssetTrends);
|
||||
public static readonly ThisYear = new DateRange(9, 'This year', false, false, DateRangeScene.Normal, DateRangeScene.TrendAnalysis, DateRangeScene.AssetTrends, DateRangeScene.InsightsExplore);
|
||||
public static readonly LastYear = new DateRange(10, 'Last year', false, false, DateRangeScene.Normal, DateRangeScene.TrendAnalysis, DateRangeScene.AssetTrends, DateRangeScene.InsightsExplore);
|
||||
public static readonly ThisFiscalYear = new DateRange(11, 'This fiscal year', false, true, DateRangeScene.Normal, DateRangeScene.TrendAnalysis, DateRangeScene.AssetTrends, DateRangeScene.InsightsExplore);
|
||||
public static readonly LastFiscalYear = new DateRange(12, 'Last fiscal year', false, true, DateRangeScene.Normal, DateRangeScene.TrendAnalysis, DateRangeScene.AssetTrends, DateRangeScene.InsightsExplore);
|
||||
|
||||
// Billing cycle date ranges for normal scene only
|
||||
public static readonly CurrentBillingCycle = new DateRange(51, 'Current Billing Cycle', true, true, DateRangeScene.Normal);
|
||||
public static readonly PreviousBillingCycle = new DateRange(52, 'Previous Billing Cycle', true, true, DateRangeScene.Normal);
|
||||
|
||||
// Date ranges for trend analysis scene only
|
||||
public static readonly RecentTwelveMonths = new DateRange(101, 'Recent 12 months', false, false, DateRangeScene.TrendAnalysis, DateRangeScene.AssetTrends);
|
||||
public static readonly RecentTwentyFourMonths = new DateRange(102, 'Recent 24 months', false, false, DateRangeScene.TrendAnalysis, DateRangeScene.AssetTrends);
|
||||
public static readonly RecentThirtySixMonths = new DateRange(103, 'Recent 36 months', false, false, DateRangeScene.TrendAnalysis, DateRangeScene.AssetTrends);
|
||||
public static readonly RecentTwoYears = new DateRange(104, 'Recent 2 years', false, false, DateRangeScene.TrendAnalysis, DateRangeScene.AssetTrends);
|
||||
public static readonly RecentThreeYears = new DateRange(105, 'Recent 3 years', false, false, DateRangeScene.TrendAnalysis, DateRangeScene.AssetTrends);
|
||||
public static readonly RecentFiveYears = new DateRange(106, 'Recent 5 years', false, false, DateRangeScene.TrendAnalysis, DateRangeScene.AssetTrends);
|
||||
public static readonly RecentTwelveMonths = new DateRange(101, 'Recent 12 months', false, false, DateRangeScene.TrendAnalysis, DateRangeScene.AssetTrends, DateRangeScene.InsightsExplore);
|
||||
public static readonly RecentTwentyFourMonths = new DateRange(102, 'Recent 24 months', false, false, DateRangeScene.TrendAnalysis, DateRangeScene.AssetTrends, DateRangeScene.InsightsExplore);
|
||||
public static readonly RecentThirtySixMonths = new DateRange(103, 'Recent 36 months', false, false, DateRangeScene.TrendAnalysis, DateRangeScene.AssetTrends, DateRangeScene.InsightsExplore);
|
||||
public static readonly RecentTwoYears = new DateRange(104, 'Recent 2 years', false, false, DateRangeScene.TrendAnalysis, DateRangeScene.AssetTrends, DateRangeScene.InsightsExplore);
|
||||
public static readonly RecentThreeYears = new DateRange(105, 'Recent 3 years', false, false, DateRangeScene.TrendAnalysis, DateRangeScene.AssetTrends, DateRangeScene.InsightsExplore);
|
||||
public static readonly RecentFiveYears = new DateRange(106, 'Recent 5 years', false, false, DateRangeScene.TrendAnalysis, DateRangeScene.AssetTrends, DateRangeScene.InsightsExplore);
|
||||
|
||||
// Custom date range
|
||||
public static readonly Custom = new DateRange(255, 'Custom Date', false, true, DateRangeScene.Normal, DateRangeScene.TrendAnalysis, DateRangeScene.AssetTrends);
|
||||
public static readonly Custom = new DateRange(255, 'Custom Date', false, true, DateRangeScene.Normal, DateRangeScene.TrendAnalysis, DateRangeScene.AssetTrends, DateRangeScene.InsightsExplore);
|
||||
|
||||
public readonly type: number;
|
||||
public readonly name: string;
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { type NameValue } from '@/core/base.ts';
|
||||
import { DateRange } from '@/core/datetime.ts';
|
||||
|
||||
export enum TransactionExploreConditionRelation {
|
||||
First = 'first',
|
||||
And = 'and',
|
||||
Or = 'or'
|
||||
}
|
||||
|
||||
export const TransactionExploreConditionRelationPriority: Record<TransactionExploreConditionRelation, number> = {
|
||||
[TransactionExploreConditionRelation.First]: 0,
|
||||
[TransactionExploreConditionRelation.Or]: 1,
|
||||
[TransactionExploreConditionRelation.And]: 2
|
||||
};
|
||||
|
||||
|
||||
export enum TransactionExploreConditionFieldType {
|
||||
TransactionType = 'transactionType',
|
||||
TransactionCategory = 'transactionCategory',
|
||||
SourceAccount = 'sourceAccount',
|
||||
DestinationAccount = 'destinationAccount',
|
||||
SourceAmount = 'sourceAmount',
|
||||
DestinationAmount = 'destinationAmount',
|
||||
TransactionTag = 'transactionTag',
|
||||
Description = 'description'
|
||||
}
|
||||
|
||||
export class TransactionExploreConditionField implements NameValue {
|
||||
private static readonly allInstances: TransactionExploreConditionField[] = [];
|
||||
private static readonly allInstancesByValue: Record<string, TransactionExploreConditionField> = {};
|
||||
|
||||
public static readonly TransactionType = new TransactionExploreConditionField('Transaction Type', TransactionExploreConditionFieldType.TransactionType);
|
||||
public static readonly TransactionCategory = new TransactionExploreConditionField('Category', TransactionExploreConditionFieldType.TransactionCategory);
|
||||
public static readonly SourceAccount = new TransactionExploreConditionField('Source Account', TransactionExploreConditionFieldType.SourceAccount);
|
||||
public static readonly DestinationAccount = new TransactionExploreConditionField('Destination Account', TransactionExploreConditionFieldType.DestinationAccount);
|
||||
public static readonly SourceAmount = new TransactionExploreConditionField('Amount', TransactionExploreConditionFieldType.SourceAmount);
|
||||
public static readonly DestinationAmount = new TransactionExploreConditionField('Transfer In Amount', TransactionExploreConditionFieldType.DestinationAmount);
|
||||
public static readonly TransactionTag = new TransactionExploreConditionField('Tags', TransactionExploreConditionFieldType.TransactionTag);
|
||||
public static readonly Description = new TransactionExploreConditionField('Description', TransactionExploreConditionFieldType.Description);
|
||||
|
||||
public readonly name: string;
|
||||
public readonly value: TransactionExploreConditionFieldType;
|
||||
|
||||
private constructor(name: string, value: TransactionExploreConditionFieldType) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
|
||||
TransactionExploreConditionField.allInstances.push(this);
|
||||
TransactionExploreConditionField.allInstancesByValue[value] = this;
|
||||
}
|
||||
|
||||
public static values(): TransactionExploreConditionField[] {
|
||||
return TransactionExploreConditionField.allInstances;
|
||||
}
|
||||
|
||||
public static valueOf(type: string): TransactionExploreConditionField | undefined {
|
||||
return TransactionExploreConditionField.allInstancesByValue[type];
|
||||
}
|
||||
}
|
||||
|
||||
export enum TransactionExploreConditionOperatorType {
|
||||
In = 'in',
|
||||
GreaterThan = 'greaterThan',
|
||||
LessThan = 'lessThan',
|
||||
Equals = 'equals',
|
||||
NotEquals = 'notEquals',
|
||||
Between = 'between',
|
||||
NotBetween = 'notBetween',
|
||||
HasAny = 'hasAny',
|
||||
HasAll = 'hasAll',
|
||||
NotHasAny = 'notHasAny',
|
||||
NotHasAll = 'notHasAll',
|
||||
IsEmpty = 'isEmpty',
|
||||
IsNotEmpty = 'isNotEmpty',
|
||||
Contains = 'contains',
|
||||
NotContains = 'notContains',
|
||||
StartsWith = 'startsWith',
|
||||
NotStartsWith = 'notStartsWith',
|
||||
EndsWith = 'endsWith',
|
||||
NotEndsWith = 'notEndsWith'
|
||||
}
|
||||
|
||||
export class TransactionExploreConditionOperator implements NameValue {
|
||||
private static readonly allInstances: TransactionExploreConditionOperator[] = [];
|
||||
private static readonly allInstancesByValue: Record<string, TransactionExploreConditionOperator> = {};
|
||||
|
||||
public static readonly In = new TransactionExploreConditionOperator('In', TransactionExploreConditionOperatorType.In);
|
||||
public static readonly GreaterThan = new TransactionExploreConditionOperator('Greater than', TransactionExploreConditionOperatorType.GreaterThan);
|
||||
public static readonly LessThan = new TransactionExploreConditionOperator('Less than', TransactionExploreConditionOperatorType.LessThan);
|
||||
public static readonly Equals = new TransactionExploreConditionOperator('Equal to', TransactionExploreConditionOperatorType.Equals);
|
||||
public static readonly NotEquals = new TransactionExploreConditionOperator('Not equal to', TransactionExploreConditionOperatorType.NotEquals);
|
||||
public static readonly Between = new TransactionExploreConditionOperator('Between', TransactionExploreConditionOperatorType.Between);
|
||||
public static readonly NotBetween = new TransactionExploreConditionOperator('Not between', TransactionExploreConditionOperatorType.NotBetween);
|
||||
public static readonly HasAny = new TransactionExploreConditionOperator('Has any', TransactionExploreConditionOperatorType.HasAny);
|
||||
public static readonly HasAll = new TransactionExploreConditionOperator('Has all', TransactionExploreConditionOperatorType.HasAll);
|
||||
public static readonly NotHasAny = new TransactionExploreConditionOperator('Not has any', TransactionExploreConditionOperatorType.NotHasAny);
|
||||
public static readonly NotHasAll = new TransactionExploreConditionOperator('Not has all', TransactionExploreConditionOperatorType.NotHasAll);
|
||||
public static readonly IsEmpty = new TransactionExploreConditionOperator('Is empty', TransactionExploreConditionOperatorType.IsEmpty);
|
||||
public static readonly IsNotEmpty = new TransactionExploreConditionOperator('Is not empty', TransactionExploreConditionOperatorType.IsNotEmpty);
|
||||
public static readonly Contains = new TransactionExploreConditionOperator('Contains', TransactionExploreConditionOperatorType.Contains);
|
||||
public static readonly NotContains = new TransactionExploreConditionOperator('Not contains', TransactionExploreConditionOperatorType.NotContains);
|
||||
public static readonly StartsWith = new TransactionExploreConditionOperator('Starts with', TransactionExploreConditionOperatorType.StartsWith);
|
||||
public static readonly NotStartsWith = new TransactionExploreConditionOperator('Not starts with', TransactionExploreConditionOperatorType.NotStartsWith);
|
||||
public static readonly EndsWith = new TransactionExploreConditionOperator('Ends with', TransactionExploreConditionOperatorType.EndsWith);
|
||||
public static readonly NotEndsWith = new TransactionExploreConditionOperator('Not ends with', TransactionExploreConditionOperatorType.NotEndsWith);
|
||||
|
||||
public readonly name: string;
|
||||
public readonly value: TransactionExploreConditionOperatorType;
|
||||
|
||||
private constructor(name: string, value: TransactionExploreConditionOperatorType) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
|
||||
TransactionExploreConditionOperator.allInstances.push(this);
|
||||
TransactionExploreConditionOperator.allInstancesByValue[value] = this;
|
||||
}
|
||||
|
||||
public static values(): TransactionExploreConditionOperator[] {
|
||||
return TransactionExploreConditionOperator.allInstances;
|
||||
}
|
||||
|
||||
public static valueOf(type: string): TransactionExploreConditionOperator | undefined {
|
||||
return TransactionExploreConditionOperator.allInstancesByValue[type];
|
||||
}
|
||||
}
|
||||
|
||||
export const DEFAULT_TRANSACTION_EXPLORE_DATE_RANGE: DateRange = DateRange.ThisMonth;
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
DEFAULT_TREND_CHART_DATA_RANGE,
|
||||
DEFAULT_ASSET_TRENDS_CHART_DATA_RANGE
|
||||
} from './statistics.ts';
|
||||
import { DEFAULT_TRANSACTION_EXPLORE_DATE_RANGE } from './explore.ts';
|
||||
import { DEFAULT_CURRENCY_CODE } from '@/consts/currency.ts';
|
||||
|
||||
export type ApplicationSettingKey = string;
|
||||
@@ -49,6 +50,9 @@ export interface ApplicationSettings extends BaseApplicationSetting {
|
||||
autoSaveTransactionDraft: string;
|
||||
autoGetCurrentGeoLocation: boolean;
|
||||
alwaysShowTransactionPicturesInMobileTransactionEditPage: boolean;
|
||||
// Insights & Explore Page
|
||||
insightsExploreDefaultDateRangeType: number;
|
||||
timezoneUsedForInsightsExplorePage: number;
|
||||
// Account List Page
|
||||
totalAmountExcludeAccountIds: Record<string, boolean>;
|
||||
// Exchange Rates Data Page
|
||||
@@ -111,6 +115,9 @@ export const ALL_ALLOWED_CLOUD_SYNC_APP_SETTING_KEY_TYPES: Record<string, UserAp
|
||||
'autoSaveTransactionDraft': UserApplicationCloudSettingType.String,
|
||||
'autoGetCurrentGeoLocation': UserApplicationCloudSettingType.Boolean,
|
||||
'alwaysShowTransactionPicturesInMobileTransactionEditPage': UserApplicationCloudSettingType.Boolean,
|
||||
// Insights & Explore Page
|
||||
'insightsExploreDefaultDateRangeType': UserApplicationCloudSettingType.Number,
|
||||
'timezoneUsedForInsightsExplorePage': UserApplicationCloudSettingType.Number,
|
||||
// Account List Page
|
||||
'totalAmountExcludeAccountIds': UserApplicationCloudSettingType.StringBooleanMap,
|
||||
// Exchange Rates Data Page
|
||||
@@ -158,6 +165,9 @@ export const DEFAULT_APPLICATION_SETTINGS: ApplicationSettings = {
|
||||
autoSaveTransactionDraft: 'disabled',
|
||||
autoGetCurrentGeoLocation: false,
|
||||
alwaysShowTransactionPicturesInMobileTransactionEditPage: false,
|
||||
// Insights & Explore Page
|
||||
insightsExploreDefaultDateRangeType: DEFAULT_TRANSACTION_EXPLORE_DATE_RANGE.type,
|
||||
timezoneUsedForInsightsExplorePage: TimezoneTypeForStatistics.Default.type,
|
||||
// Account List Page
|
||||
totalAmountExcludeAccountIds: {},
|
||||
// Exchange Rates Data Page
|
||||
|
||||
@@ -69,6 +69,7 @@ import type {
|
||||
TransactionImportRequest,
|
||||
TransactionListByMaxTimeRequest,
|
||||
TransactionListInMonthByPageRequest,
|
||||
TransactionAllListRequest,
|
||||
TransactionInfoResponse,
|
||||
TransactionInfoPageWrapperResponse,
|
||||
TransactionInfoPageWrapperResponse2,
|
||||
@@ -492,6 +493,9 @@ export default {
|
||||
const keyword = encodeURIComponent(req.keyword);
|
||||
return axios.get<ApiResponse<TransactionInfoPageWrapperResponse2>>(`v1/transactions/list/by_month.json?year=${req.year}&month=${req.month}&type=${req.type}&category_ids=${req.categoryIds}&account_ids=${req.accountIds}&tag_filter=${tagFilter}&amount_filter=${amountFilter}&keyword=${keyword}&trim_account=true&trim_category=true&trim_tag=true`);
|
||||
},
|
||||
getAllTransactions: (req: TransactionAllListRequest): ApiResponsePromise<TransactionInfoResponse[]> => {
|
||||
return axios.get<ApiResponse<TransactionInfoResponse[]>>(`v1/transactions/list/all.json?trim_account=true&trim_category=true&trim_tag=true&start_time=${req.startTime}&end_time=${req.endTime}`);
|
||||
},
|
||||
getReconciliationStatements: (req: TransactionReconciliationStatementRequest): ApiResponsePromise<TransactionReconciliationStatementResponse> => {
|
||||
return axios.get<ApiResponse<TransactionReconciliationStatementResponse>>(`v1/transactions/reconciliation_statements.json?account_id=${req.accountId}&start_time=${req.startTime}&end_time=${req.endTime}`);
|
||||
},
|
||||
|
||||
@@ -1434,6 +1434,7 @@
|
||||
"Update": "Aktualisieren",
|
||||
"Refresh": "Aktualisieren",
|
||||
"Clear": "Löschen",
|
||||
"Clear All": "Clear All",
|
||||
"Generate": "Generate",
|
||||
"Recognize": "Recognize",
|
||||
"Recognizing": "Recognizing",
|
||||
@@ -1490,6 +1491,9 @@
|
||||
"More": "Mehr",
|
||||
"All": "Alle",
|
||||
"Partial": "Partial",
|
||||
"WHERE": "WHERE",
|
||||
"AND": "AND",
|
||||
"OR": "OR",
|
||||
"Today": "Heute",
|
||||
"Yesterday": "Gestern",
|
||||
"Recent 7 days": "Letzte 7 Tage",
|
||||
@@ -1527,6 +1531,19 @@
|
||||
"Not equal to": "Ungleich",
|
||||
"Between": "Zwischen",
|
||||
"Not between": "Nicht zwischen",
|
||||
"In": "In",
|
||||
"Has any": "Has any",
|
||||
"Has all": "Has all",
|
||||
"Not has any": "Not has any",
|
||||
"Not has all": "Not has all",
|
||||
"Is empty": "Is empty",
|
||||
"Is not empty": "Is not empty",
|
||||
"Contains": "Contains",
|
||||
"Not contains": "Not contains",
|
||||
"Starts with": "Starts with",
|
||||
"Not starts with": "Not starts with",
|
||||
"Ends with": "Ends with",
|
||||
"Not ends with": "Not ends with",
|
||||
"Pie Chart": "Tortendiagramm",
|
||||
"Bar Chart": "Balkendiagramm",
|
||||
"Radar Chart": "Radar Chart",
|
||||
@@ -1675,6 +1692,20 @@
|
||||
"Transaction Calendar": "Transaction Calendar",
|
||||
"Transaction Details": "Transaktionsdetails",
|
||||
"Statistics & Analysis": "Statistiken & Analysen",
|
||||
"Insights & Explore": "Insights & Explore",
|
||||
"Query": "Query",
|
||||
"Data Table": "Data Table",
|
||||
"Chart": "Chart",
|
||||
"New Explore": "New Explore",
|
||||
"Add Query": "Add Query",
|
||||
"Remove Query": "Remove Query",
|
||||
"Modify Query Name": "Modify Query Name",
|
||||
"Add Condition": "Add Condition",
|
||||
"Remove Condition": "Remove Condition",
|
||||
"No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.",
|
||||
"Editor": "Editor",
|
||||
"Expression": "Expression",
|
||||
"Failed to generate expression": "Failed to generate expression",
|
||||
"Account List": "Kontoliste",
|
||||
"This Week": "Diese Woche",
|
||||
"This Month": "Dieser Monat",
|
||||
@@ -1997,6 +2028,7 @@
|
||||
"Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.",
|
||||
"Search transaction description": "Transaktionsbeschreibung suchen",
|
||||
"Unable to retrieve transaction list": "Transaktionsliste kann nicht abgerufen werden",
|
||||
"Unable to retrieve all transactions": "Unable to retrieve all transactions",
|
||||
"Unable to retrieve reconciliation statements": "Unable to retrieve reconciliation statements",
|
||||
"Custom Date Range": "Benutzerdefinierter Datumsbereich",
|
||||
"Select Month": "Select Month",
|
||||
@@ -2121,6 +2153,7 @@
|
||||
"Show Confirmation Every Time": "Bestätigung jedes mal anzeigen",
|
||||
"Automatically Add Geolocation": "Geolocation automatisch hinzufügen",
|
||||
"Always Show Transaction Pictures": "Always Show Transaction Pictures",
|
||||
"Insights & Explore Page": "Insights & Explore Page",
|
||||
"Account List Page": "Account List Page",
|
||||
"Accounts Included in Total": "Accounts Included in Total",
|
||||
"Exchange Rates Data Page": "Wechselkursdatenseite",
|
||||
|
||||
@@ -1434,6 +1434,7 @@
|
||||
"Update": "Update",
|
||||
"Refresh": "Refresh",
|
||||
"Clear": "Clear",
|
||||
"Clear All": "Clear All",
|
||||
"Generate": "Generate",
|
||||
"Recognize": "Recognize",
|
||||
"Recognizing": "Recognizing",
|
||||
@@ -1490,6 +1491,9 @@
|
||||
"More": "More",
|
||||
"All": "All",
|
||||
"Partial": "Partial",
|
||||
"WHERE": "WHERE",
|
||||
"AND": "AND",
|
||||
"OR": "OR",
|
||||
"Today": "Today",
|
||||
"Yesterday": "Yesterday",
|
||||
"Recent 7 days": "Recent 7 days",
|
||||
@@ -1527,6 +1531,19 @@
|
||||
"Not equal to": "Not equal to",
|
||||
"Between": "Between",
|
||||
"Not between": "Not between",
|
||||
"In": "In",
|
||||
"Has any": "Has any",
|
||||
"Has all": "Has all",
|
||||
"Not has any": "Not has any",
|
||||
"Not has all": "Not has all",
|
||||
"Is empty": "Is empty",
|
||||
"Is not empty": "Is not empty",
|
||||
"Contains": "Contains",
|
||||
"Not contains": "Not contains",
|
||||
"Starts with": "Starts with",
|
||||
"Not starts with": "Not starts with",
|
||||
"Ends with": "Ends with",
|
||||
"Not ends with": "Not ends with",
|
||||
"Pie Chart": "Pie Chart",
|
||||
"Bar Chart": "Bar Chart",
|
||||
"Radar Chart": "Radar Chart",
|
||||
@@ -1675,6 +1692,20 @@
|
||||
"Transaction Calendar": "Transaction Calendar",
|
||||
"Transaction Details": "Transaction Details",
|
||||
"Statistics & Analysis": "Statistics & Analysis",
|
||||
"Insights & Explore": "Insights & Explore",
|
||||
"Query": "Query",
|
||||
"Data Table": "Data Table",
|
||||
"Chart": "Chart",
|
||||
"New Explore": "New Explore",
|
||||
"Add Query": "Add Query",
|
||||
"Remove Query": "Remove Query",
|
||||
"Modify Query Name": "Modify Query Name",
|
||||
"Add Condition": "Add Condition",
|
||||
"Remove Condition": "Remove Condition",
|
||||
"No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.",
|
||||
"Editor": "Editor",
|
||||
"Expression": "Expression",
|
||||
"Failed to generate expression": "Failed to generate expression",
|
||||
"Account List": "Account List",
|
||||
"This Week": "This Week",
|
||||
"This Month": "This Month",
|
||||
@@ -1997,6 +2028,7 @@
|
||||
"Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.",
|
||||
"Search transaction description": "Search transaction description",
|
||||
"Unable to retrieve transaction list": "Unable to retrieve transaction list",
|
||||
"Unable to retrieve all transactions": "Unable to retrieve all transactions",
|
||||
"Unable to retrieve reconciliation statements": "Unable to retrieve reconciliation statements",
|
||||
"Custom Date Range": "Custom Date Range",
|
||||
"Select Month": "Select Month",
|
||||
@@ -2121,6 +2153,7 @@
|
||||
"Show Confirmation Every Time": "Show Confirmation Every Time",
|
||||
"Automatically Add Geolocation": "Automatically Add Geolocation",
|
||||
"Always Show Transaction Pictures": "Always Show Transaction Pictures",
|
||||
"Insights & Explore Page": "Insights & Explore Page",
|
||||
"Account List Page": "Account List Page",
|
||||
"Accounts Included in Total": "Accounts Included in Total",
|
||||
"Exchange Rates Data Page": "Exchange Rates Data Page",
|
||||
|
||||
@@ -1434,6 +1434,7 @@
|
||||
"Update": "Actualizar",
|
||||
"Refresh": "Refrescar",
|
||||
"Clear": "Borrar",
|
||||
"Clear All": "Clear All",
|
||||
"Generate": "Generar",
|
||||
"Recognize": "Recognize",
|
||||
"Recognizing": "Recognizing",
|
||||
@@ -1490,6 +1491,9 @@
|
||||
"More": "Más",
|
||||
"All": "Todo",
|
||||
"Partial": "Parcial",
|
||||
"WHERE": "WHERE",
|
||||
"AND": "AND",
|
||||
"OR": "OR",
|
||||
"Today": "Hoy",
|
||||
"Yesterday": "Ayer",
|
||||
"Recent 7 days": "Últimos 7 días",
|
||||
@@ -1527,6 +1531,19 @@
|
||||
"Not equal to": "No igual a",
|
||||
"Between": "Entre",
|
||||
"Not between": "No entre",
|
||||
"In": "In",
|
||||
"Has any": "Has any",
|
||||
"Has all": "Has all",
|
||||
"Not has any": "Not has any",
|
||||
"Not has all": "Not has all",
|
||||
"Is empty": "Is empty",
|
||||
"Is not empty": "Is not empty",
|
||||
"Contains": "Contains",
|
||||
"Not contains": "Not contains",
|
||||
"Starts with": "Starts with",
|
||||
"Not starts with": "Not starts with",
|
||||
"Ends with": "Ends with",
|
||||
"Not ends with": "Not ends with",
|
||||
"Pie Chart": "Gráfico Circular",
|
||||
"Bar Chart": "Gráfico de Barras",
|
||||
"Radar Chart": "Gráfico de Radar",
|
||||
@@ -1675,6 +1692,20 @@
|
||||
"Transaction Calendar": "Calendario de Transacciones",
|
||||
"Transaction Details": "Detalles",
|
||||
"Statistics & Analysis": "Estadísticas y Análisis",
|
||||
"Insights & Explore": "Insights & Explore",
|
||||
"Query": "Query",
|
||||
"Data Table": "Data Table",
|
||||
"Chart": "Chart",
|
||||
"New Explore": "New Explore",
|
||||
"Add Query": "Add Query",
|
||||
"Remove Query": "Remove Query",
|
||||
"Modify Query Name": "Modify Query Name",
|
||||
"Add Condition": "Add Condition",
|
||||
"Remove Condition": "Remove Condition",
|
||||
"No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.",
|
||||
"Editor": "Editor",
|
||||
"Expression": "Expression",
|
||||
"Failed to generate expression": "Failed to generate expression",
|
||||
"Account List": "Lista de Cuentas",
|
||||
"This Week": "Esta Semana",
|
||||
"This Month": "Este Mes",
|
||||
@@ -1997,6 +2028,7 @@
|
||||
"Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.",
|
||||
"Search transaction description": "Buscar descripción de la transacción",
|
||||
"Unable to retrieve transaction list": "No se puede recuperar la lista de transacciones",
|
||||
"Unable to retrieve all transactions": "Unable to retrieve all transactions",
|
||||
"Unable to retrieve reconciliation statements": "Unable to retrieve reconciliation statements",
|
||||
"Custom Date Range": "Rango de Fechas Personalizado",
|
||||
"Select Month": "Select Month",
|
||||
@@ -2121,6 +2153,7 @@
|
||||
"Show Confirmation Every Time": "Mostrar Confirmación Cada Vez",
|
||||
"Automatically Add Geolocation": "Agregar Geolocalización Automáticamente",
|
||||
"Always Show Transaction Pictures": "Always Show Transaction Pictures",
|
||||
"Insights & Explore Page": "Insights & Explore Page",
|
||||
"Account List Page": "Página de Cuentas",
|
||||
"Accounts Included in Total": "Cuentas Incluidas en el Total",
|
||||
"Exchange Rates Data Page": "Página de Tipos de Cambio",
|
||||
|
||||
@@ -1434,6 +1434,7 @@
|
||||
"Update": "Mettre à jour",
|
||||
"Refresh": "Actualiser",
|
||||
"Clear": "Effacer",
|
||||
"Clear All": "Clear All",
|
||||
"Generate": "Générer",
|
||||
"Recognize": "Reconnaître",
|
||||
"Recognizing": "Recognizing",
|
||||
@@ -1490,6 +1491,9 @@
|
||||
"More": "Plus",
|
||||
"All": "Tous",
|
||||
"Partial": "Partiel",
|
||||
"WHERE": "WHERE",
|
||||
"AND": "AND",
|
||||
"OR": "OR",
|
||||
"Today": "Aujourd'hui",
|
||||
"Yesterday": "Hier",
|
||||
"Recent 7 days": "7 derniers jours",
|
||||
@@ -1527,6 +1531,19 @@
|
||||
"Not equal to": "Différent de",
|
||||
"Between": "Entre",
|
||||
"Not between": "Pas entre",
|
||||
"In": "In",
|
||||
"Has any": "Has any",
|
||||
"Has all": "Has all",
|
||||
"Not has any": "Not has any",
|
||||
"Not has all": "Not has all",
|
||||
"Is empty": "Is empty",
|
||||
"Is not empty": "Is not empty",
|
||||
"Contains": "Contains",
|
||||
"Not contains": "Not contains",
|
||||
"Starts with": "Starts with",
|
||||
"Not starts with": "Not starts with",
|
||||
"Ends with": "Ends with",
|
||||
"Not ends with": "Not ends with",
|
||||
"Pie Chart": "Graphique en secteurs",
|
||||
"Bar Chart": "Graphique en barres",
|
||||
"Radar Chart": "Radar Chart",
|
||||
@@ -1675,6 +1692,20 @@
|
||||
"Transaction Calendar": "Calendrier des transactions",
|
||||
"Transaction Details": "Détails de la transaction",
|
||||
"Statistics & Analysis": "Statistiques et analyse",
|
||||
"Insights & Explore": "Insights & Explore",
|
||||
"Query": "Query",
|
||||
"Data Table": "Data Table",
|
||||
"Chart": "Chart",
|
||||
"New Explore": "New Explore",
|
||||
"Add Query": "Add Query",
|
||||
"Remove Query": "Remove Query",
|
||||
"Modify Query Name": "Modify Query Name",
|
||||
"Add Condition": "Add Condition",
|
||||
"Remove Condition": "Remove Condition",
|
||||
"No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.",
|
||||
"Editor": "Editor",
|
||||
"Expression": "Expression",
|
||||
"Failed to generate expression": "Failed to generate expression",
|
||||
"Account List": "Liste des comptes",
|
||||
"This Week": "Cette semaine",
|
||||
"This Month": "Ce mois",
|
||||
@@ -1997,6 +2028,7 @@
|
||||
"Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "Échec du chargement de l'image, veuillez vérifier si les configurations \"domain\" et \"root_url\" sont correctement définies.",
|
||||
"Search transaction description": "Rechercher la description de transaction",
|
||||
"Unable to retrieve transaction list": "Impossible de récupérer la liste des transactions",
|
||||
"Unable to retrieve all transactions": "Unable to retrieve all transactions",
|
||||
"Unable to retrieve reconciliation statements": "Impossible de récupérer les relevés de rapprochement",
|
||||
"Custom Date Range": "Plage de dates personnalisée",
|
||||
"Select Month": "Sélectionner le mois",
|
||||
@@ -2121,6 +2153,7 @@
|
||||
"Show Confirmation Every Time": "Afficher la confirmation à chaque fois",
|
||||
"Automatically Add Geolocation": "Ajouter automatiquement la géolocalisation",
|
||||
"Always Show Transaction Pictures": "Toujours afficher les images de transaction",
|
||||
"Insights & Explore Page": "Insights & Explore Page",
|
||||
"Account List Page": "Page de liste des comptes",
|
||||
"Accounts Included in Total": "Comptes inclus dans le total",
|
||||
"Exchange Rates Data Page": "Page des données de taux de change",
|
||||
|
||||
@@ -143,6 +143,11 @@ import {
|
||||
ChartDateAggregationType
|
||||
} from '@/core/statistics.ts';
|
||||
|
||||
import {
|
||||
TransactionExploreConditionField,
|
||||
TransactionExploreConditionOperator
|
||||
} from '@/core/explore.ts';
|
||||
|
||||
import {
|
||||
type LocalizedImportFileCategoryAndTypes,
|
||||
type LocalizedImportFileType,
|
||||
@@ -553,6 +558,19 @@ export function useI18n() {
|
||||
return ret;
|
||||
}
|
||||
|
||||
function getLocalizedNameValue(nameValues: NameValue[]): NameValue[] {
|
||||
const ret: NameValue[] = [];
|
||||
|
||||
for (const nameValue of nameValues) {
|
||||
ret.push({
|
||||
name: t(nameValue.name),
|
||||
value: nameValue.value
|
||||
});
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function getLocalizedDisplayNameAndTypeWithSystemDefault(typeAndNames: TypeAndName[], defaultValue: number, defaultType: TypeAndName): TypeAndDisplayName[] {
|
||||
const ret: TypeAndDisplayName[] = [];
|
||||
|
||||
@@ -2366,6 +2384,8 @@ export function useI18n() {
|
||||
getAllTransactionDefaultCategories,
|
||||
getAllDisplayExchangeRates,
|
||||
getAllSupportedImportFileCagtegoryAndTypes,
|
||||
getAllTransactionExploreConditionFields: () => getLocalizedNameValue(TransactionExploreConditionField.values()),
|
||||
getAllTransactionExploreConditionOperators: (operators?: TransactionExploreConditionOperator[]) => getLocalizedNameValue(operators ?? TransactionExploreConditionOperator.values()),
|
||||
// get localized info
|
||||
getLanguageInfo,
|
||||
getMonthShortName,
|
||||
|
||||
@@ -1434,6 +1434,7 @@
|
||||
"Update": "Aggiorna",
|
||||
"Refresh": "Aggiorna",
|
||||
"Clear": "Pulisci",
|
||||
"Clear All": "Clear All",
|
||||
"Generate": "Generate",
|
||||
"Recognize": "Recognize",
|
||||
"Recognizing": "Recognizing",
|
||||
@@ -1490,6 +1491,9 @@
|
||||
"More": "Altro",
|
||||
"All": "Tutti",
|
||||
"Partial": "Partial",
|
||||
"WHERE": "WHERE",
|
||||
"AND": "AND",
|
||||
"OR": "OR",
|
||||
"Today": "Oggi",
|
||||
"Yesterday": "Ieri",
|
||||
"Recent 7 days": "Ultimi 7 giorni",
|
||||
@@ -1527,6 +1531,19 @@
|
||||
"Not equal to": "Diverso da",
|
||||
"Between": "Tra",
|
||||
"Not between": "Non tra",
|
||||
"In": "In",
|
||||
"Has any": "Has any",
|
||||
"Has all": "Has all",
|
||||
"Not has any": "Not has any",
|
||||
"Not has all": "Not has all",
|
||||
"Is empty": "Is empty",
|
||||
"Is not empty": "Is not empty",
|
||||
"Contains": "Contains",
|
||||
"Not contains": "Not contains",
|
||||
"Starts with": "Starts with",
|
||||
"Not starts with": "Not starts with",
|
||||
"Ends with": "Ends with",
|
||||
"Not ends with": "Not ends with",
|
||||
"Pie Chart": "Grafico a torta",
|
||||
"Bar Chart": "Grafico a barre",
|
||||
"Radar Chart": "Radar Chart",
|
||||
@@ -1675,6 +1692,20 @@
|
||||
"Transaction Calendar": "Transaction Calendar",
|
||||
"Transaction Details": "Dettagli transazione",
|
||||
"Statistics & Analysis": "Statistiche e analisi",
|
||||
"Insights & Explore": "Insights & Explore",
|
||||
"Query": "Query",
|
||||
"Data Table": "Data Table",
|
||||
"Chart": "Chart",
|
||||
"New Explore": "New Explore",
|
||||
"Add Query": "Add Query",
|
||||
"Remove Query": "Remove Query",
|
||||
"Modify Query Name": "Modify Query Name",
|
||||
"Add Condition": "Add Condition",
|
||||
"Remove Condition": "Remove Condition",
|
||||
"No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.",
|
||||
"Editor": "Editor",
|
||||
"Expression": "Expression",
|
||||
"Failed to generate expression": "Failed to generate expression",
|
||||
"Account List": "Elenco account",
|
||||
"This Week": "Questa settimana",
|
||||
"This Month": "Questo mese",
|
||||
@@ -1997,6 +2028,7 @@
|
||||
"Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.",
|
||||
"Search transaction description": "Cerca descrizione transazione",
|
||||
"Unable to retrieve transaction list": "Impossibile recuperare l'elenco delle transazioni",
|
||||
"Unable to retrieve all transactions": "Unable to retrieve all transactions",
|
||||
"Unable to retrieve reconciliation statements": "Unable to retrieve reconciliation statements",
|
||||
"Custom Date Range": "Intervallo date personalizzato",
|
||||
"Select Month": "Select Month",
|
||||
@@ -2121,6 +2153,7 @@
|
||||
"Show Confirmation Every Time": "Mostra conferma ogni volta",
|
||||
"Automatically Add Geolocation": "Aggiungi automaticamente geolocalizzazione",
|
||||
"Always Show Transaction Pictures": "Always Show Transaction Pictures",
|
||||
"Insights & Explore Page": "Insights & Explore Page",
|
||||
"Account List Page": "Account List Page",
|
||||
"Accounts Included in Total": "Accounts Included in Total",
|
||||
"Exchange Rates Data Page": "Pagina dati tassi di cambio",
|
||||
|
||||
@@ -1434,6 +1434,7 @@
|
||||
"Update": "アップデート",
|
||||
"Refresh": "リフレッシュ",
|
||||
"Clear": "消去",
|
||||
"Clear All": "Clear All",
|
||||
"Generate": "Generate",
|
||||
"Recognize": "Recognize",
|
||||
"Recognizing": "Recognizing",
|
||||
@@ -1490,6 +1491,9 @@
|
||||
"More": "さらに",
|
||||
"All": "すべて",
|
||||
"Partial": "Partial",
|
||||
"WHERE": "WHERE",
|
||||
"AND": "AND",
|
||||
"OR": "OR",
|
||||
"Today": "今日",
|
||||
"Yesterday": "昨日",
|
||||
"Recent 7 days": "直近7日間",
|
||||
@@ -1527,6 +1531,19 @@
|
||||
"Not equal to": "等しくない",
|
||||
"Between": "間",
|
||||
"Not between": "間ではない",
|
||||
"In": "In",
|
||||
"Has any": "Has any",
|
||||
"Has all": "Has all",
|
||||
"Not has any": "Not has any",
|
||||
"Not has all": "Not has all",
|
||||
"Is empty": "Is empty",
|
||||
"Is not empty": "Is not empty",
|
||||
"Contains": "Contains",
|
||||
"Not contains": "Not contains",
|
||||
"Starts with": "Starts with",
|
||||
"Not starts with": "Not starts with",
|
||||
"Ends with": "Ends with",
|
||||
"Not ends with": "Not ends with",
|
||||
"Pie Chart": "円グラフ",
|
||||
"Bar Chart": "棒グラフ",
|
||||
"Radar Chart": "Radar Chart",
|
||||
@@ -1675,6 +1692,20 @@
|
||||
"Transaction Calendar": "Transaction Calendar",
|
||||
"Transaction Details": "取引の詳細",
|
||||
"Statistics & Analysis": "統計と分析",
|
||||
"Insights & Explore": "Insights & Explore",
|
||||
"Query": "Query",
|
||||
"Data Table": "Data Table",
|
||||
"Chart": "Chart",
|
||||
"New Explore": "New Explore",
|
||||
"Add Query": "Add Query",
|
||||
"Remove Query": "Remove Query",
|
||||
"Modify Query Name": "Modify Query Name",
|
||||
"Add Condition": "Add Condition",
|
||||
"Remove Condition": "Remove Condition",
|
||||
"No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.",
|
||||
"Editor": "Editor",
|
||||
"Expression": "Expression",
|
||||
"Failed to generate expression": "Failed to generate expression",
|
||||
"Account List": "口座リスト",
|
||||
"This Week": "今週",
|
||||
"This Month": "今月",
|
||||
@@ -1997,6 +2028,7 @@
|
||||
"Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.",
|
||||
"Search transaction description": "取引の説明を検索",
|
||||
"Unable to retrieve transaction list": "取引リストを取得できません",
|
||||
"Unable to retrieve all transactions": "Unable to retrieve all transactions",
|
||||
"Unable to retrieve reconciliation statements": "Unable to retrieve reconciliation statements",
|
||||
"Custom Date Range": "カスタム日付範囲",
|
||||
"Select Month": "Select Month",
|
||||
@@ -2121,6 +2153,7 @@
|
||||
"Show Confirmation Every Time": "確認を毎回表示",
|
||||
"Automatically Add Geolocation": "座標を自動的に追加",
|
||||
"Always Show Transaction Pictures": "Always Show Transaction Pictures",
|
||||
"Insights & Explore Page": "Insights & Explore Page",
|
||||
"Account List Page": "Account List Page",
|
||||
"Accounts Included in Total": "Accounts Included in Total",
|
||||
"Exchange Rates Data Page": "為替レートデータページ",
|
||||
|
||||
@@ -1434,6 +1434,7 @@
|
||||
"Update": "ನವೀಕರಿಸು",
|
||||
"Refresh": "ರಿಫ್ರೆಶ್",
|
||||
"Clear": "ಕ್ಲಿಯರ್",
|
||||
"Clear All": "Clear All",
|
||||
"Generate": "ರಚಿಸು",
|
||||
"Recognize": "ಗುರುತಿಸು",
|
||||
"Recognizing": "ಗುರುತಿಸಲಾಗುತ್ತಿದೆ",
|
||||
@@ -1490,6 +1491,9 @@
|
||||
"More": "ಇನ್ನಷ್ಟು",
|
||||
"All": "ಎಲ್ಲಾ",
|
||||
"Partial": "ಭಾಗಶಃ",
|
||||
"WHERE": "WHERE",
|
||||
"AND": "AND",
|
||||
"OR": "OR",
|
||||
"Today": "ಇಂದು",
|
||||
"Yesterday": "ನಿನ್ನೆ",
|
||||
"Recent 7 days": "ಇತ್ತೀಚಿನ 7 ದಿನಗಳು",
|
||||
@@ -1527,6 +1531,19 @@
|
||||
"Not equal to": "ಸಮಾನವಲ್ಲ",
|
||||
"Between": "ನಡುವೆ",
|
||||
"Not between": "ನಡುವಲ್ಲ",
|
||||
"In": "In",
|
||||
"Has any": "Has any",
|
||||
"Has all": "Has all",
|
||||
"Not has any": "Not has any",
|
||||
"Not has all": "Not has all",
|
||||
"Is empty": "Is empty",
|
||||
"Is not empty": "Is not empty",
|
||||
"Contains": "Contains",
|
||||
"Not contains": "Not contains",
|
||||
"Starts with": "Starts with",
|
||||
"Not starts with": "Not starts with",
|
||||
"Ends with": "Ends with",
|
||||
"Not ends with": "Not ends with",
|
||||
"Pie Chart": "ಪಾಯಿ ಚಾರ್ಟ್",
|
||||
"Bar Chart": "ಬಾರ್ ಚಾರ್ಟ್",
|
||||
"Radar Chart": "ರಡಾರ್ ಚಾರ್ಟ್",
|
||||
@@ -1675,6 +1692,20 @@
|
||||
"Transaction Calendar": "ವಹಿವಾಟು ಕ್ಯಾಲೆಂಡರ್",
|
||||
"Transaction Details": "ವಹಿವಾಟಿನ ವಿವರಗಳು",
|
||||
"Statistics & Analysis": "ಸಂಖ್ಯಾಶಾಸ್ತ್ರ ಮತ್ತು ವಿಶ್ಲೇಷಣೆ",
|
||||
"Insights & Explore": "Insights & Explore",
|
||||
"Query": "Query",
|
||||
"Data Table": "Data Table",
|
||||
"Chart": "Chart",
|
||||
"New Explore": "New Explore",
|
||||
"Add Query": "Add Query",
|
||||
"Remove Query": "Remove Query",
|
||||
"Modify Query Name": "Modify Query Name",
|
||||
"Add Condition": "Add Condition",
|
||||
"Remove Condition": "Remove Condition",
|
||||
"No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.",
|
||||
"Editor": "Editor",
|
||||
"Expression": "Expression",
|
||||
"Failed to generate expression": "Failed to generate expression",
|
||||
"Account List": "ಖಾತೆಗಳ ಪಟ್ಟಿ",
|
||||
"This Week": "ಈ ವಾರ",
|
||||
"This Month": "ಈ ತಿಂಗಳು",
|
||||
@@ -1997,6 +2028,7 @@
|
||||
"Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "ಚಿತ್ರ ಲೋಡ್ ಮಾಡಲು ವಿಫಲವಾಗಿದೆ, ದಯವಿಟ್ಟು \"domain\" ಮತ್ತು \"root_url\" ಸಂರಚನೆ ಸರಿಯಾಗಿದೆಯೇ ಎಂದು ಪರಿಶೀಲಿಸಿ.",
|
||||
"Search transaction description": "ವಹಿವಾಟು ವಿವರಣೆ ಹುಡುಕಿ",
|
||||
"Unable to retrieve transaction list": "ವಹಿವಾಟು ಪಟ್ಟಿ ಪಡೆಯಲು ಸಾಧ್ಯವಾಗಿಲ್ಲ",
|
||||
"Unable to retrieve all transactions": "Unable to retrieve all transactions",
|
||||
"Unable to retrieve reconciliation statements": "ಪುನಃಸಮಾಧಾನ ಹೇಳಿಕೆಗಳನ್ನು ಪಡೆಯಲು ಸಾಧ್ಯವಾಗಿಲ್ಲ",
|
||||
"Custom Date Range": "ಕಸ್ಟಮ್ ದಿನಾಂಕ ವ್ಯಾಪ್ತಿ",
|
||||
"Select Month": "ತಿಂಗಳು ಆಯ್ಕೆಮಾಡಿ",
|
||||
@@ -2121,6 +2153,7 @@
|
||||
"Show Confirmation Every Time": "ಪ್ರತಿ ಬಾರಿ ದೃಢೀಕರಣ ತೋರಿಸಿ",
|
||||
"Automatically Add Geolocation": "ಭೌಗೋಳಿಕ ಸ್ಥಾನವನ್ನು ಸ್ವಯಂ ಸೇರಿಸಿ",
|
||||
"Always Show Transaction Pictures": "ವಹಿವಾಟು ಚಿತ್ರಗಳನ್ನು ಯಾವಾಗಲೂ ತೋರಿಸಿ",
|
||||
"Insights & Explore Page": "Insights & Explore Page",
|
||||
"Account List Page": "ಖಾತೆ ಪಟ್ಟಿ ಪುಟ",
|
||||
"Accounts Included in Total": "ಒಟ್ಟು ಮೊತ್ತದಲ್ಲಿ ಒಳಗೊಂಡ ಖಾತೆಗಳು",
|
||||
"Exchange Rates Data Page": "ವಿನಿಮಯ ದರಗಳ ಪುಟ",
|
||||
|
||||
@@ -1434,6 +1434,7 @@
|
||||
"Update": "업데이트",
|
||||
"Refresh": "새로고침",
|
||||
"Clear": "지우기",
|
||||
"Clear All": "Clear All",
|
||||
"Generate": "생성",
|
||||
"Recognize": "인식",
|
||||
"Recognizing": "Recognizing",
|
||||
@@ -1490,6 +1491,9 @@
|
||||
"More": "더 보기",
|
||||
"All": "전체",
|
||||
"Partial": "부분",
|
||||
"WHERE": "WHERE",
|
||||
"AND": "AND",
|
||||
"OR": "OR",
|
||||
"Today": "오늘",
|
||||
"Yesterday": "어제",
|
||||
"Recent 7 days": "최근 7일",
|
||||
@@ -1527,6 +1531,19 @@
|
||||
"Not equal to": "같지 않음",
|
||||
"Between": "사이",
|
||||
"Not between": "사이 아님",
|
||||
"In": "In",
|
||||
"Has any": "Has any",
|
||||
"Has all": "Has all",
|
||||
"Not has any": "Not has any",
|
||||
"Not has all": "Not has all",
|
||||
"Is empty": "Is empty",
|
||||
"Is not empty": "Is not empty",
|
||||
"Contains": "Contains",
|
||||
"Not contains": "Not contains",
|
||||
"Starts with": "Starts with",
|
||||
"Not starts with": "Not starts with",
|
||||
"Ends with": "Ends with",
|
||||
"Not ends with": "Not ends with",
|
||||
"Pie Chart": "원형 차트",
|
||||
"Bar Chart": "막대 차트",
|
||||
"Radar Chart": "Radar Chart",
|
||||
@@ -1675,6 +1692,20 @@
|
||||
"Transaction Calendar": "거래 달력",
|
||||
"Transaction Details": "거래 세부사항",
|
||||
"Statistics & Analysis": "통계 및 분석",
|
||||
"Insights & Explore": "Insights & Explore",
|
||||
"Query": "Query",
|
||||
"Data Table": "Data Table",
|
||||
"Chart": "Chart",
|
||||
"New Explore": "New Explore",
|
||||
"Add Query": "Add Query",
|
||||
"Remove Query": "Remove Query",
|
||||
"Modify Query Name": "Modify Query Name",
|
||||
"Add Condition": "Add Condition",
|
||||
"Remove Condition": "Remove Condition",
|
||||
"No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.",
|
||||
"Editor": "Editor",
|
||||
"Expression": "Expression",
|
||||
"Failed to generate expression": "Failed to generate expression",
|
||||
"Account List": "계좌 목록",
|
||||
"This Week": "이번 주",
|
||||
"This Month": "이번 달",
|
||||
@@ -1997,6 +2028,7 @@
|
||||
"Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "이미지를 로드하지 못했습니다. \"domain\" 및 \"root_url\" 구성이 올바르게 설정되었는지 확인하십시오.",
|
||||
"Search transaction description": "거래 설명 검색",
|
||||
"Unable to retrieve transaction list": "거래 목록을 검색할 수 없습니다",
|
||||
"Unable to retrieve all transactions": "Unable to retrieve all transactions",
|
||||
"Unable to retrieve reconciliation statements": "조정 명세서를 검색할 수 없습니다",
|
||||
"Custom Date Range": "사용자 지정 날짜 범위",
|
||||
"Select Month": "월 선택",
|
||||
@@ -2121,6 +2153,7 @@
|
||||
"Show Confirmation Every Time": "매번 확인 표시",
|
||||
"Automatically Add Geolocation": "지리적 위치 자동 추가",
|
||||
"Always Show Transaction Pictures": "거래 사진 항상 표시",
|
||||
"Insights & Explore Page": "Insights & Explore Page",
|
||||
"Account List Page": "계좌 목록 페이지",
|
||||
"Accounts Included in Total": "총계에 포함된 계좌",
|
||||
"Exchange Rates Data Page": "환율 데이터 페이지",
|
||||
|
||||
@@ -1434,6 +1434,7 @@
|
||||
"Update": "Bijwerken",
|
||||
"Refresh": "Vernieuwen",
|
||||
"Clear": "Wissen",
|
||||
"Clear All": "Clear All",
|
||||
"Generate": "Genereren",
|
||||
"Recognize": "Recognize",
|
||||
"Recognizing": "Recognizing",
|
||||
@@ -1490,6 +1491,9 @@
|
||||
"More": "Meer",
|
||||
"All": "Alles",
|
||||
"Partial": "Gedeeltelijk",
|
||||
"WHERE": "WHERE",
|
||||
"AND": "AND",
|
||||
"OR": "OR",
|
||||
"Today": "Vandaag",
|
||||
"Yesterday": "Gisteren",
|
||||
"Recent 7 days": "Afgelopen 7 dagen",
|
||||
@@ -1527,6 +1531,19 @@
|
||||
"Not equal to": "Niet gelijk aan",
|
||||
"Between": "Tussen",
|
||||
"Not between": "Niet tussen",
|
||||
"In": "In",
|
||||
"Has any": "Has any",
|
||||
"Has all": "Has all",
|
||||
"Not has any": "Not has any",
|
||||
"Not has all": "Not has all",
|
||||
"Is empty": "Is empty",
|
||||
"Is not empty": "Is not empty",
|
||||
"Contains": "Contains",
|
||||
"Not contains": "Not contains",
|
||||
"Starts with": "Starts with",
|
||||
"Not starts with": "Not starts with",
|
||||
"Ends with": "Ends with",
|
||||
"Not ends with": "Not ends with",
|
||||
"Pie Chart": "Cirkeldiagram",
|
||||
"Bar Chart": "Balkdiagram",
|
||||
"Radar Chart": "Radar Chart",
|
||||
@@ -1675,6 +1692,20 @@
|
||||
"Transaction Calendar": "Transactiekalender",
|
||||
"Transaction Details": "Transactiedetails",
|
||||
"Statistics & Analysis": "Statistieken & Analyse",
|
||||
"Insights & Explore": "Insights & Explore",
|
||||
"Query": "Query",
|
||||
"Data Table": "Data Table",
|
||||
"Chart": "Chart",
|
||||
"New Explore": "New Explore",
|
||||
"Add Query": "Add Query",
|
||||
"Remove Query": "Remove Query",
|
||||
"Modify Query Name": "Modify Query Name",
|
||||
"Add Condition": "Add Condition",
|
||||
"Remove Condition": "Remove Condition",
|
||||
"No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.",
|
||||
"Editor": "Editor",
|
||||
"Expression": "Expression",
|
||||
"Failed to generate expression": "Failed to generate expression",
|
||||
"Account List": "Rekeningenlijst",
|
||||
"This Week": "Deze week",
|
||||
"This Month": "Deze maand",
|
||||
@@ -1997,6 +2028,7 @@
|
||||
"Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "Afbeelding laden mislukt; controleer of de configuratie-waarden \"domain\" en \"root_url\" correct zijn ingesteld.",
|
||||
"Search transaction description": "Transactiebeschrijving zoeken",
|
||||
"Unable to retrieve transaction list": "Kan transactielijst niet ophalen",
|
||||
"Unable to retrieve all transactions": "Unable to retrieve all transactions",
|
||||
"Unable to retrieve reconciliation statements": "Kan afstemmingsrapporten niet ophalen",
|
||||
"Custom Date Range": "Aangepast datumbereik",
|
||||
"Select Month": "Select Month",
|
||||
@@ -2121,6 +2153,7 @@
|
||||
"Show Confirmation Every Time": "Elke keer bevestiging tonen",
|
||||
"Automatically Add Geolocation": "Geolocatie automatisch toevoegen",
|
||||
"Always Show Transaction Pictures": "Transactie-afbeeldingen altijd tonen",
|
||||
"Insights & Explore Page": "Insights & Explore Page",
|
||||
"Account List Page": "Rekeningenpagina",
|
||||
"Accounts Included in Total": "Rekeningen opgenomen in totaal",
|
||||
"Exchange Rates Data Page": "Wisselkoersgegevenspagina",
|
||||
|
||||
@@ -1434,6 +1434,7 @@
|
||||
"Update": "Atualizar",
|
||||
"Refresh": "Atualizar",
|
||||
"Clear": "Limpar",
|
||||
"Clear All": "Clear All",
|
||||
"Generate": "Generate",
|
||||
"Recognize": "Recognize",
|
||||
"Recognizing": "Recognizing",
|
||||
@@ -1490,6 +1491,9 @@
|
||||
"More": "Mais",
|
||||
"All": "Todos",
|
||||
"Partial": "Partial",
|
||||
"WHERE": "WHERE",
|
||||
"AND": "AND",
|
||||
"OR": "OR",
|
||||
"Today": "Hoje",
|
||||
"Yesterday": "Ontem",
|
||||
"Recent 7 days": "Últimos 7 dias",
|
||||
@@ -1527,6 +1531,19 @@
|
||||
"Not equal to": "Diferente de",
|
||||
"Between": "Entre",
|
||||
"Not between": "Não entre",
|
||||
"In": "In",
|
||||
"Has any": "Has any",
|
||||
"Has all": "Has all",
|
||||
"Not has any": "Not has any",
|
||||
"Not has all": "Not has all",
|
||||
"Is empty": "Is empty",
|
||||
"Is not empty": "Is not empty",
|
||||
"Contains": "Contains",
|
||||
"Not contains": "Not contains",
|
||||
"Starts with": "Starts with",
|
||||
"Not starts with": "Not starts with",
|
||||
"Ends with": "Ends with",
|
||||
"Not ends with": "Not ends with",
|
||||
"Pie Chart": "Gráfico de Pizza",
|
||||
"Bar Chart": "Gráfico de Barras",
|
||||
"Radar Chart": "Radar Chart",
|
||||
@@ -1675,6 +1692,20 @@
|
||||
"Transaction Calendar": "Calendário de Transações",
|
||||
"Transaction Details": "Detalhes da Transação",
|
||||
"Statistics & Analysis": "Estatísticas e Análise",
|
||||
"Insights & Explore": "Insights & Explore",
|
||||
"Query": "Query",
|
||||
"Data Table": "Data Table",
|
||||
"Chart": "Chart",
|
||||
"New Explore": "New Explore",
|
||||
"Add Query": "Add Query",
|
||||
"Remove Query": "Remove Query",
|
||||
"Modify Query Name": "Modify Query Name",
|
||||
"Add Condition": "Add Condition",
|
||||
"Remove Condition": "Remove Condition",
|
||||
"No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.",
|
||||
"Editor": "Editor",
|
||||
"Expression": "Expression",
|
||||
"Failed to generate expression": "Failed to generate expression",
|
||||
"Account List": "Lista de Contas",
|
||||
"This Week": "Esta Semana",
|
||||
"This Month": "Este Mês",
|
||||
@@ -1997,6 +2028,7 @@
|
||||
"Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "Falha ao carregar a imagem, por favor verifique se as configurações \"domain\" e \"root_url\" estão configuradas corretamente.",
|
||||
"Search transaction description": "Pesquisar descrição da transação",
|
||||
"Unable to retrieve transaction list": "Incapaz de recuperar a lista de transações",
|
||||
"Unable to retrieve all transactions": "Unable to retrieve all transactions",
|
||||
"Unable to retrieve reconciliation statements": "Unable to retrieve reconciliation statements",
|
||||
"Custom Date Range": "Intervalo de Datas Personalizado",
|
||||
"Select Month": "Select Month",
|
||||
@@ -2121,6 +2153,7 @@
|
||||
"Show Confirmation Every Time": "Mostrar Confirmação Toda Vez",
|
||||
"Automatically Add Geolocation": "Adicionar Geolocalização Automaticamente",
|
||||
"Always Show Transaction Pictures": "Sempre Mostrar Imagens de Transações",
|
||||
"Insights & Explore Page": "Insights & Explore Page",
|
||||
"Account List Page": "Account List Page",
|
||||
"Accounts Included in Total": "Accounts Included in Total",
|
||||
"Exchange Rates Data Page": "Página de Dados de Taxas de Câmbio",
|
||||
|
||||
@@ -1434,6 +1434,7 @@
|
||||
"Update": "Обновить",
|
||||
"Refresh": "Обновить",
|
||||
"Clear": "Очистить",
|
||||
"Clear All": "Clear All",
|
||||
"Generate": "Generate",
|
||||
"Recognize": "Recognize",
|
||||
"Recognizing": "Recognizing",
|
||||
@@ -1490,6 +1491,9 @@
|
||||
"More": "Еще",
|
||||
"All": "Все",
|
||||
"Partial": "Partial",
|
||||
"WHERE": "WHERE",
|
||||
"AND": "AND",
|
||||
"OR": "OR",
|
||||
"Today": "Сегодня",
|
||||
"Yesterday": "Вчера",
|
||||
"Recent 7 days": "Последние 7 дней",
|
||||
@@ -1527,6 +1531,19 @@
|
||||
"Not equal to": "Не равно",
|
||||
"Between": "Между",
|
||||
"Not between": "Не между",
|
||||
"In": "In",
|
||||
"Has any": "Has any",
|
||||
"Has all": "Has all",
|
||||
"Not has any": "Not has any",
|
||||
"Not has all": "Not has all",
|
||||
"Is empty": "Is empty",
|
||||
"Is not empty": "Is not empty",
|
||||
"Contains": "Contains",
|
||||
"Not contains": "Not contains",
|
||||
"Starts with": "Starts with",
|
||||
"Not starts with": "Not starts with",
|
||||
"Ends with": "Ends with",
|
||||
"Not ends with": "Not ends with",
|
||||
"Pie Chart": "Круговая диаграмма",
|
||||
"Bar Chart": "Гистограмма",
|
||||
"Radar Chart": "Radar Chart",
|
||||
@@ -1675,6 +1692,20 @@
|
||||
"Transaction Calendar": "Transaction Calendar",
|
||||
"Transaction Details": "Детали транзакции",
|
||||
"Statistics & Analysis": "Статистика и анализ",
|
||||
"Insights & Explore": "Insights & Explore",
|
||||
"Query": "Query",
|
||||
"Data Table": "Data Table",
|
||||
"Chart": "Chart",
|
||||
"New Explore": "New Explore",
|
||||
"Add Query": "Add Query",
|
||||
"Remove Query": "Remove Query",
|
||||
"Modify Query Name": "Modify Query Name",
|
||||
"Add Condition": "Add Condition",
|
||||
"Remove Condition": "Remove Condition",
|
||||
"No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.",
|
||||
"Editor": "Editor",
|
||||
"Expression": "Expression",
|
||||
"Failed to generate expression": "Failed to generate expression",
|
||||
"Account List": "Список счетов",
|
||||
"This Week": "На этой неделе",
|
||||
"This Month": "В этом месяце",
|
||||
@@ -1997,6 +2028,7 @@
|
||||
"Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.",
|
||||
"Search transaction description": "Поиск описания транзакции",
|
||||
"Unable to retrieve transaction list": "Не удалось получить список транзакций",
|
||||
"Unable to retrieve all transactions": "Unable to retrieve all transactions",
|
||||
"Unable to retrieve reconciliation statements": "Unable to retrieve reconciliation statements",
|
||||
"Custom Date Range": "Пользовательский диапазон дат",
|
||||
"Select Month": "Select Month",
|
||||
@@ -2121,6 +2153,7 @@
|
||||
"Show Confirmation Every Time": "Показывать подтверждение каждый раз",
|
||||
"Automatically Add Geolocation": "Автоматически добавлять геолокацию",
|
||||
"Always Show Transaction Pictures": "Always Show Transaction Pictures",
|
||||
"Insights & Explore Page": "Insights & Explore Page",
|
||||
"Account List Page": "Account List Page",
|
||||
"Accounts Included in Total": "Accounts Included in Total",
|
||||
"Exchange Rates Data Page": "Страница данных о курсах валют",
|
||||
|
||||
@@ -1434,6 +1434,7 @@
|
||||
"Update": "อัปเดต",
|
||||
"Refresh": "รีเฟรช",
|
||||
"Clear": "ล้าง",
|
||||
"Clear All": "Clear All",
|
||||
"Generate": "สร้าง",
|
||||
"Recognize": "จดจำ",
|
||||
"Recognizing": "Recognizing",
|
||||
@@ -1490,6 +1491,9 @@
|
||||
"More": "เพิ่มเติม",
|
||||
"All": "ทั้งหมด",
|
||||
"Partial": "บางส่วน",
|
||||
"WHERE": "WHERE",
|
||||
"AND": "AND",
|
||||
"OR": "OR",
|
||||
"Today": "วันนี้",
|
||||
"Yesterday": "เมื่อวาน",
|
||||
"Recent 7 days": "7 วันที่ผ่านมา",
|
||||
@@ -1527,6 +1531,19 @@
|
||||
"Not equal to": "ไม่เท่ากับ",
|
||||
"Between": "ระหว่าง",
|
||||
"Not between": "ไม่อยู่ระหว่าง",
|
||||
"In": "In",
|
||||
"Has any": "Has any",
|
||||
"Has all": "Has all",
|
||||
"Not has any": "Not has any",
|
||||
"Not has all": "Not has all",
|
||||
"Is empty": "Is empty",
|
||||
"Is not empty": "Is not empty",
|
||||
"Contains": "Contains",
|
||||
"Not contains": "Not contains",
|
||||
"Starts with": "Starts with",
|
||||
"Not starts with": "Not starts with",
|
||||
"Ends with": "Ends with",
|
||||
"Not ends with": "Not ends with",
|
||||
"Pie Chart": "กราฟวงกลม",
|
||||
"Bar Chart": "กราฟแท่ง",
|
||||
"Radar Chart": "Radar Chart",
|
||||
@@ -1675,6 +1692,20 @@
|
||||
"Transaction Calendar": "ปฏิทินธุรกรรม",
|
||||
"Transaction Details": "รายละเอียดธุรกรรม",
|
||||
"Statistics & Analysis": "สถิติ & การวิเคราะห์",
|
||||
"Insights & Explore": "Insights & Explore",
|
||||
"Query": "Query",
|
||||
"Data Table": "Data Table",
|
||||
"Chart": "Chart",
|
||||
"New Explore": "New Explore",
|
||||
"Add Query": "Add Query",
|
||||
"Remove Query": "Remove Query",
|
||||
"Modify Query Name": "Modify Query Name",
|
||||
"Add Condition": "Add Condition",
|
||||
"Remove Condition": "Remove Condition",
|
||||
"No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.",
|
||||
"Editor": "Editor",
|
||||
"Expression": "Expression",
|
||||
"Failed to generate expression": "Failed to generate expression",
|
||||
"Account List": "รายการบัญชี",
|
||||
"This Week": "สัปดาห์นี้",
|
||||
"This Month": "เดือนนี้",
|
||||
@@ -1997,6 +2028,7 @@
|
||||
"Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "โหลดรูปภาพล้มเหลว กรุณาตรวจสอบการตั้งค่า \"domain\" และ \"root_url\"",
|
||||
"Search transaction description": "ค้นหาคำอธิบายรายการ",
|
||||
"Unable to retrieve transaction list": "ไม่สามารถดึงรายการได้",
|
||||
"Unable to retrieve all transactions": "Unable to retrieve all transactions",
|
||||
"Unable to retrieve reconciliation statements": "ไม่สามารถดึงงบกระทบยอดได้",
|
||||
"Custom Date Range": "ช่วงวันที่กำหนดเอง",
|
||||
"Select Month": "เลือกเดือน",
|
||||
@@ -2121,6 +2153,7 @@
|
||||
"Show Confirmation Every Time": "แสดงการยืนยันทุกครั้ง",
|
||||
"Automatically Add Geolocation": "เพิ่มตำแหน่งทางภูมิศาสตร์อัตโนมัติ",
|
||||
"Always Show Transaction Pictures": "แสดงรูปภาพรายการเสมอ",
|
||||
"Insights & Explore Page": "Insights & Explore Page",
|
||||
"Account List Page": "หน้าบัญชี",
|
||||
"Accounts Included in Total": "บัญชีที่รวมในผลรวม",
|
||||
"Exchange Rates Data Page": "หน้าข้อมูลอัตราแลกเปลี่ยน",
|
||||
|
||||
@@ -1434,6 +1434,7 @@
|
||||
"Update": "Güncelle",
|
||||
"Refresh": "Yenile",
|
||||
"Clear": "Temizle",
|
||||
"Clear All": "Clear All",
|
||||
"Generate": "Oluştur",
|
||||
"Recognize": "Tanı",
|
||||
"Recognizing": "Tanınıyor",
|
||||
@@ -1490,6 +1491,9 @@
|
||||
"More": "Daha Fazla",
|
||||
"All": "Tümü",
|
||||
"Partial": "Kısmi",
|
||||
"WHERE": "WHERE",
|
||||
"AND": "AND",
|
||||
"OR": "OR",
|
||||
"Today": "Bugün",
|
||||
"Yesterday": "Dün",
|
||||
"Recent 7 days": "Son 7 gün",
|
||||
@@ -1527,6 +1531,19 @@
|
||||
"Not equal to": "Eşit değildir",
|
||||
"Between": "Arasında",
|
||||
"Not between": "Arasında değil",
|
||||
"In": "In",
|
||||
"Has any": "Has any",
|
||||
"Has all": "Has all",
|
||||
"Not has any": "Not has any",
|
||||
"Not has all": "Not has all",
|
||||
"Is empty": "Is empty",
|
||||
"Is not empty": "Is not empty",
|
||||
"Contains": "Contains",
|
||||
"Not contains": "Not contains",
|
||||
"Starts with": "Starts with",
|
||||
"Not starts with": "Not starts with",
|
||||
"Ends with": "Ends with",
|
||||
"Not ends with": "Not ends with",
|
||||
"Pie Chart": "Pasta Grafiği",
|
||||
"Bar Chart": "Çubuk Grafik",
|
||||
"Radar Chart": "Radar Grafiği",
|
||||
@@ -1675,6 +1692,20 @@
|
||||
"Transaction Calendar": "İşlem Takvimi",
|
||||
"Transaction Details": "İşlem Detayları",
|
||||
"Statistics & Analysis": "İstatistik & Analiz",
|
||||
"Insights & Explore": "Insights & Explore",
|
||||
"Query": "Query",
|
||||
"Data Table": "Data Table",
|
||||
"Chart": "Chart",
|
||||
"New Explore": "New Explore",
|
||||
"Add Query": "Add Query",
|
||||
"Remove Query": "Remove Query",
|
||||
"Modify Query Name": "Modify Query Name",
|
||||
"Add Condition": "Add Condition",
|
||||
"Remove Condition": "Remove Condition",
|
||||
"No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.",
|
||||
"Editor": "Editor",
|
||||
"Expression": "Expression",
|
||||
"Failed to generate expression": "Failed to generate expression",
|
||||
"Account List": "Hesap Listesi",
|
||||
"This Week": "Bu Hafta",
|
||||
"This Month": "Bu Ay",
|
||||
@@ -1997,6 +2028,7 @@
|
||||
"Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "Görüntü yüklenemedi, lütfen \"domain\" ve \"root_url\" ayarlarının doğru yapılandırıldığını kontrol edin.",
|
||||
"Search transaction description": "İşlem açıklamasını ara",
|
||||
"Unable to retrieve transaction list": "İşlem listesi alınamadı",
|
||||
"Unable to retrieve all transactions": "Unable to retrieve all transactions",
|
||||
"Unable to retrieve reconciliation statements": "Mutabakat ekstreleri alınamadı",
|
||||
"Custom Date Range": "Özel Tarih Aralığı",
|
||||
"Select Month": "Ay Seç",
|
||||
@@ -2121,6 +2153,7 @@
|
||||
"Show Confirmation Every Time": "Her Seferinde Onay Göster",
|
||||
"Automatically Add Geolocation": "Otomatik Olarak Konum Ekle",
|
||||
"Always Show Transaction Pictures": "İşlem Resimlerini Her Zaman Göster",
|
||||
"Insights & Explore Page": "Insights & Explore Page",
|
||||
"Account List Page": "Hesap Listesi Sayfası",
|
||||
"Accounts Included in Total": "Toplama Dahil Edilen Hesaplar",
|
||||
"Exchange Rates Data Page": "Döviz Kuru Verileri Sayfası",
|
||||
|
||||
@@ -1434,6 +1434,7 @@
|
||||
"Update": "Оновити",
|
||||
"Refresh": "Оновити",
|
||||
"Clear": "Очистити",
|
||||
"Clear All": "Clear All",
|
||||
"Generate": "Generate",
|
||||
"Recognize": "Recognize",
|
||||
"Recognizing": "Recognizing",
|
||||
@@ -1490,6 +1491,9 @@
|
||||
"More": "Більше",
|
||||
"All": "Усе",
|
||||
"Partial": "Partial",
|
||||
"WHERE": "WHERE",
|
||||
"AND": "AND",
|
||||
"OR": "OR",
|
||||
"Today": "Сьогодні",
|
||||
"Yesterday": "Вчора",
|
||||
"Recent 7 days": "Останні 7 днів",
|
||||
@@ -1527,6 +1531,19 @@
|
||||
"Not equal to": "Не дорівнює",
|
||||
"Between": "Між",
|
||||
"Not between": "Не між",
|
||||
"In": "In",
|
||||
"Has any": "Has any",
|
||||
"Has all": "Has all",
|
||||
"Not has any": "Not has any",
|
||||
"Not has all": "Not has all",
|
||||
"Is empty": "Is empty",
|
||||
"Is not empty": "Is not empty",
|
||||
"Contains": "Contains",
|
||||
"Not contains": "Not contains",
|
||||
"Starts with": "Starts with",
|
||||
"Not starts with": "Not starts with",
|
||||
"Ends with": "Ends with",
|
||||
"Not ends with": "Not ends with",
|
||||
"Pie Chart": "Кругова діаграма",
|
||||
"Bar Chart": "Гістограма",
|
||||
"Radar Chart": "Radar Chart",
|
||||
@@ -1675,6 +1692,20 @@
|
||||
"Transaction Calendar": "Transaction Calendar",
|
||||
"Transaction Details": "Деталі по транзакціях",
|
||||
"Statistics & Analysis": "Статистика та аналіз",
|
||||
"Insights & Explore": "Insights & Explore",
|
||||
"Query": "Query",
|
||||
"Data Table": "Data Table",
|
||||
"Chart": "Chart",
|
||||
"New Explore": "New Explore",
|
||||
"Add Query": "Add Query",
|
||||
"Remove Query": "Remove Query",
|
||||
"Modify Query Name": "Modify Query Name",
|
||||
"Add Condition": "Add Condition",
|
||||
"Remove Condition": "Remove Condition",
|
||||
"No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.",
|
||||
"Editor": "Editor",
|
||||
"Expression": "Expression",
|
||||
"Failed to generate expression": "Failed to generate expression",
|
||||
"Account List": "Список рахунків",
|
||||
"This Week": "Цього тижня",
|
||||
"This Month": "Цього місяця",
|
||||
@@ -1997,6 +2028,7 @@
|
||||
"Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.",
|
||||
"Search transaction description": "Пошук за описом транзакції",
|
||||
"Unable to retrieve transaction list": "Не вдалося отримати список транзакцій",
|
||||
"Unable to retrieve all transactions": "Unable to retrieve all transactions",
|
||||
"Unable to retrieve reconciliation statements": "Unable to retrieve reconciliation statements",
|
||||
"Custom Date Range": "Користувацький діапазон дат",
|
||||
"Select Month": "Select Month",
|
||||
@@ -2121,6 +2153,7 @@
|
||||
"Show Confirmation Every Time": "Показувати підтвердження щоразу",
|
||||
"Automatically Add Geolocation": "Автоматично додавати геолокацію",
|
||||
"Always Show Transaction Pictures": "Always Show Transaction Pictures",
|
||||
"Insights & Explore Page": "Insights & Explore Page",
|
||||
"Account List Page": "Account List Page",
|
||||
"Accounts Included in Total": "Accounts Included in Total",
|
||||
"Exchange Rates Data Page": "Сторінка курсів валют",
|
||||
|
||||
@@ -1434,6 +1434,7 @@
|
||||
"Update": "Cập nhật",
|
||||
"Refresh": "Làm mới",
|
||||
"Clear": "Xóa",
|
||||
"Clear All": "Clear All",
|
||||
"Generate": "Generate",
|
||||
"Recognize": "Recognize",
|
||||
"Recognizing": "Recognizing",
|
||||
@@ -1490,6 +1491,9 @@
|
||||
"More": "Thêm",
|
||||
"All": "Tất cả",
|
||||
"Partial": "Partial",
|
||||
"WHERE": "WHERE",
|
||||
"AND": "AND",
|
||||
"OR": "OR",
|
||||
"Today": "Hôm nay",
|
||||
"Yesterday": "Hôm qua",
|
||||
"Recent 7 days": "7 ngày gần đây",
|
||||
@@ -1527,6 +1531,19 @@
|
||||
"Not equal to": "Không bằng",
|
||||
"Between": "Giữa",
|
||||
"Not between": "Không giữa",
|
||||
"In": "In",
|
||||
"Has any": "Has any",
|
||||
"Has all": "Has all",
|
||||
"Not has any": "Not has any",
|
||||
"Not has all": "Not has all",
|
||||
"Is empty": "Is empty",
|
||||
"Is not empty": "Is not empty",
|
||||
"Contains": "Contains",
|
||||
"Not contains": "Not contains",
|
||||
"Starts with": "Starts with",
|
||||
"Not starts with": "Not starts with",
|
||||
"Ends with": "Ends with",
|
||||
"Not ends with": "Not ends with",
|
||||
"Pie Chart": "Biểu đồ tròn",
|
||||
"Bar Chart": "Biểu đồ cột",
|
||||
"Radar Chart": "Radar Chart",
|
||||
@@ -1675,6 +1692,20 @@
|
||||
"Transaction Calendar": "Transaction Calendar",
|
||||
"Transaction Details": "Chi tiết giao dịch",
|
||||
"Statistics & Analysis": "Thống kê & Phân tích",
|
||||
"Insights & Explore": "Insights & Explore",
|
||||
"Query": "Query",
|
||||
"Data Table": "Data Table",
|
||||
"Chart": "Chart",
|
||||
"New Explore": "New Explore",
|
||||
"Add Query": "Add Query",
|
||||
"Remove Query": "Remove Query",
|
||||
"Modify Query Name": "Modify Query Name",
|
||||
"Add Condition": "Add Condition",
|
||||
"Remove Condition": "Remove Condition",
|
||||
"No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.",
|
||||
"Editor": "Editor",
|
||||
"Expression": "Expression",
|
||||
"Failed to generate expression": "Failed to generate expression",
|
||||
"Account List": "Danh sách tài khoản",
|
||||
"This Week": "Tuần này",
|
||||
"This Month": "Tháng này",
|
||||
@@ -1997,6 +2028,7 @@
|
||||
"Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.",
|
||||
"Search transaction description": "Tìm kiếm mô tả giao dịch",
|
||||
"Unable to retrieve transaction list": "Không thể lấy danh sách giao dịch",
|
||||
"Unable to retrieve all transactions": "Unable to retrieve all transactions",
|
||||
"Unable to retrieve reconciliation statements": "Unable to retrieve reconciliation statements",
|
||||
"Custom Date Range": "Phạm vi ngày tùy chỉnh",
|
||||
"Select Month": "Select Month",
|
||||
@@ -2121,6 +2153,7 @@
|
||||
"Show Confirmation Every Time": "Hiển thị xác nhận mỗi lần",
|
||||
"Automatically Add Geolocation": "Tự động thêm vị trí địa lý",
|
||||
"Always Show Transaction Pictures": "Always Show Transaction Pictures",
|
||||
"Insights & Explore Page": "Insights & Explore Page",
|
||||
"Account List Page": "Account List Page",
|
||||
"Accounts Included in Total": "Accounts Included in Total",
|
||||
"Exchange Rates Data Page": "Trang dữ liệu tỷ giá hối đoái",
|
||||
|
||||
@@ -1434,6 +1434,7 @@
|
||||
"Update": "更新",
|
||||
"Refresh": "刷新",
|
||||
"Clear": "清除",
|
||||
"Clear All": "全部清除",
|
||||
"Generate": "生成",
|
||||
"Recognize": "识别",
|
||||
"Recognizing": "正在识别",
|
||||
@@ -1490,6 +1491,9 @@
|
||||
"More": "更多",
|
||||
"All": "全部",
|
||||
"Partial": "部分",
|
||||
"WHERE": "条件",
|
||||
"AND": "与",
|
||||
"OR": "或",
|
||||
"Today": "今天",
|
||||
"Yesterday": "昨天",
|
||||
"Recent 7 days": "最近7天",
|
||||
@@ -1527,6 +1531,19 @@
|
||||
"Not equal to": "不等于",
|
||||
"Between": "介于",
|
||||
"Not between": "不介于",
|
||||
"In": "在范围内",
|
||||
"Has any": "包含任意",
|
||||
"Has all": "包含全部",
|
||||
"Not has any": "不包含任意",
|
||||
"Not has all": "不包含全部",
|
||||
"Is empty": "为空",
|
||||
"Is not empty": "不为空",
|
||||
"Contains": "包含",
|
||||
"Not contains": "不包含",
|
||||
"Starts with": "开头是",
|
||||
"Not starts with": "开头不是",
|
||||
"Ends with": "结尾是",
|
||||
"Not ends with": "结尾不是",
|
||||
"Pie Chart": "饼图",
|
||||
"Bar Chart": "条形图",
|
||||
"Radar Chart": "雷达图",
|
||||
@@ -1675,6 +1692,20 @@
|
||||
"Transaction Calendar": "交易日历",
|
||||
"Transaction Details": "交易详情",
|
||||
"Statistics & Analysis": "统计分析",
|
||||
"Insights & Explore": "洞察探索",
|
||||
"Query": "查询",
|
||||
"Data Table": "数据表格",
|
||||
"Chart": "图表",
|
||||
"New Explore": "新的探索",
|
||||
"Add Query": "添加查询",
|
||||
"Remove Query": "移除查询",
|
||||
"Modify Query Name": "修改查询名称",
|
||||
"Add Condition": "添加条件",
|
||||
"Remove Condition": "移除条件",
|
||||
"No conditions defined. All transactions will match.": "没有定义条件。所有交易都会匹配。",
|
||||
"Editor": "编辑器",
|
||||
"Expression": "表达式",
|
||||
"Failed to generate expression": "生成表达式失败",
|
||||
"Account List": "账户列表",
|
||||
"This Week": "本周",
|
||||
"This Month": "本月",
|
||||
@@ -1997,6 +2028,7 @@
|
||||
"Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "无法加载图片,请检查配置 \"domain\" 和 \"root_url\" 是否设置正确。",
|
||||
"Search transaction description": "搜索交易描述",
|
||||
"Unable to retrieve transaction list": "无法获取交易列表",
|
||||
"Unable to retrieve all transactions": "无法获取所有交易",
|
||||
"Unable to retrieve reconciliation statements": "无法获取对账单",
|
||||
"Custom Date Range": "自定义日期范围",
|
||||
"Select Month": "选择月份",
|
||||
@@ -2121,6 +2153,7 @@
|
||||
"Show Confirmation Every Time": "每次提示确认",
|
||||
"Automatically Add Geolocation": "自动添加地理位置",
|
||||
"Always Show Transaction Pictures": "总是显示交易图片",
|
||||
"Insights & Explore Page": "洞察探索页面",
|
||||
"Account List Page": "账户列表页面",
|
||||
"Accounts Included in Total": "计入总金额的账户",
|
||||
"Exchange Rates Data Page": "汇率数据页面",
|
||||
|
||||
@@ -1434,6 +1434,7 @@
|
||||
"Update": "更新",
|
||||
"Refresh": "重新載入",
|
||||
"Clear": "清除",
|
||||
"Clear All": "全部清除",
|
||||
"Generate": "產生",
|
||||
"Recognize": "識別",
|
||||
"Recognizing": "正在識別",
|
||||
@@ -1490,6 +1491,9 @@
|
||||
"More": "更多",
|
||||
"All": "全部",
|
||||
"Partial": "部分",
|
||||
"WHERE": "條件",
|
||||
"AND": "且",
|
||||
"OR": "或",
|
||||
"Today": "今天",
|
||||
"Yesterday": "昨天",
|
||||
"Recent 7 days": "最近7天",
|
||||
@@ -1527,6 +1531,19 @@
|
||||
"Not equal to": "不等於",
|
||||
"Between": "介於",
|
||||
"Not between": "不介於",
|
||||
"In": "包含於",
|
||||
"Has any": "包含任一",
|
||||
"Has all": "包含所有",
|
||||
"Not has any": "不包含任一",
|
||||
"Not has all": "不包含所有",
|
||||
"Is empty": "為空",
|
||||
"Is not empty": "不為空",
|
||||
"Contains": "包含",
|
||||
"Not contains": "不包含",
|
||||
"Starts with": "開頭是",
|
||||
"Not starts with": "開頭不是",
|
||||
"Ends with": "結尾是",
|
||||
"Not ends with": "結尾不是",
|
||||
"Pie Chart": "圓餅圖",
|
||||
"Bar Chart": "長條圖",
|
||||
"Radar Chart": "雷達圖",
|
||||
@@ -1675,6 +1692,20 @@
|
||||
"Transaction Calendar": "交易日曆",
|
||||
"Transaction Details": "交易詳情",
|
||||
"Statistics & Analysis": "統計分析",
|
||||
"Insights & Explore": "洞察探索",
|
||||
"Query": "查詢",
|
||||
"Data Table": "資料表格",
|
||||
"Chart": "圖表",
|
||||
"New Explore": "新的探索",
|
||||
"Add Query": "新增查詢",
|
||||
"Remove Query": "移除查詢",
|
||||
"Modify Query Name": "修改查詢名稱",
|
||||
"Add Condition": "新增條件",
|
||||
"Remove Condition": "移除條件",
|
||||
"No conditions defined. All transactions will match.": "沒有定義條件。所有交易都符合。",
|
||||
"Editor": "編輯器",
|
||||
"Expression": "表達式",
|
||||
"Failed to generate expression": "產生表達式失敗",
|
||||
"Account List": "帳戶清單",
|
||||
"This Week": "本週",
|
||||
"This Month": "本月",
|
||||
@@ -1997,6 +2028,7 @@
|
||||
"Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "無法載入圖片,請檢查設定 \"domain\" 和 \"root_url\" 是否設定正確。",
|
||||
"Search transaction description": "搜尋交易描述",
|
||||
"Unable to retrieve transaction list": "無法取得交易清單",
|
||||
"Unable to retrieve all transactions": "無法取得所有交易",
|
||||
"Unable to retrieve reconciliation statements": "無法取得對帳單",
|
||||
"Custom Date Range": "自訂日期範圍",
|
||||
"Select Month": "選擇月份",
|
||||
@@ -2121,6 +2153,7 @@
|
||||
"Show Confirmation Every Time": "每次提示確認",
|
||||
"Automatically Add Geolocation": "自動新增地理位置",
|
||||
"Always Show Transaction Pictures": "總是顯示交易圖片",
|
||||
"Insights & Explore Page": "洞察探索頁面",
|
||||
"Account List Page": "帳戶清單頁面",
|
||||
"Accounts Included in Total": "計入總金額的帳戶",
|
||||
"Exchange Rates Data Page": "匯率資料頁面",
|
||||
|
||||
@@ -0,0 +1,902 @@
|
||||
import { type PartialRecord, itemAndIndex, keysIfValueEquals } from '@/core/base.ts';
|
||||
import { AccountType } from '@/core/account.ts';
|
||||
import { TransactionType } from '@/core/transaction.ts';
|
||||
import {
|
||||
TransactionExploreConditionRelation,
|
||||
TransactionExploreConditionRelationPriority,
|
||||
TransactionExploreConditionFieldType,
|
||||
TransactionExploreConditionField,
|
||||
TransactionExploreConditionOperatorType,
|
||||
TransactionExploreConditionOperator
|
||||
} from '@/core/explore.ts';
|
||||
|
||||
import { Account } from '@/models/account.ts';
|
||||
import { TransactionCategory } from '@/models/transaction_category.ts';
|
||||
import { TransactionTag } from '@/models/transaction_tag.ts';
|
||||
import { type TransactionInsightDataItem } from '@/models/transaction.ts';
|
||||
|
||||
interface ExpressionNode {
|
||||
textualExpression: string;
|
||||
operator?: TransactionExploreConditionRelation;
|
||||
}
|
||||
|
||||
export class TransactionExploreQuery {
|
||||
public name: string;
|
||||
public conditions: TransactionExploreConditionWithRelation[];
|
||||
|
||||
private constructor(name: string, conditions: TransactionExploreConditionWithRelation[]) {
|
||||
this.name = name;
|
||||
this.conditions = conditions;
|
||||
}
|
||||
|
||||
public addNewCondition(field: TransactionExploreConditionField, isFirst: boolean): TransactionExploreConditionWithRelation {
|
||||
let condition: TransactionExploreCondition;
|
||||
|
||||
switch (field) {
|
||||
case TransactionExploreConditionField.TransactionType:
|
||||
condition = new TransactionExploreTransactionTypeCondition([ TransactionType.Expense, TransactionType.Income, TransactionType.Transfer ]);
|
||||
break;
|
||||
case TransactionExploreConditionField.TransactionCategory:
|
||||
condition = new TransactionExploreTransactionCategoryCondition([]);
|
||||
break;
|
||||
case TransactionExploreConditionField.SourceAccount:
|
||||
condition = new TransactionExploreSourceAccountCondition([]);
|
||||
break;
|
||||
case TransactionExploreConditionField.DestinationAccount:
|
||||
condition = new TransactionExploreDestinationAccountCondition([]);
|
||||
break;
|
||||
case TransactionExploreConditionField.SourceAmount:
|
||||
condition = new TransactionExploreSourceAmountCondition(TransactionExploreConditionOperatorType.Between, [0, 0]);
|
||||
break;
|
||||
case TransactionExploreConditionField.DestinationAmount:
|
||||
condition = new TransactionExploreDestinationAmountCondition(TransactionExploreConditionOperatorType.Between, [0, 0]);
|
||||
break;
|
||||
case TransactionExploreConditionField.TransactionTag:
|
||||
condition = new TransactionExploreTransactionTagCondition(TransactionExploreConditionOperatorType.HasAny, []);
|
||||
break;
|
||||
case TransactionExploreConditionField.Description:
|
||||
condition = new TransactionExploreDescriptionCondition(TransactionExploreConditionOperatorType.Contains, '');
|
||||
break;
|
||||
default:
|
||||
condition = new TransactionExploreTransactionTypeCondition([ TransactionType.Expense, TransactionType.Income, TransactionType.Transfer ]);
|
||||
break;
|
||||
}
|
||||
|
||||
return new TransactionExploreConditionWithRelation(
|
||||
condition,
|
||||
isFirst ? TransactionExploreConditionRelation.First : TransactionExploreConditionRelation.And
|
||||
);
|
||||
}
|
||||
|
||||
public match(transaction: TransactionInsightDataItem): boolean {
|
||||
if (!this.conditions || this.conditions.length < 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const postfixExprTokens = this.getPostfixExprTokens();
|
||||
const stack: boolean[] = [];
|
||||
|
||||
for (const token of postfixExprTokens) {
|
||||
if (token === TransactionExploreConditionRelation.And || token === TransactionExploreConditionRelation.Or) {
|
||||
const right = stack.pop();
|
||||
const left = stack.pop();
|
||||
|
||||
if (left === undefined || right === undefined) {
|
||||
throw new Error('invalid postfix expression');
|
||||
}
|
||||
|
||||
if (token === TransactionExploreConditionRelation.And) {
|
||||
stack.push(left && right);
|
||||
} else if (token === TransactionExploreConditionRelation.Or) {
|
||||
stack.push(left || right);
|
||||
} else {
|
||||
throw new Error('invalid postfix expression');
|
||||
}
|
||||
} else {
|
||||
stack.push(token.match(transaction));
|
||||
}
|
||||
}
|
||||
|
||||
if (stack.length !== 1) {
|
||||
throw new Error('invalid postfix evaluation result');
|
||||
}
|
||||
|
||||
return stack[0] as boolean;
|
||||
}
|
||||
|
||||
public toExpression(allCategoriesMap: Record<string, TransactionCategory>, allAccountsMap: Record<string, Account>, allTagsMap: Record<string, TransactionTag>): string {
|
||||
const postfixExprTokens = this.getPostfixExprTokens();
|
||||
const stack: ExpressionNode[] = [];
|
||||
|
||||
for (const token of postfixExprTokens) {
|
||||
if (token === TransactionExploreConditionRelation.And || token === TransactionExploreConditionRelation.Or) {
|
||||
const right = stack.pop();
|
||||
const left = stack.pop();
|
||||
|
||||
if (left === undefined || right === undefined) {
|
||||
throw new Error('invalid postfix expression');
|
||||
}
|
||||
|
||||
let leftExpression = left.textualExpression;
|
||||
let rightExpression = right.textualExpression;
|
||||
|
||||
if (left.operator && left.operator !== token) {
|
||||
leftExpression = `(${leftExpression})`;
|
||||
}
|
||||
|
||||
if (right.operator && right.operator !== token) {
|
||||
rightExpression = `(${rightExpression})`;
|
||||
}
|
||||
|
||||
stack.push({
|
||||
textualExpression: `${leftExpression} ${token.toUpperCase()} ${rightExpression}`,
|
||||
operator: token
|
||||
});
|
||||
} else {
|
||||
stack.push({
|
||||
textualExpression: token.toExpression(allCategoriesMap, allAccountsMap, allTagsMap)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (stack.length !== 1) {
|
||||
throw new Error('invalid postfix evaluation result');
|
||||
}
|
||||
|
||||
const finalNode = stack[0];
|
||||
|
||||
if (!finalNode) {
|
||||
throw new Error('invalid postfix evaluation result');
|
||||
}
|
||||
|
||||
return finalNode.textualExpression;
|
||||
}
|
||||
|
||||
public getPostfixExprTokens(): (TransactionExploreCondition | TransactionExploreConditionRelation.And | TransactionExploreConditionRelation.Or)[] {
|
||||
const finalTokens: (TransactionExploreCondition | TransactionExploreConditionRelation.And | TransactionExploreConditionRelation.Or)[] = [];
|
||||
|
||||
if (this.conditions.length < 1) {
|
||||
return finalTokens;
|
||||
}
|
||||
|
||||
const operatorStack: TransactionExploreConditionRelation[] = [];
|
||||
const firstCondition = this.conditions[0] as TransactionExploreConditionWithRelation;
|
||||
|
||||
if (firstCondition.relation !== TransactionExploreConditionRelation.First) {
|
||||
throw new Error('the first condition must have relation "first"');
|
||||
}
|
||||
|
||||
finalTokens.push(firstCondition.condition);
|
||||
|
||||
for (const [item, index] of itemAndIndex(this.conditions)) {
|
||||
if (index < 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.relation === TransactionExploreConditionRelation.First) {
|
||||
throw new Error('only the first condition can have relation "first"');
|
||||
}
|
||||
|
||||
const currentOperator = item.relation;
|
||||
|
||||
while (operatorStack.length > 0) {
|
||||
const topOperator = operatorStack[operatorStack.length - 1];
|
||||
const isAndOrOperator = topOperator === TransactionExploreConditionRelation.And || topOperator === TransactionExploreConditionRelation.Or;
|
||||
|
||||
if (isAndOrOperator && TransactionExploreConditionRelationPriority[topOperator] >= TransactionExploreConditionRelationPriority[currentOperator]) {
|
||||
finalTokens.push(topOperator);
|
||||
operatorStack.pop();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
operatorStack.push(currentOperator);
|
||||
finalTokens.push(item.condition);
|
||||
}
|
||||
|
||||
while (operatorStack.length > 0) {
|
||||
const topOperator = operatorStack.pop();
|
||||
|
||||
if (topOperator !== TransactionExploreConditionRelation.And && topOperator !== TransactionExploreConditionRelation.Or) {
|
||||
throw new Error('invalid operator in stack');
|
||||
}
|
||||
|
||||
finalTokens.push(topOperator);
|
||||
}
|
||||
|
||||
return finalTokens;
|
||||
}
|
||||
|
||||
public toJson(): string {
|
||||
return JSON.stringify({
|
||||
name: this.name,
|
||||
conditions: this.conditions.map(condition => condition.toJsonObject())
|
||||
});
|
||||
}
|
||||
|
||||
public static create(): TransactionExploreQuery {
|
||||
return new TransactionExploreQuery("", []);
|
||||
}
|
||||
|
||||
public static parse(json: string): TransactionExploreQuery | null {
|
||||
const parsed = JSON.parse(json);
|
||||
const nameFieldValue = parsed['name'] as unknown;
|
||||
const conditionsFieldValue = parsed['conditions'] as unknown;
|
||||
|
||||
if (typeof nameFieldValue !== 'string' || !Array.isArray(conditionsFieldValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name: string = nameFieldValue;
|
||||
const conditions: TransactionExploreConditionWithRelation[] = [];
|
||||
|
||||
for (const [item, index] of itemAndIndex(conditionsFieldValue)) {
|
||||
const condition = TransactionExploreConditionWithRelation.parse(item);
|
||||
|
||||
if (condition === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (index === 0 && condition.relation !== TransactionExploreConditionRelation.First) {
|
||||
return null;
|
||||
} else if (index > 0 && condition.relation === TransactionExploreConditionRelation.First) {
|
||||
return null;
|
||||
}
|
||||
|
||||
conditions.push(condition);
|
||||
}
|
||||
|
||||
return new TransactionExploreQuery(name, conditions);
|
||||
}
|
||||
}
|
||||
|
||||
export class TransactionExploreConditionWithRelation {
|
||||
public condition: TransactionExploreCondition;
|
||||
public relation: TransactionExploreConditionRelation;
|
||||
|
||||
constructor(condition: TransactionExploreCondition, relation: TransactionExploreConditionRelation) {
|
||||
this.condition = condition;
|
||||
this.relation = relation;
|
||||
}
|
||||
|
||||
public getSupportedOperators(): TransactionExploreConditionOperator[] {
|
||||
let operatorTypes: PartialRecord<TransactionExploreConditionOperatorType, true> = {};
|
||||
|
||||
switch (this.condition.field) {
|
||||
case TransactionExploreConditionField.TransactionType.value:
|
||||
operatorTypes = TransactionExploreTransactionTypeCondition.supportedOperators;
|
||||
break;
|
||||
case TransactionExploreConditionField.TransactionCategory.value:
|
||||
operatorTypes = TransactionExploreTransactionCategoryCondition.supportedOperators;
|
||||
break;
|
||||
case TransactionExploreConditionField.SourceAccount.value:
|
||||
operatorTypes = TransactionExploreSourceAccountCondition.supportedOperators;
|
||||
break;
|
||||
case TransactionExploreConditionField.DestinationAccount.value:
|
||||
operatorTypes = TransactionExploreDestinationAccountCondition.supportedOperators;
|
||||
break;
|
||||
case TransactionExploreConditionField.SourceAmount.value:
|
||||
operatorTypes = TransactionExploreSourceAmountCondition.supportedOperators;
|
||||
break;
|
||||
case TransactionExploreConditionField.DestinationAmount.value:
|
||||
operatorTypes = TransactionExploreDestinationAmountCondition.supportedOperators;
|
||||
break;
|
||||
case TransactionExploreConditionField.TransactionTag.value:
|
||||
operatorTypes = TransactionExploreTransactionTagCondition.supportedOperators;
|
||||
break;
|
||||
case TransactionExploreConditionField.Description.value:
|
||||
operatorTypes = TransactionExploreDescriptionCondition.supportedOperators;
|
||||
break;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
|
||||
const result: TransactionExploreConditionOperator[] = [];
|
||||
|
||||
for (const key of keysIfValueEquals(operatorTypes, true)) {
|
||||
const operator = TransactionExploreConditionOperator.valueOf(key);
|
||||
|
||||
if (operator) {
|
||||
result.push(operator);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public toJsonObject(): unknown {
|
||||
return {
|
||||
condition: {
|
||||
field: this.condition.field,
|
||||
operator: this.condition.operator,
|
||||
value: this.condition.getValueForStore()
|
||||
},
|
||||
relation: this.relation
|
||||
};
|
||||
}
|
||||
|
||||
public static parse(data: unknown): TransactionExploreConditionWithRelation | null {
|
||||
if (typeof data !== 'object' || !data || !('condition' in data) || !('relation' in data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const conditionObject = data['condition'];
|
||||
const relation = data['relation'];
|
||||
|
||||
if (typeof conditionObject !== 'object' || !conditionObject || !('field' in conditionObject) || !('operator' in conditionObject) || !('value' in conditionObject) || typeof relation !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const conditionField = conditionObject['field'];
|
||||
const conditionOperator = conditionObject['operator'] as TransactionExploreConditionOperatorType;
|
||||
const conditionValue = conditionObject['value'];
|
||||
|
||||
let condition: TransactionExploreCondition | null = null;
|
||||
|
||||
switch (conditionField) {
|
||||
case TransactionExploreConditionField.TransactionType.value:
|
||||
if (conditionOperator === TransactionExploreConditionOperatorType.In && Array.isArray(conditionValue)) {
|
||||
condition = new TransactionExploreTransactionTypeCondition(conditionValue as number[]);
|
||||
}
|
||||
break;
|
||||
case TransactionExploreConditionField.TransactionCategory.value:
|
||||
if (conditionOperator === TransactionExploreConditionOperatorType.In && Array.isArray(conditionValue)) {
|
||||
condition = new TransactionExploreTransactionCategoryCondition(conditionValue as string[]);
|
||||
}
|
||||
break;
|
||||
case TransactionExploreConditionField.SourceAccount.value:
|
||||
if (conditionOperator === TransactionExploreConditionOperatorType.In && Array.isArray(conditionValue)) {
|
||||
condition = new TransactionExploreSourceAccountCondition(conditionValue as string[]);
|
||||
}
|
||||
break;
|
||||
case TransactionExploreConditionField.DestinationAccount.value:
|
||||
if (conditionOperator === TransactionExploreConditionOperatorType.In && Array.isArray(conditionValue)) {
|
||||
condition = new TransactionExploreDestinationAccountCondition(conditionValue as string[]);
|
||||
}
|
||||
break;
|
||||
case TransactionExploreConditionField.SourceAmount.value:
|
||||
if (TransactionExploreSourceAmountCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue) && conditionValue.length === 2) {
|
||||
condition = new TransactionExploreSourceAmountCondition(conditionOperator as AmountConditionOperator, conditionValue as [number, number]);
|
||||
}
|
||||
break;
|
||||
case TransactionExploreConditionField.DestinationAmount.value:
|
||||
if (TransactionExploreDestinationAmountCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue) && conditionValue.length === 2) {
|
||||
condition = new TransactionExploreDestinationAmountCondition(conditionOperator as AmountConditionOperator, conditionValue as [number, number]);
|
||||
}
|
||||
break;
|
||||
case TransactionExploreConditionField.TransactionTag.value:
|
||||
if (TransactionExploreTransactionTagCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue)) {
|
||||
condition = new TransactionExploreTransactionTagCondition(conditionOperator as TransactionTagConditionOperator, conditionValue as string[]);
|
||||
}
|
||||
break;
|
||||
case TransactionExploreConditionField.Description.value:
|
||||
if (TransactionExploreDescriptionCondition.supportedOperators[conditionOperator] && typeof conditionValue === 'string') {
|
||||
condition = new TransactionExploreDescriptionCondition(conditionOperator as DescriptionConditionOperator, conditionValue);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (condition === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (relation !== TransactionExploreConditionRelation.First && relation !== TransactionExploreConditionRelation.And && relation !== TransactionExploreConditionRelation.Or) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new TransactionExploreConditionWithRelation(condition, relation);
|
||||
}
|
||||
}
|
||||
|
||||
export interface TransactionExploreCondition<T = TransactionExploreConditionFieldType, V = string | string[] | number[]> {
|
||||
readonly field: T;
|
||||
readonly operator: TransactionExploreConditionOperatorType;
|
||||
value: V;
|
||||
|
||||
getValueForStore(): V;
|
||||
match(transaction: TransactionInsightDataItem): boolean;
|
||||
toExpression(allCategoriesMap: Record<string, TransactionCategory>, allAccountsMap: Record<string, Account>, allTagsMap: Record<string, TransactionTag>): string;
|
||||
}
|
||||
|
||||
export class TransactionExploreTransactionTypeCondition implements TransactionExploreCondition<TransactionExploreConditionFieldType.TransactionType, number[]> {
|
||||
public static readonly supportedOperators: PartialRecord<TransactionExploreConditionOperatorType, true> = {
|
||||
[TransactionExploreConditionOperatorType.In]: true
|
||||
};
|
||||
public readonly field = TransactionExploreConditionFieldType.TransactionType;
|
||||
public readonly operator: TransactionExploreConditionOperatorType.In = TransactionExploreConditionOperatorType.In;
|
||||
public value: number[];
|
||||
|
||||
constructor(value: number[]) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public getValueForStore(): number[] {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public match(transaction: TransactionInsightDataItem): boolean {
|
||||
return this.value.includes(transaction.type);
|
||||
}
|
||||
|
||||
public toExpression(): string {
|
||||
const textualTypes = this.value.map(type => {
|
||||
if (type === TransactionType.Income) {
|
||||
return 'Income';
|
||||
} else if (type === TransactionType.Expense) {
|
||||
return 'Expense';
|
||||
} else if (type === TransactionType.Transfer) {
|
||||
return 'Transfer';
|
||||
} else {
|
||||
return type.toString();
|
||||
}
|
||||
}).join(', ');
|
||||
return `type IN (${textualTypes})`;
|
||||
}
|
||||
}
|
||||
|
||||
export class TransactionExploreTransactionCategoryCondition implements TransactionExploreCondition<TransactionExploreConditionFieldType.TransactionCategory, string[]> {
|
||||
public static readonly supportedOperators: PartialRecord<TransactionExploreConditionOperatorType, true> = {
|
||||
[TransactionExploreConditionOperatorType.In]: true
|
||||
};
|
||||
public readonly field = TransactionExploreConditionFieldType.TransactionCategory;
|
||||
public readonly operator: TransactionExploreConditionOperatorType.In = TransactionExploreConditionOperatorType.In;
|
||||
public value: string[];
|
||||
|
||||
constructor(value: string[]) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public getValueForStore(): string[] {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public match(transaction: TransactionInsightDataItem): boolean {
|
||||
return this.value.includes(transaction.primaryCategory?.id ?? '') || this.value.includes(transaction.secondaryCategory?.id ?? transaction.categoryId);
|
||||
}
|
||||
|
||||
public toExpression(allCategoriesMap: Record<string, TransactionCategory>): string {
|
||||
const textualCategories = this.value.map(id => {
|
||||
const category = allCategoriesMap[id];
|
||||
|
||||
if (category) {
|
||||
if (!category.parentId || category.parentId === '0') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `'${category.name}'`;
|
||||
} else {
|
||||
return `'${id}'`;
|
||||
}
|
||||
}).filter(item => !!item).join(', ');
|
||||
return `category IN (${textualCategories})`;
|
||||
}
|
||||
}
|
||||
|
||||
export class TransactionExploreSourceAccountCondition implements TransactionExploreCondition<TransactionExploreConditionFieldType.SourceAccount, string[]> {
|
||||
public static readonly supportedOperators: PartialRecord<TransactionExploreConditionOperatorType, true> = {
|
||||
[TransactionExploreConditionOperatorType.In]: true
|
||||
};
|
||||
public readonly field = TransactionExploreConditionFieldType.SourceAccount;
|
||||
public readonly operator: TransactionExploreConditionOperatorType.In = TransactionExploreConditionOperatorType.In;
|
||||
public value: string[];
|
||||
|
||||
constructor(value: string[]) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public getValueForStore(): string[] {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public match(transaction: TransactionInsightDataItem): boolean {
|
||||
return this.value.includes(transaction.sourceAccountId);
|
||||
}
|
||||
|
||||
public toExpression(allCategoriesMap: Record<string, TransactionCategory>, allAccountsMap: Record<string, Account>): string {
|
||||
const textualAccounts = this.value.map(id => {
|
||||
const account = allAccountsMap[id];
|
||||
|
||||
if (account) {
|
||||
if (account.type === AccountType.MultiSubAccounts.type) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `'${account.name}'`;
|
||||
} else {
|
||||
return `'${id}'`;
|
||||
}
|
||||
}).filter(item => !!item).join(', ');
|
||||
return `source_account IN (${textualAccounts})`;
|
||||
}
|
||||
}
|
||||
|
||||
export class TransactionExploreDestinationAccountCondition implements TransactionExploreCondition<TransactionExploreConditionFieldType.DestinationAccount, string[]> {
|
||||
public static readonly supportedOperators: PartialRecord<TransactionExploreConditionOperatorType, true> = {
|
||||
[TransactionExploreConditionOperatorType.In]: true
|
||||
};
|
||||
public readonly field = TransactionExploreConditionFieldType.DestinationAccount;
|
||||
public readonly operator: TransactionExploreConditionOperatorType.In = TransactionExploreConditionOperatorType.In;
|
||||
public value: string[];
|
||||
|
||||
constructor(value: string[]) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public getValueForStore(): string[] {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public match(transaction: TransactionInsightDataItem): boolean {
|
||||
return this.value.includes(transaction.destinationAccountId);
|
||||
}
|
||||
|
||||
public toExpression(allCategoriesMap: Record<string, TransactionCategory>, allAccountsMap: Record<string, Account>): string {
|
||||
const textualAccounts = this.value.map(id => {
|
||||
const account = allAccountsMap[id];
|
||||
|
||||
if (account) {
|
||||
if (account.type === AccountType.MultiSubAccounts.type) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `'${account.name}'`;
|
||||
} else {
|
||||
return `'${id}'`;
|
||||
}
|
||||
}).filter(item => !!item).join(', ');
|
||||
return `destination_account IN (${textualAccounts})`;
|
||||
}
|
||||
}
|
||||
|
||||
type AmountConditionField = TransactionExploreConditionFieldType.SourceAmount | TransactionExploreConditionFieldType.DestinationAmount;
|
||||
type AmountConditionOperator = TransactionExploreConditionOperatorType.Equals |
|
||||
TransactionExploreConditionOperatorType.NotEquals |
|
||||
TransactionExploreConditionOperatorType.GreaterThan |
|
||||
TransactionExploreConditionOperatorType.LessThan |
|
||||
TransactionExploreConditionOperatorType.Between |
|
||||
TransactionExploreConditionOperatorType.NotBetween;
|
||||
|
||||
export abstract class AbstractTransactionExploreAmountCondition<T = AmountConditionField> implements TransactionExploreCondition<T, [number, number]> {
|
||||
public static readonly supportedOperators: PartialRecord<TransactionExploreConditionOperatorType, true> = {
|
||||
[TransactionExploreConditionOperatorType.Equals]: true,
|
||||
[TransactionExploreConditionOperatorType.NotEquals]: true,
|
||||
[TransactionExploreConditionOperatorType.GreaterThan]: true,
|
||||
[TransactionExploreConditionOperatorType.LessThan]: true,
|
||||
[TransactionExploreConditionOperatorType.Between]: true,
|
||||
[TransactionExploreConditionOperatorType.NotBetween]: true
|
||||
};
|
||||
public abstract readonly field: T;
|
||||
public readonly operator: AmountConditionOperator = TransactionExploreConditionOperatorType.Between;
|
||||
public value: [number, number];
|
||||
|
||||
protected constructor(operator: AmountConditionOperator, value: [number, number]) {
|
||||
this.operator = operator;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public getValueForStore(): [number, number] {
|
||||
if (this.operator === TransactionExploreConditionOperatorType.Between || this.operator === TransactionExploreConditionOperatorType.NotBetween) {
|
||||
return [this.value[0], this.value[1]];
|
||||
} else {
|
||||
return [this.value[0], this.value[0]];
|
||||
}
|
||||
}
|
||||
|
||||
public abstract match(transaction: TransactionInsightDataItem): boolean;
|
||||
public abstract toExpression(allCategoriesMap: Record<string, TransactionCategory>, allAccountsMap: Record<string, Account>, allTagsMap: Record<string, TransactionTag>): string;
|
||||
|
||||
protected matchAmount(amount: number): boolean {
|
||||
switch (this.operator) {
|
||||
case TransactionExploreConditionOperatorType.GreaterThan:
|
||||
return amount > this.value[0];
|
||||
case TransactionExploreConditionOperatorType.LessThan:
|
||||
return amount < this.value[0];
|
||||
case TransactionExploreConditionOperatorType.Equals:
|
||||
return amount === this.value[0];
|
||||
case TransactionExploreConditionOperatorType.NotEquals:
|
||||
return amount !== this.value[0];
|
||||
case TransactionExploreConditionOperatorType.Between:
|
||||
return amount >= this.value[0] && amount <= this.value[1];
|
||||
case TransactionExploreConditionOperatorType.NotBetween:
|
||||
return amount < this.value[0] || amount > this.value[1];
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected getExpression(amountFieldName: string): string {
|
||||
let expressionAmount1 = this.value[0].toString(10);
|
||||
let expressionAmount2 = this.value[1].toString(10);
|
||||
|
||||
if (expressionAmount1.length > 2) {
|
||||
expressionAmount1 = `${expressionAmount1.substring(0, expressionAmount1.length - 2)}.${expressionAmount1.substring(expressionAmount1.length - 2)}`;
|
||||
}
|
||||
|
||||
if (expressionAmount2.length > 2) {
|
||||
expressionAmount2 = `${expressionAmount2.substring(0, expressionAmount2.length - 2)}.${expressionAmount2.substring(expressionAmount2.length - 2)}`;
|
||||
}
|
||||
|
||||
switch (this.operator) {
|
||||
case TransactionExploreConditionOperatorType.GreaterThan:
|
||||
return `${amountFieldName} > ${expressionAmount1}`;
|
||||
case TransactionExploreConditionOperatorType.LessThan:
|
||||
return `${amountFieldName} < ${expressionAmount1}`;
|
||||
case TransactionExploreConditionOperatorType.Equals:
|
||||
return `${amountFieldName} = ${expressionAmount1}`;
|
||||
case TransactionExploreConditionOperatorType.NotEquals:
|
||||
return `${amountFieldName} <> ${expressionAmount1}`;
|
||||
case TransactionExploreConditionOperatorType.Between:
|
||||
return `(${amountFieldName} >= ${expressionAmount1} AND ${amountFieldName} <= ${expressionAmount2})`;
|
||||
case TransactionExploreConditionOperatorType.NotBetween:
|
||||
return `(${amountFieldName} < ${expressionAmount1} OR ${amountFieldName} > ${expressionAmount2})`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class TransactionExploreSourceAmountCondition extends AbstractTransactionExploreAmountCondition<TransactionExploreConditionFieldType.SourceAmount> {
|
||||
public readonly field = TransactionExploreConditionFieldType.SourceAmount;
|
||||
|
||||
constructor(operator: AmountConditionOperator, value: [number, number]) {
|
||||
super(operator, value);
|
||||
}
|
||||
|
||||
public match(transaction: TransactionInsightDataItem): boolean {
|
||||
return super.matchAmount(transaction.sourceAmount);
|
||||
}
|
||||
|
||||
public toExpression(): string {
|
||||
return this.getExpression('source_amount');
|
||||
}
|
||||
}
|
||||
|
||||
export class TransactionExploreDestinationAmountCondition extends AbstractTransactionExploreAmountCondition<TransactionExploreConditionFieldType.DestinationAmount> {
|
||||
public readonly field = TransactionExploreConditionFieldType.DestinationAmount;
|
||||
|
||||
constructor(operator: AmountConditionOperator, value: [number, number]) {
|
||||
super(operator, value);
|
||||
}
|
||||
|
||||
public match(transaction: TransactionInsightDataItem): boolean {
|
||||
return super.matchAmount(transaction.destinationAmount);
|
||||
}
|
||||
|
||||
public toExpression(): string {
|
||||
return this.getExpression('destination_amount');
|
||||
}
|
||||
}
|
||||
|
||||
type TransactionTagConditionOperator = TransactionExploreConditionOperatorType.IsEmpty |
|
||||
TransactionExploreConditionOperatorType.IsNotEmpty |
|
||||
TransactionExploreConditionOperatorType.Equals |
|
||||
TransactionExploreConditionOperatorType.NotEquals |
|
||||
TransactionExploreConditionOperatorType.HasAny |
|
||||
TransactionExploreConditionOperatorType.HasAll |
|
||||
TransactionExploreConditionOperatorType.NotHasAny |
|
||||
TransactionExploreConditionOperatorType.NotHasAll;
|
||||
|
||||
export class TransactionExploreTransactionTagCondition implements TransactionExploreCondition<TransactionExploreConditionFieldType.TransactionTag, string[]> {
|
||||
public static readonly supportedOperators: PartialRecord<TransactionExploreConditionOperatorType, true> = {
|
||||
[TransactionExploreConditionOperatorType.IsEmpty]: true,
|
||||
[TransactionExploreConditionOperatorType.IsNotEmpty]: true,
|
||||
[TransactionExploreConditionOperatorType.Equals]: true,
|
||||
[TransactionExploreConditionOperatorType.NotEquals]: true,
|
||||
[TransactionExploreConditionOperatorType.HasAny]: true,
|
||||
[TransactionExploreConditionOperatorType.HasAll]: true,
|
||||
[TransactionExploreConditionOperatorType.NotHasAny]: true,
|
||||
[TransactionExploreConditionOperatorType.NotHasAll]: true
|
||||
};
|
||||
public readonly field = TransactionExploreConditionFieldType.TransactionTag;
|
||||
public readonly operator: TransactionTagConditionOperator = TransactionExploreConditionOperatorType.HasAny;
|
||||
public value: string[];
|
||||
|
||||
constructor(operator: TransactionTagConditionOperator, value: string[]) {
|
||||
this.operator = operator;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public getValueForStore(): string[] {
|
||||
if (this.operator === TransactionExploreConditionOperatorType.IsEmpty || this.operator === TransactionExploreConditionOperatorType.IsNotEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public match(transaction: TransactionInsightDataItem): boolean {
|
||||
const transactionTags: Record<string, boolean> = {};
|
||||
|
||||
for (const tagId of transaction.tagIds) {
|
||||
transactionTags[tagId] = true;
|
||||
}
|
||||
|
||||
if (this.operator === TransactionExploreConditionOperatorType.IsEmpty || this.value.length < 1) {
|
||||
return transaction.tagIds.length < 1;
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.IsNotEmpty) {
|
||||
return transaction.tagIds.length > 0;
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.Equals || this.operator === TransactionExploreConditionOperatorType.NotEquals) {
|
||||
let hasAll = true;
|
||||
|
||||
for (const tagId of this.value) {
|
||||
if (!transactionTags[tagId]) {
|
||||
hasAll = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const hasSameCount = transaction.tagIds.length === this.value.length;
|
||||
|
||||
if (this.operator === TransactionExploreConditionOperatorType.Equals && hasAll && hasSameCount) {
|
||||
return true;
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.NotEquals && (!hasAll || !hasSameCount)) {
|
||||
return true;
|
||||
}
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.HasAny || this.operator === TransactionExploreConditionOperatorType.NotHasAny) {
|
||||
let hasAny = false;
|
||||
|
||||
for (const tagId of this.value) {
|
||||
if (transactionTags[tagId]) {
|
||||
hasAny = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.operator === TransactionExploreConditionOperatorType.HasAny && hasAny) {
|
||||
return true;
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.NotHasAny && !hasAny) {
|
||||
return true;
|
||||
}
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.HasAll || this.operator === TransactionExploreConditionOperatorType.NotHasAll) {
|
||||
let hasAll = true;
|
||||
|
||||
for (const tagId of this.value) {
|
||||
if (!transactionTags[tagId]) {
|
||||
hasAll = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.operator === TransactionExploreConditionOperatorType.HasAll && hasAll) {
|
||||
return true;
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.NotHasAll && !hasAll) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public toExpression(allCategoriesMap: Record<string, TransactionCategory>, allAccountsMap: Record<string, Account>, allTagsMap: Record<string, TransactionTag>): string {
|
||||
if (this.operator === TransactionExploreConditionOperatorType.IsEmpty) {
|
||||
return `tags IS EMPTY`;
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.IsNotEmpty) {
|
||||
return `tags IS NOT EMPTY`;
|
||||
}
|
||||
|
||||
const textualTags = this.value.map(id => {
|
||||
const tag = allTagsMap[id];
|
||||
|
||||
if (tag) {
|
||||
return `'${tag.name}'`;
|
||||
} else {
|
||||
return `'${id}'`;
|
||||
}
|
||||
}).join(', ');
|
||||
|
||||
if (this.operator === TransactionExploreConditionOperatorType.Equals) {
|
||||
return `tags FULL MATCHES (${textualTags})`;
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.NotEquals) {
|
||||
return `tags NOT FULL MATCHES (${textualTags})`;
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.HasAny) {
|
||||
return `tags HAS ANY (${textualTags})`;
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.HasAll) {
|
||||
return `tags HAS ALL (${textualTags})`;
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.NotHasAny) {
|
||||
return `tags NOT HAS ANY (${textualTags})`;
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.NotHasAll) {
|
||||
return `tags NOT HAS ALL (${textualTags})`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type DescriptionConditionOperator = TransactionExploreConditionOperatorType.IsEmpty |
|
||||
TransactionExploreConditionOperatorType.IsNotEmpty |
|
||||
TransactionExploreConditionOperatorType.Equals |
|
||||
TransactionExploreConditionOperatorType.NotEquals |
|
||||
TransactionExploreConditionOperatorType.Contains |
|
||||
TransactionExploreConditionOperatorType.NotContains |
|
||||
TransactionExploreConditionOperatorType.StartsWith |
|
||||
TransactionExploreConditionOperatorType.NotStartsWith |
|
||||
TransactionExploreConditionOperatorType.EndsWith |
|
||||
TransactionExploreConditionOperatorType.NotEndsWith;
|
||||
|
||||
export class TransactionExploreDescriptionCondition implements TransactionExploreCondition<TransactionExploreConditionFieldType.Description, string> {
|
||||
public static readonly supportedOperators: PartialRecord<TransactionExploreConditionOperatorType, true> = {
|
||||
[TransactionExploreConditionOperatorType.IsEmpty]: true,
|
||||
[TransactionExploreConditionOperatorType.IsNotEmpty]: true,
|
||||
[TransactionExploreConditionOperatorType.Equals]: true,
|
||||
[TransactionExploreConditionOperatorType.NotEquals]: true,
|
||||
[TransactionExploreConditionOperatorType.Contains]: true,
|
||||
[TransactionExploreConditionOperatorType.NotContains]: true,
|
||||
[TransactionExploreConditionOperatorType.StartsWith]: true,
|
||||
[TransactionExploreConditionOperatorType.NotStartsWith]: true,
|
||||
[TransactionExploreConditionOperatorType.EndsWith]: true,
|
||||
[TransactionExploreConditionOperatorType.NotEndsWith]: true
|
||||
};
|
||||
public readonly field = TransactionExploreConditionFieldType.Description;
|
||||
public readonly operator: DescriptionConditionOperator = TransactionExploreConditionOperatorType.Contains;
|
||||
public value: string;
|
||||
|
||||
constructor(operator: DescriptionConditionOperator, value: string) {
|
||||
this.operator = operator;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public getValueForStore(): string {
|
||||
if (this.operator === TransactionExploreConditionOperatorType.IsEmpty || this.operator === TransactionExploreConditionOperatorType.IsNotEmpty) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public match(transaction: TransactionInsightDataItem): boolean {
|
||||
const description = transaction.comment || '';
|
||||
|
||||
if (this.operator === TransactionExploreConditionOperatorType.IsEmpty) {
|
||||
return description.length === 0;
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.IsNotEmpty) {
|
||||
return description.length > 0;
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.Equals) {
|
||||
return description === this.value;
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.NotEquals) {
|
||||
return description !== this.value;
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.Contains) {
|
||||
return description.includes(this.value);
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.NotContains) {
|
||||
return !description.includes(this.value);
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.StartsWith) {
|
||||
return description.startsWith(this.value);
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.NotStartsWith) {
|
||||
return !description.startsWith(this.value);
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.EndsWith) {
|
||||
return description.endsWith(this.value);
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.NotEndsWith) {
|
||||
return !description.endsWith(this.value);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public toExpression(): string {
|
||||
if (this.operator === TransactionExploreConditionOperatorType.IsEmpty) {
|
||||
return `description IS EMPTY`;
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.IsNotEmpty) {
|
||||
return `description IS NOT EMPTY`;
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.Equals) {
|
||||
return `description = '${this.value.replace(/'/g, "''")}'`;
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.NotEquals) {
|
||||
return `description <> '${this.value.replace(/'/g, "''")}'`;
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.Contains) {
|
||||
return `description CONTAINS '${this.value.replace(/'/g, "''")}'`;
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.NotContains) {
|
||||
return `description NOT CONTAINS '${this.value.replace(/'/g, "''")}'`;
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.StartsWith) {
|
||||
return `description STARTS WITH '${this.value.replace(/'/g, "''")}'`;
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.NotStartsWith) {
|
||||
return `description NOT STARTS WITH '${this.value.replace(/'/g, "''")}'`;
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.EndsWith) {
|
||||
return `description ENDS WITH '${this.value.replace(/'/g, "''")}'`;
|
||||
} else if (this.operator === TransactionExploreConditionOperatorType.NotEndsWith) {
|
||||
return `description NOT ENDS WITH '${this.value.replace(/'/g, "''")}'`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -597,6 +597,11 @@ export interface TransactionListInMonthByPageRequest {
|
||||
readonly keyword: string;
|
||||
}
|
||||
|
||||
export interface TransactionAllListRequest {
|
||||
readonly startTime: number;
|
||||
readonly endTime: number;
|
||||
}
|
||||
|
||||
export interface TransactionReconciliationStatementRequest {
|
||||
readonly accountId: string;
|
||||
readonly startTime: number;
|
||||
@@ -948,6 +953,27 @@ export interface TransactionAssetTrendsAnalysisDataAmount extends Record<string,
|
||||
readonly totalAmount: number;
|
||||
}
|
||||
|
||||
export interface TransactionInsightDataItem extends TransactionInfoResponse {
|
||||
readonly id: string;
|
||||
readonly time: number;
|
||||
readonly utcOffset: number;
|
||||
readonly type: number;
|
||||
readonly primaryCategory: TransactionCategoryInfoResponse;
|
||||
readonly primaryCategoryName: string;
|
||||
readonly secondaryCategory: TransactionCategoryInfoResponse;
|
||||
readonly secondaryCategoryName: string;
|
||||
readonly sourceAccount: AccountInfoResponse;
|
||||
readonly sourceAccountName: string;
|
||||
readonly destinationAccount?: AccountInfoResponse;
|
||||
readonly destinationAccountName?: string;
|
||||
readonly sourceAmount: number;
|
||||
readonly destinationAmount: number;
|
||||
readonly hideAmount: boolean;
|
||||
readonly tags: TransactionTagInfoResponse[];
|
||||
readonly comment: string;
|
||||
readonly geoLocation?: TransactionGeoLocationResponse;
|
||||
}
|
||||
|
||||
export type TransactionAmountsResponse = PartialRecord<TransactionAmountsRequestType, TransactionAmountsResponseItem>;
|
||||
|
||||
export interface TransactionAmountsResponseItem {
|
||||
|
||||
@@ -18,6 +18,8 @@ import TransactionListPage from '@/views/desktop/transactions/ListPage.vue';
|
||||
|
||||
import StatisticsTransactionPage from '@/views/desktop/statistics/TransactionPage.vue';
|
||||
|
||||
import InsightsExplorePage from '@/views/desktop/insights/ExplorePage.vue';
|
||||
|
||||
import AccountListPage from '@/views/desktop/accounts/ListPage.vue';
|
||||
|
||||
import TransactionCategoryListPage from '@/views/desktop/categories/ListPage.vue';
|
||||
@@ -136,6 +138,18 @@ const router = createRouter({
|
||||
initAssetTrendsDateAggregationType: route.query['assetTrendsDateAggregationType']
|
||||
})
|
||||
},
|
||||
{
|
||||
path: '/insights/explore',
|
||||
component: InsightsExplorePage,
|
||||
beforeEnter: checkLogin,
|
||||
props: route => ({
|
||||
initId: route.query['id'],
|
||||
initActiveTab: route.query['activeTab'],
|
||||
initDateRangeType: route.query['dateRangeType'],
|
||||
initStartTime: route.query['startTime'],
|
||||
initEndTime: route.query['endTime']
|
||||
})
|
||||
},
|
||||
{
|
||||
path: '/account/list',
|
||||
component: AccountListPage,
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
import { ref, computed } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { useSettingsStore } from './setting.ts';
|
||||
import { useUserStore } from './user.ts';
|
||||
import { useAccountsStore } from './account.ts';
|
||||
import { useTransactionCategoriesStore } from './transactionCategory.ts';
|
||||
import { useTransactionTagsStore } from './transactionTag.ts';
|
||||
|
||||
import { DateRangeScene, DateRange } from '@/core/datetime.ts';
|
||||
import { DEFAULT_TRANSACTION_EXPLORE_DATE_RANGE } from '@/core/explore.ts';
|
||||
|
||||
import { type Account } from '@/models/account.ts';
|
||||
import { type TransactionCategory } from '@/models/transaction_category.ts';
|
||||
import { type TransactionTag } from '@/models/transaction_tag.ts';
|
||||
import {
|
||||
type TransactionInfoResponse,
|
||||
type TransactionInsightDataItem
|
||||
} from '@/models/transaction.ts';
|
||||
import {
|
||||
TransactionExploreQuery
|
||||
} from '@/models/explore.ts';
|
||||
|
||||
import { isInteger, isEquals } from '@/lib/common.ts';
|
||||
import { getDateRangeByDateType } from '@/lib/datetime.ts';
|
||||
import services from '@/lib/services.ts';
|
||||
import logger from '@/lib/logger.ts';
|
||||
|
||||
export interface TransactionExplorePartialFilter {
|
||||
dateRangeType?: number;
|
||||
startTime?: number;
|
||||
endTime?: number;
|
||||
queryId?: string;
|
||||
}
|
||||
|
||||
export interface TransactionExploreFilter extends TransactionExplorePartialFilter {
|
||||
dateRangeType: number;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
query: TransactionExploreQuery[];
|
||||
}
|
||||
|
||||
export const useExploresStore = defineStore('explores', () => {
|
||||
const settingsStore = useSettingsStore();
|
||||
const userStore = useUserStore();
|
||||
const accountsStore = useAccountsStore();
|
||||
const transactionCategoriesStore = useTransactionCategoriesStore();
|
||||
const transactionTagsStore = useTransactionTagsStore();
|
||||
|
||||
const transactionExploreFilter = ref<TransactionExploreFilter>({
|
||||
dateRangeType: DEFAULT_TRANSACTION_EXPLORE_DATE_RANGE.type,
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
query: []
|
||||
});
|
||||
|
||||
const transactionExploreAllData = ref<TransactionInfoResponse[]>([]);
|
||||
const transactionExploreStateInvalid = ref<boolean>(true);
|
||||
|
||||
const allTransactions = computed<TransactionInsightDataItem[]>(() => {
|
||||
if (!transactionExploreAllData.value || transactionExploreAllData.value.length < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result: TransactionInsightDataItem[] = [];
|
||||
|
||||
for (const transaction of transactionExploreAllData.value) {
|
||||
const sourceAccount: Account | undefined = accountsStore.allAccountsMap[transaction.sourceAccountId];
|
||||
|
||||
if (!sourceAccount) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let destinationAccount: Account | undefined = undefined
|
||||
|
||||
if (transaction.destinationAccountId && transaction.destinationAccountId !== '0') {
|
||||
destinationAccount = accountsStore.allAccountsMap[transaction.destinationAccountId];
|
||||
|
||||
if (!destinationAccount) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const secondaryCategory: TransactionCategory | undefined = transactionCategoriesStore.allTransactionCategoriesMap[transaction.categoryId];
|
||||
|
||||
if (!secondaryCategory) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const primaryCategory: TransactionCategory | undefined = transactionCategoriesStore.allTransactionCategoriesMap[secondaryCategory.parentId];
|
||||
|
||||
if (!primaryCategory) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tags: TransactionTag[] = [];
|
||||
|
||||
for (const tagId of transaction.tagIds) {
|
||||
const tag: TransactionTag | undefined = transactionTagsStore.allTransactionTagsMap[tagId];
|
||||
|
||||
if (tag) {
|
||||
tags.push(tag);
|
||||
}
|
||||
}
|
||||
|
||||
const item: TransactionInsightDataItem = {
|
||||
...transaction,
|
||||
id: transaction.id,
|
||||
time: transaction.time,
|
||||
utcOffset: transaction.utcOffset,
|
||||
type: transaction.type,
|
||||
primaryCategory: primaryCategory,
|
||||
primaryCategoryName: primaryCategory.name,
|
||||
secondaryCategory: secondaryCategory,
|
||||
secondaryCategoryName: secondaryCategory.name,
|
||||
sourceAccount: sourceAccount,
|
||||
sourceAccountName: sourceAccount.name,
|
||||
destinationAccount: destinationAccount,
|
||||
destinationAccountName: destinationAccount?.name,
|
||||
sourceAmount: transaction.sourceAmount,
|
||||
destinationAmount: transaction.destinationAmount,
|
||||
hideAmount: transaction.hideAmount,
|
||||
tags: tags,
|
||||
comment: transaction.comment,
|
||||
geoLocation: transaction.geoLocation
|
||||
};
|
||||
|
||||
result.push(item);
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
const filteredTransactions = computed<TransactionInsightDataItem[]>(() => {
|
||||
if (!allTransactions.value || allTransactions.value.length < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!transactionExploreFilter.value.query || transactionExploreFilter.value.query.length < 1) {
|
||||
return allTransactions.value;
|
||||
}
|
||||
|
||||
const result: TransactionInsightDataItem[] = [];
|
||||
|
||||
for (const transaction of allTransactions.value) {
|
||||
for (const query of transactionExploreFilter.value.query) {
|
||||
if (query.match(transaction)) {
|
||||
result.push(transaction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
function updateTransactionExploreInvalidState(invalidState: boolean): void {
|
||||
transactionExploreStateInvalid.value = invalidState;
|
||||
}
|
||||
|
||||
function resetTransactionExplores(): void {
|
||||
transactionExploreFilter.value.dateRangeType = DEFAULT_TRANSACTION_EXPLORE_DATE_RANGE.type;
|
||||
transactionExploreFilter.value.startTime = 0;
|
||||
transactionExploreFilter.value.endTime = 0;
|
||||
transactionExploreFilter.value.query = [];
|
||||
transactionExploreAllData.value = [];
|
||||
transactionExploreStateInvalid.value = true;
|
||||
}
|
||||
|
||||
function initTransactionExploreFilter(filter?: TransactionExplorePartialFilter, resetQuery?: boolean): void {
|
||||
if (filter && isInteger(filter.dateRangeType)) {
|
||||
transactionExploreFilter.value.dateRangeType = filter.dateRangeType;
|
||||
} else {
|
||||
transactionExploreFilter.value.dateRangeType = settingsStore.appSettings.insightsExploreDefaultDateRangeType;
|
||||
}
|
||||
|
||||
let dateRangeTypeValid = true;
|
||||
|
||||
if (!DateRange.isAvailableForScene(transactionExploreFilter.value.dateRangeType, DateRangeScene.InsightsExplore)) {
|
||||
transactionExploreFilter.value.dateRangeType = DEFAULT_TRANSACTION_EXPLORE_DATE_RANGE.type;
|
||||
dateRangeTypeValid = false;
|
||||
}
|
||||
|
||||
if (dateRangeTypeValid && transactionExploreFilter.value.dateRangeType === DateRange.Custom.type) {
|
||||
if (filter && isInteger(filter.startTime)) {
|
||||
transactionExploreFilter.value.startTime = filter.startTime;
|
||||
} else {
|
||||
transactionExploreFilter.value.startTime = 0;
|
||||
}
|
||||
|
||||
if (filter && isInteger(filter.endTime)) {
|
||||
transactionExploreFilter.value.endTime = filter.endTime;
|
||||
} else {
|
||||
transactionExploreFilter.value.endTime = 0;
|
||||
}
|
||||
} else {
|
||||
const dateRange = getDateRangeByDateType(transactionExploreFilter.value.dateRangeType, userStore.currentUserFirstDayOfWeek, userStore.currentUserFiscalYearStart);
|
||||
|
||||
if (dateRange) {
|
||||
transactionExploreFilter.value.dateRangeType = dateRange.dateType;
|
||||
transactionExploreFilter.value.startTime = dateRange.minTime;
|
||||
transactionExploreFilter.value.endTime = dateRange.maxTime;
|
||||
}
|
||||
}
|
||||
|
||||
if (resetQuery) {
|
||||
transactionExploreFilter.value.query = [];
|
||||
}
|
||||
}
|
||||
|
||||
function updateTransactionExploreFilter(filter: TransactionExplorePartialFilter): boolean {
|
||||
let changed = false;
|
||||
|
||||
if (filter && isInteger(filter.dateRangeType) && transactionExploreFilter.value.dateRangeType !== filter.dateRangeType) {
|
||||
transactionExploreFilter.value.dateRangeType = filter.dateRangeType;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (filter && isInteger(filter.startTime) && transactionExploreFilter.value.startTime !== filter.startTime) {
|
||||
transactionExploreFilter.value.startTime = filter.startTime;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (filter && isInteger(filter.endTime) && transactionExploreFilter.value.endTime !== filter.endTime) {
|
||||
transactionExploreFilter.value.endTime = filter.endTime;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
function getTransactionExplorePageParams(currentExploreId: string, activeTab: string): string {
|
||||
const querys: string[] = [];
|
||||
|
||||
if (currentExploreId) {
|
||||
querys.push('id=' + currentExploreId);
|
||||
}
|
||||
|
||||
if (activeTab) {
|
||||
querys.push('activeTab=' + activeTab);
|
||||
}
|
||||
|
||||
querys.push('dateRangeType=' + transactionExploreFilter.value.dateRangeType);
|
||||
querys.push('startTime=' + transactionExploreFilter.value.startTime);
|
||||
querys.push('endTime=' + transactionExploreFilter.value.endTime);
|
||||
|
||||
return querys.join('&');
|
||||
}
|
||||
|
||||
function loadAllTransactions({ force }: { force: boolean }): Promise<TransactionInfoResponse[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
services.getAllTransactions({
|
||||
startTime: transactionExploreFilter.value.startTime,
|
||||
endTime: transactionExploreFilter.value.endTime
|
||||
}).then(response => {
|
||||
const data = response.data;
|
||||
|
||||
if (!data || !data.success || !data.result) {
|
||||
reject({ message: 'Unable to retrieve all transactions' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (transactionExploreStateInvalid.value) {
|
||||
updateTransactionExploreInvalidState(false);
|
||||
}
|
||||
|
||||
if (force && data.result && isEquals(transactionExploreAllData.value, data.result)) {
|
||||
reject({ message: 'Data is up to date', isUpToDate: true });
|
||||
return;
|
||||
}
|
||||
|
||||
transactionExploreAllData.value = data.result;
|
||||
|
||||
resolve(data.result);
|
||||
}).catch(error => {
|
||||
logger.error('failed to load all transactions', error);
|
||||
|
||||
if (error.response && error.response.data && error.response.data.errorMessage) {
|
||||
reject({ error: error.response.data });
|
||||
} else if (!error.processed) {
|
||||
reject({ message: 'Unable to retrieve all transactions' });
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
// states
|
||||
transactionExploreFilter,
|
||||
transactionExploreStateInvalid,
|
||||
// computed
|
||||
filteredTransactions,
|
||||
// functions
|
||||
updateTransactionExploreInvalidState,
|
||||
resetTransactionExplores,
|
||||
initTransactionExploreFilter,
|
||||
updateTransactionExploreFilter,
|
||||
getTransactionExplorePageParams,
|
||||
loadAllTransactions
|
||||
};
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import { useTransactionTemplatesStore } from './transactionTemplate.ts';
|
||||
import { useTransactionsStore } from './transaction.ts';
|
||||
import { useOverviewStore } from './overview.ts';
|
||||
import { useStatisticsStore } from './statistics.ts';
|
||||
import { useExploresStore } from './explore.ts';
|
||||
import { useExchangeRatesStore } from './exchangeRates.ts';
|
||||
|
||||
import type { AuthResponse, RegisterResponse } from '@/models/auth_response.ts';
|
||||
@@ -49,6 +50,7 @@ export const useRootStore = defineStore('root', () => {
|
||||
const transactionsStore = useTransactionsStore();
|
||||
const overviewStore = useOverviewStore();
|
||||
const statisticsStore = useStatisticsStore();
|
||||
const exploresStore = useExploresStore();
|
||||
const exchangeRatesStore = useExchangeRatesStore();
|
||||
|
||||
const currentNotification = ref<string | null>(null);
|
||||
@@ -60,6 +62,7 @@ export const useRootStore = defineStore('root', () => {
|
||||
|
||||
setNotificationContent(null);
|
||||
|
||||
exploresStore.resetTransactionExplores();
|
||||
statisticsStore.resetTransactionStatistics();
|
||||
overviewStore.resetTransactionOverview();
|
||||
transactionsStore.resetTransactions();
|
||||
|
||||
@@ -245,6 +245,19 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
updateUserApplicationCloudSettingValue('alwaysShowTransactionPicturesInMobileTransactionEditPage', value);
|
||||
}
|
||||
|
||||
// Insights & Explore Page
|
||||
function setInsightsExploreDefaultDateRangeType(value: number): void {
|
||||
updateApplicationSettingsValue('insightsExploreDefaultDateRangeType', value);
|
||||
appSettings.value.insightsExploreDefaultDateRangeType = value;
|
||||
updateUserApplicationCloudSettingValue('insightsExploreDefaultDateRangeType', value);
|
||||
}
|
||||
|
||||
function setTimezoneUsedForInsightsExplorePage(value: number): void {
|
||||
updateApplicationSettingsValue('timezoneUsedForInsightsExplorePage', value);
|
||||
appSettings.value.timezoneUsedForInsightsExplorePage = value;
|
||||
updateUserApplicationCloudSettingValue('timezoneUsedForInsightsExplorePage', value);
|
||||
}
|
||||
|
||||
// Account List Page
|
||||
function setTotalAmountExcludeAccountIds(value: Record<string, boolean>): void {
|
||||
updateApplicationSettingsValue('totalAmountExcludeAccountIds', value);
|
||||
@@ -467,6 +480,9 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
setAutoSaveTransactionDraft,
|
||||
setAutoGetCurrentGeoLocation,
|
||||
setAlwaysShowTransactionPicturesInMobileTransactionEditPage,
|
||||
// -- Insights & Explore Page
|
||||
setInsightsExploreDefaultDateRangeType,
|
||||
setTimezoneUsedForInsightsExplorePage,
|
||||
// -- Account List Page
|
||||
setTotalAmountExcludeAccountIds,
|
||||
// -- Exchange Rates Data Page
|
||||
|
||||
@@ -15,9 +15,9 @@ import {
|
||||
isAccountOrSubAccountsAllChecked
|
||||
} from '@/lib/account.ts';
|
||||
|
||||
export type AccountFilterType = 'statisticsDefault' | 'statisticsCurrent' | 'homePageOverview' | 'transactionListCurrent' | 'accountListTotalAmount';
|
||||
export type AccountFilterType = 'statisticsDefault' | 'statisticsCurrent' | 'homePageOverview' | 'transactionListCurrent' | 'accountListTotalAmount' | 'custom';
|
||||
|
||||
export function useAccountFilterSettingPageBase(type?: AccountFilterType) {
|
||||
export function useAccountFilterSettingPageBase(type?: AccountFilterType, selectedAccountIds?: string[]) {
|
||||
const settingsStore = useSettingsStore();
|
||||
const accountsStore = useAccountsStore();
|
||||
const transactionsStore = useTransactionsStore();
|
||||
@@ -46,7 +46,7 @@ export function useAccountFilterSettingPageBase(type?: AccountFilterType) {
|
||||
});
|
||||
|
||||
const allowHiddenAccount = computed<boolean>(() => {
|
||||
return type === 'statisticsDefault' || type === 'statisticsCurrent' || type === 'homePageOverview' || type === 'transactionListCurrent';
|
||||
return type === 'statisticsDefault' || type === 'statisticsCurrent' || type === 'homePageOverview' || type === 'transactionListCurrent' || type === 'custom';
|
||||
});
|
||||
|
||||
const allCategorizedAccounts = computed<Record<number, CategorizedAccount>>(() => filterCategorizedAccounts(accountsStore.allCategorizedAccountsMap, filterContent.value, showHidden.value));
|
||||
@@ -92,6 +92,8 @@ export function useAccountFilterSettingPageBase(type?: AccountFilterType) {
|
||||
|
||||
if (type === 'transactionListCurrent' && transactionsStore.allFilterAccountIdsCount > 0) {
|
||||
allAccountIds[account.id] = true;
|
||||
} else if (type === 'custom') {
|
||||
allAccountIds[account.id] = true;
|
||||
} else {
|
||||
allAccountIds[account.id] = false;
|
||||
}
|
||||
@@ -119,12 +121,26 @@ export function useAccountFilterSettingPageBase(type?: AccountFilterType) {
|
||||
} else if (type === 'accountListTotalAmount') {
|
||||
filterAccountIds.value = Object.assign(allAccountIds, settingsStore.appSettings.totalAmountExcludeAccountIds);
|
||||
return true;
|
||||
} else if (type === 'custom') {
|
||||
if (selectedAccountIds) {
|
||||
for (const accountId of selectedAccountIds) {
|
||||
const account = accountsStore.allAccountsMap[accountId];
|
||||
|
||||
if (account) {
|
||||
selectAccountOrSubAccounts(allAccountIds, account, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filterAccountIds.value = allAccountIds;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function saveFilterAccountIds(): boolean {
|
||||
function saveFilterAccountIds(): [boolean, string[]] {
|
||||
const selectedAccountIds: string[] = [];
|
||||
const filteredAccountIds: Record<string, boolean> = {};
|
||||
let isAllSelected = true;
|
||||
let finalAccountIds = '';
|
||||
@@ -150,6 +166,7 @@ export function useAccountFilterSettingPageBase(type?: AccountFilterType) {
|
||||
}
|
||||
|
||||
finalAccountIds += accountId;
|
||||
selectedAccountIds.push(accountId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +191,7 @@ export function useAccountFilterSettingPageBase(type?: AccountFilterType) {
|
||||
settingsStore.setTotalAmountExcludeAccountIds(filteredAccountIds);
|
||||
}
|
||||
|
||||
return changed;
|
||||
return [changed, selectedAccountIds];
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -50,6 +50,13 @@ export const ALL_APPLICATION_CLOUD_SETTINGS: CategorizedApplicationCloudSettingI
|
||||
{ settingKey: 'alwaysShowTransactionPicturesInMobileTransactionEditPage', settingName: 'Always Show Transaction Pictures', mobile: true, desktop: false }
|
||||
]
|
||||
},
|
||||
{
|
||||
categoryName: 'Insights & Explore Page',
|
||||
items: [
|
||||
{ settingKey: 'insightsExploreDefaultDateRangeType', settingName: 'Default Date Range', mobile: false, desktop: true },
|
||||
{ settingKey: 'timezoneUsedForInsightsExplorePage', settingName: 'Timezone Used for Date Range', mobile: false, desktop: true },
|
||||
]
|
||||
},
|
||||
{
|
||||
categoryName: 'Account List Page',
|
||||
items: [
|
||||
|
||||
@@ -21,9 +21,9 @@ import {
|
||||
isCategoryOrSubCategoriesAllChecked
|
||||
} from '@/lib/category.ts';
|
||||
|
||||
export type CategoryFilterType = 'statisticsDefault' | 'statisticsCurrent' | 'homePageOverview' | 'transactionListCurrent';
|
||||
export type CategoryFilterType = 'statisticsDefault' | 'statisticsCurrent' | 'homePageOverview' | 'transactionListCurrent' | 'custom';
|
||||
|
||||
export function useCategoryFilterSettingPageBase(type?: CategoryFilterType, allowCategoryTypesStr?: string) {
|
||||
export function useCategoryFilterSettingPageBase(type?: CategoryFilterType, allowCategoryTypesStr?: string, selectedCategoryIds?: string[]) {
|
||||
const { tt } = useI18n();
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
@@ -111,6 +111,8 @@ export function useCategoryFilterSettingPageBase(type?: CategoryFilterType, allo
|
||||
|
||||
if (type === 'transactionListCurrent' && transactionsStore.allFilterCategoryIdsCount > 0) {
|
||||
allCategoryIds[category.id] = true;
|
||||
} else if (type === 'custom') {
|
||||
allCategoryIds[category.id] = true;
|
||||
} else {
|
||||
allCategoryIds[category.id] = false;
|
||||
}
|
||||
@@ -136,6 +138,21 @@ export function useCategoryFilterSettingPageBase(type?: CategoryFilterType, allo
|
||||
}
|
||||
}
|
||||
|
||||
filterCategoryIds.value = allCategoryIds;
|
||||
return true;
|
||||
} else if (type === 'custom') {
|
||||
if (selectedCategoryIds) {
|
||||
for (const categoryId of selectedCategoryIds) {
|
||||
const category = transactionCategoriesStore.allTransactionCategoriesMap[categoryId];
|
||||
|
||||
if (category && (!category.subCategories || !category.subCategories.length)) {
|
||||
allCategoryIds[category.id] = false;
|
||||
} else if (category) {
|
||||
selectAllSubCategories(allCategoryIds, false, category);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filterCategoryIds.value = allCategoryIds;
|
||||
return true;
|
||||
} else {
|
||||
@@ -143,7 +160,8 @@ export function useCategoryFilterSettingPageBase(type?: CategoryFilterType, allo
|
||||
}
|
||||
}
|
||||
|
||||
function saveFilterCategoryIds(): boolean {
|
||||
function saveFilterCategoryIds(): [boolean, string[]] {
|
||||
const selectedCategoryIds: string[] = [];
|
||||
const filteredCategoryIds: Record<string, boolean> = {};
|
||||
let isAllSelected = true;
|
||||
let finalCategoryIds = '';
|
||||
@@ -165,6 +183,7 @@ export function useCategoryFilterSettingPageBase(type?: CategoryFilterType, allo
|
||||
}
|
||||
|
||||
finalCategoryIds += categoryId;
|
||||
selectedCategoryIds.push(categoryId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,7 +206,7 @@ export function useCategoryFilterSettingPageBase(type?: CategoryFilterType, allo
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
return [changed, selectedCategoryIds];
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -44,6 +44,12 @@
|
||||
<span class="nav-item-title">{{ tt('Statistics & Analysis') }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="nav-link">
|
||||
<router-link to="/insights/explore">
|
||||
<v-icon class="nav-item-icon" :icon="mdiCompassOutline"/>
|
||||
<span class="nav-item-title">{{ tt('Insights & Explore') }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="nav-section-title">
|
||||
<div class="title-wrapper">
|
||||
<span class="title-text">{{ tt('Basis Data') }}</span>
|
||||
@@ -228,6 +234,7 @@ import {
|
||||
mdiClipboardTextOutline,
|
||||
mdiClipboardTextClockOutline,
|
||||
mdiChartPieOutline,
|
||||
mdiCompassOutline,
|
||||
mdiSwapHorizontal,
|
||||
mdiCogOutline,
|
||||
mdiCellphone,
|
||||
|
||||
@@ -224,6 +224,40 @@
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12">
|
||||
<v-card :title="tt('Insights & Explore Page')">
|
||||
<v-form>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
item-title="displayName"
|
||||
item-value="type"
|
||||
persistent-placeholder
|
||||
:label="tt('Default Date Range')"
|
||||
:placeholder="tt('Default Date Range')"
|
||||
:items="allInsightsExploreDefaultDateRanges"
|
||||
v-model="insightsExploreDefaultDateRangeType"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
item-title="displayName"
|
||||
item-value="type"
|
||||
persistent-placeholder
|
||||
:label="tt('Timezone Used for Date Range')"
|
||||
:placeholder="tt('Timezone Used for Date Range')"
|
||||
:items="allTimezoneTypesUsedForStatistics"
|
||||
v-model="timezoneUsedForInsightsExplorePage"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12">
|
||||
<v-card :title="tt('Account List Page')">
|
||||
<v-form>
|
||||
@@ -305,9 +339,11 @@ import { useAppSettingPageBase } from '@/views/base/settings/AppSettingsPageBase
|
||||
import { useSettingsStore } from '@/stores/setting.ts';
|
||||
import { useAccountsStore } from '@/stores/account.ts';
|
||||
import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts';
|
||||
import { useExploresStore } from '@/stores/explore.ts';
|
||||
|
||||
import type { LocalizedSwitchOption } from '@/core/base.ts';
|
||||
import { ThemeType } from '@/core/theme.ts';
|
||||
import { type LocalizedDateRange, DateRangeScene } from '@/core/datetime.ts';
|
||||
import { CategoryType } from '@/core/category.ts';
|
||||
|
||||
import { getSystemTheme } from '@/lib/ui/common.ts';
|
||||
@@ -316,7 +352,7 @@ type SnackBarType = InstanceType<typeof SnackBar>;
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const { tt, getAllEnableDisableOptions } = useI18n();
|
||||
const { tt, getAllEnableDisableOptions, getAllDateRanges } = useI18n();
|
||||
const {
|
||||
loadingAccounts,
|
||||
loadingTransactionCategories,
|
||||
@@ -347,6 +383,7 @@ const {
|
||||
const settingsStore = useSettingsStore();
|
||||
const accountsStore = useAccountsStore();
|
||||
const transactionCategoriesStore = useTransactionCategoriesStore();
|
||||
const exploresStore = useExploresStore();
|
||||
|
||||
const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
||||
|
||||
@@ -355,6 +392,7 @@ const showTransactionCategoriesIncludedInHomePageOverviewDialog = ref<boolean>(f
|
||||
const showAccountsIncludedInTotalDialog = ref<boolean>(false);
|
||||
|
||||
const enableDisableOptions = computed<LocalizedSwitchOption[]>(() => getAllEnableDisableOptions());
|
||||
const allInsightsExploreDefaultDateRanges = computed<LocalizedDateRange[]>(() => getAllDateRanges(DateRangeScene.InsightsExplore, false));
|
||||
|
||||
const currentTheme = computed<string>({
|
||||
get: () => settingsStore.appSettings.theme,
|
||||
@@ -376,6 +414,19 @@ const showAddTransactionButtonInDesktopNavbar = computed<boolean>({
|
||||
set: (value) => settingsStore.setShowAddTransactionButtonInDesktopNavbar(value)
|
||||
});
|
||||
|
||||
const insightsExploreDefaultDateRangeType = computed<number>({
|
||||
get: () => settingsStore.appSettings.insightsExploreDefaultDateRangeType,
|
||||
set: (value) => settingsStore.setInsightsExploreDefaultDateRangeType(value)
|
||||
});
|
||||
|
||||
const timezoneUsedForInsightsExplorePage = computed<number>({
|
||||
get: () => settingsStore.appSettings.timezoneUsedForInsightsExplorePage,
|
||||
set: (value: number) => {
|
||||
settingsStore.setTimezoneUsedForInsightsExplorePage(value);
|
||||
exploresStore.updateTransactionExploreInvalidState(true);
|
||||
}
|
||||
});
|
||||
|
||||
function init(): void {
|
||||
loadingAccounts.value = true;
|
||||
|
||||
|
||||
@@ -162,12 +162,13 @@ type SnackBarType = InstanceType<typeof SnackBar>;
|
||||
|
||||
const props = defineProps<{
|
||||
type: AccountFilterType;
|
||||
selectedAccountIds?: string[];
|
||||
dialogMode?: boolean;
|
||||
autoSave?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'settings:change', changed: boolean): void;
|
||||
(e: 'settings:change', changed: boolean, selectedAccountIds?: string[]): void;
|
||||
}>();
|
||||
|
||||
const { tt } = useI18n();
|
||||
@@ -187,7 +188,7 @@ const {
|
||||
isAccountChecked,
|
||||
loadFilterAccountIds,
|
||||
saveFilterAccountIds
|
||||
} = useAccountFilterSettingPageBase(props.type);
|
||||
} = useAccountFilterSettingPageBase(props.type, props.selectedAccountIds);
|
||||
|
||||
const accountsStore = useAccountsStore();
|
||||
|
||||
@@ -254,8 +255,8 @@ function selectInvertAccounts(): void {
|
||||
}
|
||||
|
||||
function save(): void {
|
||||
const changed = saveFilterAccountIds();
|
||||
emit('settings:change', changed);
|
||||
const [changed, selectedAccountIds] = saveFilterAccountIds();
|
||||
emit('settings:change', changed, selectedAccountIds);
|
||||
}
|
||||
|
||||
function cancel(): void {
|
||||
|
||||
@@ -160,13 +160,14 @@ type SnackBarType = InstanceType<typeof SnackBar>;
|
||||
|
||||
const props = defineProps<{
|
||||
type: CategoryFilterType;
|
||||
selectedCategoryIds?: string[];
|
||||
dialogMode?: boolean;
|
||||
autoSave?: boolean;
|
||||
categoryTypes?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'settings:change', changed: boolean): void;
|
||||
(e: 'settings:change', changed: boolean, selectedCategoryIds?: string[]): void;
|
||||
}>();
|
||||
|
||||
const { tt } = useI18n();
|
||||
@@ -186,7 +187,7 @@ const {
|
||||
getCategoryTypeName,
|
||||
loadFilterCategoryIds,
|
||||
saveFilterCategoryIds
|
||||
} = useCategoryFilterSettingPageBase(props.type, props.categoryTypes);
|
||||
} = useCategoryFilterSettingPageBase(props.type, props.categoryTypes, props.selectedCategoryIds);
|
||||
|
||||
const transactionCategoriesStore = useTransactionCategoriesStore();
|
||||
|
||||
@@ -261,8 +262,8 @@ function selectInvertCategories(): void {
|
||||
}
|
||||
|
||||
function save(): void {
|
||||
const changed = saveFilterCategoryIds();
|
||||
emit('settings:change', changed);
|
||||
const [changed, selectedCategoryIds] = saveFilterCategoryIds();
|
||||
emit('settings:change', changed, selectedCategoryIds);
|
||||
}
|
||||
|
||||
function cancel(): void {
|
||||
|
||||
@@ -0,0 +1,441 @@
|
||||
<template>
|
||||
<v-row class="match-height">
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<v-layout>
|
||||
<v-navigation-drawer :permanent="alwaysShowNav" v-model="showNav">
|
||||
<div class="mx-6 my-4">
|
||||
<btn-vertical-group :disabled="loading" :buttons="allTabs" v-model="activeTab" />
|
||||
</div>
|
||||
<v-divider />
|
||||
<div class="mx-6 mt-4" v-if="activeTab === 'table'">
|
||||
<span class="text-subtitle-2">{{ tt('Transactions Per Page') }}</span>
|
||||
<v-select class="mt-2" density="compact"
|
||||
item-title="name"
|
||||
item-value="value"
|
||||
:disabled="loading"
|
||||
:items="allPageCounts"
|
||||
v-model="countPerPage"
|
||||
/>
|
||||
</div>
|
||||
<v-tabs show-arrows class="my-4" direction="vertical"
|
||||
:disabled="loading" v-model="currentExploreId">
|
||||
<v-tab class="tab-text-truncate" key="new" value="">
|
||||
<span class="text-truncate">{{ tt('New Explore') }}</span>
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
</v-navigation-drawer>
|
||||
<v-main>
|
||||
<v-card variant="flat" min-height="800">
|
||||
<template #title>
|
||||
<div class="title-and-toolbar d-flex align-center">
|
||||
<v-btn class="me-3 d-md-none" density="compact" color="default" variant="plain"
|
||||
:ripple="false" :icon="true" @click="showNav = !showNav">
|
||||
<v-icon :icon="mdiMenu" size="24" />
|
||||
</v-btn>
|
||||
<span>{{ tt('Insights & Explore') }}</span>
|
||||
<v-btn-group class="ms-4" color="default" density="comfortable" variant="outlined" divided>
|
||||
<v-btn class="button-icon-with-direction" :icon="mdiArrowLeft"
|
||||
:disabled="loading || !canShiftDateRange"
|
||||
@click="shiftDateRange(-1)"/>
|
||||
<v-menu location="bottom" max-height="500">
|
||||
<template #activator="{ props }">
|
||||
<v-btn :disabled="loading"
|
||||
v-bind="props">{{ displayQueryDateRangeName }}</v-btn>
|
||||
</template>
|
||||
<v-list :selected="[query.dateRangeType]">
|
||||
<v-list-item :key="dateRange.type" :value="dateRange.type"
|
||||
:append-icon="(query.dateRangeType === dateRange.type ? mdiCheck : undefined)"
|
||||
v-for="dateRange in allDateRanges">
|
||||
<v-list-item-title class="cursor-pointer"
|
||||
@click="setDateFilter(dateRange.type)">
|
||||
<div class="d-flex align-center">
|
||||
<span>{{ dateRange.displayName }}</span>
|
||||
</div>
|
||||
<div class="statistics-custom-datetime-range smaller" v-if="dateRange.isUserCustomRange && query.dateRangeType === dateRange.type && !!query.startTime && !!query.endTime">
|
||||
<span>{{ displayQueryStartTime }}</span>
|
||||
<span> - </span>
|
||||
<br/>
|
||||
<span>{{ displayQueryEndTime }}</span>
|
||||
</div>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-btn class="button-icon-with-direction" :icon="mdiArrowRight"
|
||||
:disabled="loading || !canShiftDateRange"
|
||||
@click="shiftDateRange(1)"/>
|
||||
</v-btn-group>
|
||||
|
||||
<v-btn density="compact" color="default" variant="text" size="24"
|
||||
class="ms-2" :icon="true" :loading="loading" @click="reload(true)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20"/>
|
||||
</template>
|
||||
<v-icon :icon="mdiRefresh" size="24" />
|
||||
<v-tooltip activator="parent">{{ tt('Refresh') }}</v-tooltip>
|
||||
</v-btn>
|
||||
<v-spacer/>
|
||||
<v-btn density="comfortable" color="default" variant="text" class="ms-2"
|
||||
:disabled="loading" :icon="true"
|
||||
v-if="activeTab !== 'query'">
|
||||
<v-icon :icon="mdiDotsVertical" />
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
<v-list-item :prepend-icon="mdiExport"
|
||||
:title="tt('Export Results')"
|
||||
:disabled="loading || !filteredTransactions || filteredTransactions.length < 1"
|
||||
@click="exportResults"></v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-window class="d-flex flex-grow-1 disable-tab-transition w-100-window-container" v-model="activeTab">
|
||||
<v-window-item value="query">
|
||||
<explore-query-tab :loading="loading" />
|
||||
</v-window-item>
|
||||
<v-window-item value="table">
|
||||
<explore-data-table-tab ref="exploreDataTableTab"
|
||||
:loading="loading"
|
||||
v-model:count-per-page="countPerPage" />
|
||||
</v-window-item>
|
||||
<v-window-item value="chart">
|
||||
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-card>
|
||||
</v-main>
|
||||
</v-layout>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<date-range-selection-dialog :title="tt('Custom Date Range')"
|
||||
:min-time="query.startTime"
|
||||
:max-time="query.endTime"
|
||||
v-model:show="showCustomDateRangeDialog"
|
||||
@dateRange:change="setCustomDateFilter"
|
||||
@error="onShowDateRangeError" />
|
||||
|
||||
<export-dialog ref="exportDialog" />
|
||||
|
||||
<snack-bar ref="snackbar" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ExploreQueryTab from '@/views/desktop/insights/tabs/ExploreQueryTab.vue';
|
||||
import ExploreDataTableTab from '@/views/desktop/insights/tabs/ExploreDataTableTab.vue';
|
||||
import ExportDialog from '@/views/desktop/statistics/transaction/dialogs/ExportDialog.vue';
|
||||
import SnackBar from '@/components/desktop/SnackBar.vue';
|
||||
|
||||
import { ref, computed, useTemplateRef, watch } from 'vue';
|
||||
import { useRouter, onBeforeRouteUpdate } from 'vue-router';
|
||||
import { useDisplay } from 'vuetify';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
|
||||
import { useUserStore } from '@/stores/user.ts';
|
||||
import { useAccountsStore } from '@/stores/account.ts';
|
||||
import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts';
|
||||
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
|
||||
import { type TransactionExplorePartialFilter, type TransactionExploreFilter, useExploresStore } from '@/stores/explore.ts';
|
||||
|
||||
import type { NameNumeralValue } from '@/core/base.ts';
|
||||
import type { NumeralSystem } from '@/core/numeral.ts';
|
||||
import { type WeekDayValue, type LocalizedDateRange, DateRangeScene, DateRange } from '@/core/datetime.ts';
|
||||
|
||||
import {
|
||||
type TransactionInsightDataItem
|
||||
} from '@/models/transaction.ts';
|
||||
|
||||
import {
|
||||
getShiftedDateRangeAndDateType,
|
||||
getDateTypeByDateRange,
|
||||
getDateRangeByDateType,
|
||||
} from '@/lib/datetime.ts';
|
||||
|
||||
import {
|
||||
mdiMenu,
|
||||
mdiArrowLeft,
|
||||
mdiArrowRight,
|
||||
mdiCheck,
|
||||
mdiRefresh,
|
||||
mdiDotsVertical,
|
||||
mdiExport
|
||||
} from '@mdi/js';
|
||||
|
||||
interface InsightsExploreProps {
|
||||
initId?: string;
|
||||
initActiveTab?: string,
|
||||
initDateRangeType?: string,
|
||||
initStartTime?: string,
|
||||
initEndTime?: string,
|
||||
}
|
||||
|
||||
const props = defineProps<InsightsExploreProps>();
|
||||
|
||||
type ExplorePageTabType = 'query' | 'table' | 'chart';
|
||||
type SnackBarType = InstanceType<typeof SnackBar>;
|
||||
type ExploreDataTableTabType = InstanceType<typeof ExploreDataTableTab>;
|
||||
type ExportDialogType = InstanceType<typeof ExportDialog>;
|
||||
|
||||
const router = useRouter();
|
||||
const display = useDisplay();
|
||||
|
||||
const {
|
||||
tt,
|
||||
getAllDateRanges,
|
||||
getCurrentNumeralSystemType,
|
||||
formatUnixTimeToLongDateTime,
|
||||
formatDateRange
|
||||
} = useI18n();
|
||||
|
||||
const userStore = useUserStore();
|
||||
const accountsStore = useAccountsStore();
|
||||
const transactionCategoriesStore = useTransactionCategoriesStore();
|
||||
const transactionTagsStore = useTransactionTagsStore();
|
||||
const exploresStore = useExploresStore();
|
||||
|
||||
const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
||||
const exploreDataTableTab = useTemplateRef<ExploreDataTableTabType>('exploreDataTableTab');
|
||||
const exportDialog = useTemplateRef<ExportDialogType>('exportDialog');
|
||||
|
||||
const loading = ref<boolean>(true);
|
||||
const initing = ref<boolean>(true);
|
||||
const alwaysShowNav = ref<boolean>(display.mdAndUp.value);
|
||||
const showNav = ref<boolean>(display.mdAndUp.value);
|
||||
const activeTab = ref<ExplorePageTabType>('query');
|
||||
const currentExploreId = ref<string>('');
|
||||
const countPerPage = ref<number>(15);
|
||||
const showCustomDateRangeDialog = ref<boolean>(false);
|
||||
|
||||
const firstDayOfWeek = computed<WeekDayValue>(() => userStore.currentUserFirstDayOfWeek);
|
||||
const fiscalYearStart = computed<number>(() => userStore.currentUserFiscalYearStart);
|
||||
const numeralSystem = computed<NumeralSystem>(() => getCurrentNumeralSystemType());
|
||||
|
||||
const query = computed<TransactionExploreFilter>(() => exploresStore.transactionExploreFilter);
|
||||
const filteredTransactions = computed<TransactionInsightDataItem[]>(() => exploresStore.filteredTransactions);
|
||||
|
||||
const allDateRanges = computed<LocalizedDateRange[]>(() => getAllDateRanges(DateRangeScene.InsightsExplore, true));
|
||||
const canShiftDateRange = computed<boolean>(() => query.value.dateRangeType !== DateRange.All.type);
|
||||
const displayQueryDateRangeName = computed<string>(() => formatDateRange(query.value.dateRangeType, query.value.startTime, query.value.endTime));
|
||||
const displayQueryStartTime = computed<string>(() => formatUnixTimeToLongDateTime(query.value.startTime));
|
||||
const displayQueryEndTime = computed<string>(() => formatUnixTimeToLongDateTime(query.value.endTime));
|
||||
|
||||
const allTabs = computed<{ name: string, value: ExplorePageTabType }[]>(() => {
|
||||
return [
|
||||
{
|
||||
name: tt('Query'),
|
||||
value: 'query'
|
||||
},
|
||||
{
|
||||
name: tt('Data Table'),
|
||||
value: 'table'
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
const allPageCounts = computed<NameNumeralValue[]>(() => {
|
||||
const pageCounts: NameNumeralValue[] = [];
|
||||
const availableCountPerPage: number[] = [ 5, 10, 15, 20, 25, 30, 50 ];
|
||||
|
||||
for (const count of availableCountPerPage) {
|
||||
pageCounts.push({ value: count, name: numeralSystem.value.formatNumber(count) });
|
||||
}
|
||||
|
||||
pageCounts.push({ value: -1, name: tt('All') });
|
||||
|
||||
return pageCounts;
|
||||
});
|
||||
|
||||
function getFilterLinkUrl(): string {
|
||||
return `/insights/explore?${exploresStore.getTransactionExplorePageParams(currentExploreId.value, activeTab.value)}`;
|
||||
}
|
||||
|
||||
function init(initProps: InsightsExploreProps): void {
|
||||
const filter: TransactionExplorePartialFilter = {
|
||||
dateRangeType: initProps.initDateRangeType ? parseInt(initProps.initDateRangeType) : undefined,
|
||||
startTime: initProps.initStartTime ? parseInt(initProps.initStartTime) : undefined,
|
||||
endTime: initProps.initEndTime ? parseInt(initProps.initEndTime) : undefined
|
||||
};
|
||||
|
||||
let needReload = false;
|
||||
|
||||
if (filter.dateRangeType !== query.value.dateRangeType) {
|
||||
needReload = true;
|
||||
} else if (filter.dateRangeType === DateRange.Custom.type) {
|
||||
if (filter.startTime !== query.value.startTime
|
||||
|| filter.endTime !== query.value.endTime) {
|
||||
needReload = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (initProps.initActiveTab === 'query' || initProps.initActiveTab === 'table' || initProps.initActiveTab === 'chart') {
|
||||
if (initProps.initActiveTab !== activeTab.value) {
|
||||
activeTab.value = initProps.initActiveTab;
|
||||
needReload = true;
|
||||
}
|
||||
} else {
|
||||
activeTab.value = 'query';
|
||||
}
|
||||
|
||||
exploresStore.initTransactionExploreFilter(filter);
|
||||
|
||||
if (!needReload && !exploresStore.transactionExploreStateInvalid) {
|
||||
loading.value = false;
|
||||
initing.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
accountsStore.loadAllAccounts({ force: false }),
|
||||
transactionCategoriesStore.loadAllCategories({ force: false }),
|
||||
transactionTagsStore.loadAllTags({ force: false })
|
||||
]).then(() => {
|
||||
return exploresStore.loadAllTransactions({ force: false });
|
||||
}).then(() => {
|
||||
loading.value = false;
|
||||
initing.value = false;
|
||||
}).catch(error => {
|
||||
loading.value = false;
|
||||
initing.value = false;
|
||||
|
||||
if (!error.processed) {
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function reload(force: boolean): Promise<unknown> | null {
|
||||
loading.value = true;
|
||||
|
||||
return exploresStore.loadAllTransactions({
|
||||
force: force
|
||||
}).then(() => {
|
||||
loading.value = false;
|
||||
|
||||
if (force) {
|
||||
snackbar.value?.showMessage('Data has been updated');
|
||||
}
|
||||
}).catch(error => {
|
||||
loading.value = false;
|
||||
|
||||
if (!error.processed) {
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function exportResults(): void {
|
||||
if (activeTab.value === 'table' && filteredTransactions.value) {
|
||||
const results = exploreDataTableTab.value?.buildExportResults();
|
||||
|
||||
if (results) {
|
||||
exportDialog.value?.open(results);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setDateFilter(dateType: number): void {
|
||||
if (dateType === DateRange.Custom.type) { // Custom
|
||||
showCustomDateRangeDialog.value = true;
|
||||
return;
|
||||
} else if (query.value.dateRangeType === dateType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dateRange = getDateRangeByDateType(dateType, firstDayOfWeek.value, fiscalYearStart.value);
|
||||
|
||||
if (!dateRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
const changed = exploresStore.updateTransactionExploreFilter({
|
||||
dateRangeType: dateRange.dateType,
|
||||
startTime: dateRange.minTime,
|
||||
endTime: dateRange.maxTime
|
||||
});
|
||||
|
||||
if (changed) {
|
||||
loading.value = true;
|
||||
exploresStore.updateTransactionExploreInvalidState(true);
|
||||
router.push(getFilterLinkUrl());
|
||||
}
|
||||
}
|
||||
|
||||
function setCustomDateFilter(startTime: number, endTime: number): void {
|
||||
if (!startTime || !endTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chartDateType = getDateTypeByDateRange(startTime, endTime, firstDayOfWeek.value, fiscalYearStart.value, DateRangeScene.InsightsExplore);
|
||||
|
||||
const changed = exploresStore.updateTransactionExploreFilter({
|
||||
dateRangeType: chartDateType,
|
||||
startTime: startTime,
|
||||
endTime: endTime
|
||||
});
|
||||
|
||||
showCustomDateRangeDialog.value = false;
|
||||
|
||||
if (changed) {
|
||||
loading.value = true;
|
||||
exploresStore.updateTransactionExploreInvalidState(true);
|
||||
router.push(getFilterLinkUrl());
|
||||
}
|
||||
}
|
||||
|
||||
function shiftDateRange(scale: number): void {
|
||||
if (query.value.dateRangeType === DateRange.All.type) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newDateRange = getShiftedDateRangeAndDateType(query.value.startTime, query.value.endTime, scale, firstDayOfWeek.value, fiscalYearStart.value, DateRangeScene.Normal);
|
||||
|
||||
const changed = exploresStore.updateTransactionExploreFilter({
|
||||
dateRangeType: newDateRange.dateType,
|
||||
startTime: newDateRange.minTime,
|
||||
endTime: newDateRange.maxTime
|
||||
});
|
||||
|
||||
if (changed) {
|
||||
loading.value = true;
|
||||
exploresStore.updateTransactionExploreInvalidState(true);
|
||||
router.push(getFilterLinkUrl());
|
||||
}
|
||||
}
|
||||
|
||||
function onShowDateRangeError(message: string): void {
|
||||
snackbar.value?.showError(message);
|
||||
}
|
||||
|
||||
onBeforeRouteUpdate((to) => {
|
||||
if (to.query) {
|
||||
init({
|
||||
initId: (to.query['id'] as string | null) || undefined,
|
||||
initActiveTab: (to.query['activeTab'] as string | null) || undefined,
|
||||
initDateRangeType: (to.query['dateRangeType'] as string | null) || undefined,
|
||||
initStartTime: (to.query['startTime'] as string | null) || undefined,
|
||||
initEndTime: (to.query['endTime'] as string | null) || undefined,
|
||||
});
|
||||
} else {
|
||||
init({});
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => display.mdAndUp.value, (newValue) => {
|
||||
alwaysShowNav.value = newValue;
|
||||
|
||||
if (!showNav.value) {
|
||||
showNav.value = newValue;
|
||||
}
|
||||
});
|
||||
|
||||
watch(activeTab, () => {
|
||||
router.push(getFilterLinkUrl());
|
||||
});
|
||||
|
||||
init(props);
|
||||
</script>
|
||||
@@ -0,0 +1,291 @@
|
||||
<template>
|
||||
<v-data-table
|
||||
fixed-header
|
||||
fixed-footer
|
||||
multi-sort
|
||||
item-value="index"
|
||||
:class="{ 'insights-explore-table': true, 'text-sm': true, 'disabled': loading, 'loading-skeleton': loading }"
|
||||
:headers="dataTableHeaders"
|
||||
:items="filteredTransactions"
|
||||
:hover="true"
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
v-model:page="currentPage"
|
||||
>
|
||||
<template #item.time="{ item }">
|
||||
<span>{{ getDisplayDateTime(item) }}</span>
|
||||
<v-chip class="ms-1" variant="flat" color="secondary" size="x-small"
|
||||
v-if="item.utcOffset !== currentTimezoneOffsetMinutes">{{ getDisplayTimezone(item) }}</v-chip>
|
||||
</template>
|
||||
<template #item.type="{ item }">
|
||||
<v-chip label variant="outlined" size="x-small"
|
||||
:class="{ 'text-income' : item.type === TransactionType.Income, 'text-expense': item.type === TransactionType.Expense }"
|
||||
:color="getTransactionTypeColor(item)">{{ getDisplayTransactionType(item) }}</v-chip>
|
||||
</template>
|
||||
<template #item.secondaryCategoryName="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<ItemIcon size="24px" icon-type="category"
|
||||
:icon-id="item.secondaryCategory?.icon ?? ''"
|
||||
:color="item.secondaryCategory?.color ?? ''"
|
||||
v-if="item.secondaryCategory?.color"></ItemIcon>
|
||||
<v-icon size="24" :icon="mdiPencilBoxOutline" v-else-if="!item.secondaryCategory || !item.secondaryCategory?.color" />
|
||||
<span class="ms-2" v-if="item.type === TransactionType.ModifyBalance">
|
||||
{{ tt('Modify Balance') }}
|
||||
</span>
|
||||
<span class="ms-2" v-else-if="item.type !== TransactionType.ModifyBalance && item.secondaryCategory">
|
||||
{{ item.secondaryCategory?.name }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #item.sourceAmount="{ item }">
|
||||
<span :class="{ 'text-expense': item.type === TransactionType.Expense, 'text-income': item.type === TransactionType.Income }">{{ getDisplaySourceAmount(item) }}</span>
|
||||
<v-icon class="icon-with-direction mx-1" size="13" :icon="mdiArrowRight" v-if="item.type === TransactionType.Transfer && item.sourceAccount?.id !== item.destinationAccount?.id && getDisplaySourceAmount(item) !== getDisplayDestinationAmount(item)"></v-icon>
|
||||
<span v-if="item.type === TransactionType.Transfer && item.sourceAccount?.id !== item.destinationAccount?.id && getDisplaySourceAmount(item) !== getDisplayDestinationAmount(item)">{{ getDisplayDestinationAmount(item) }}</span>
|
||||
</template>
|
||||
<template #item.sourceAccountName="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<span v-if="item.sourceAccount">{{ item.sourceAccount?.name }}</span>
|
||||
<v-icon class="icon-with-direction mx-1" size="13" :icon="mdiArrowRight" v-if="item.type === TransactionType.Transfer"></v-icon>
|
||||
<span v-if="item.type === TransactionType.Transfer && item.destinationAccount">{{ item.destinationAccount?.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #no-data>
|
||||
<div v-if="loading && (!filteredTransactions || filteredTransactions.length < 1)">
|
||||
<div class="ms-1" style="padding-top: 3px; padding-bottom: 3px" :key="itemIdx" v-for="itemIdx in skeletonData">
|
||||
<v-skeleton-loader type="text" :loading="true"></v-skeleton-loader>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ tt('No transaction data') }}
|
||||
</div>
|
||||
</template>
|
||||
<template #bottom>
|
||||
<div class="title-and-toolbar d-flex align-center justify-center text-no-wrap mt-2 mb-4">
|
||||
<pagination-buttons :disabled="loading"
|
||||
:totalPageCount="totalPageCount"
|
||||
v-model="currentPage">
|
||||
</pagination-buttons>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PaginationButtons from '@/components/desktop/PaginationButtons.vue';
|
||||
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
|
||||
import { useSettingsStore } from '@/stores/setting.ts';
|
||||
import { useUserStore } from '@/stores/user.ts';
|
||||
import { useExploresStore } from '@/stores/explore.ts';
|
||||
|
||||
import { TransactionType } from '@/core/transaction.ts';
|
||||
|
||||
import {
|
||||
type TransactionInsightDataItem
|
||||
} from '@/models/transaction.ts';
|
||||
|
||||
import {
|
||||
getUtcOffsetByUtcOffsetMinutes,
|
||||
getTimezoneOffsetMinutes,
|
||||
parseDateTimeFromUnixTime
|
||||
} from '@/lib/datetime.ts';
|
||||
|
||||
import {
|
||||
mdiArrowRight,
|
||||
mdiPencilBoxOutline
|
||||
} from '@mdi/js';
|
||||
|
||||
interface InsightsExploreDataTableTabProps {
|
||||
loading?: boolean;
|
||||
countPerPage: number;
|
||||
}
|
||||
|
||||
const props = defineProps<InsightsExploreDataTableTabProps>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:countPerPage', value: number): void;
|
||||
}>();
|
||||
|
||||
const {
|
||||
tt,
|
||||
formatUnixTimeToLongDateTime,
|
||||
formatUnixTimeToGregorianDefaultDateTime,
|
||||
formatAmountToWesternArabicNumeralsWithoutDigitGrouping,
|
||||
formatAmountToLocalizedNumeralsWithCurrency
|
||||
} = useI18n();
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
const userStore = useUserStore();
|
||||
const exploresStore = useExploresStore();
|
||||
|
||||
const currentPage = ref<number>(1);
|
||||
|
||||
const currentTimezoneOffsetMinutes = computed<number>(() => getTimezoneOffsetMinutes(settingsStore.appSettings.timeZone));
|
||||
const defaultCurrency = computed<string>(() => userStore.currentUserDefaultCurrency);
|
||||
|
||||
const filteredTransactions = computed<TransactionInsightDataItem[]>(() => exploresStore.filteredTransactions);
|
||||
|
||||
const itemsPerPage = computed<number>({
|
||||
get: () => props.countPerPage,
|
||||
set: (value: number) => emit('update:countPerPage', value)
|
||||
})
|
||||
|
||||
const skeletonData = computed<number[]>(() => {
|
||||
const data: number[] = [];
|
||||
|
||||
for (let i = 0; i < itemsPerPage.value; i++) {
|
||||
data.push(i);
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
const totalPageCount = computed<number>(() => {
|
||||
if (!filteredTransactions.value || filteredTransactions.value.length < 1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const count = filteredTransactions.value.length;
|
||||
return Math.ceil(count / itemsPerPage.value);
|
||||
});
|
||||
|
||||
const dataTableHeaders = computed<object[]>(() => {
|
||||
const headers: object[] = [];
|
||||
|
||||
headers.push({ key: 'time', value: 'time', title: tt('Transaction Time'), sortable: true, nowrap: true });
|
||||
headers.push({ key: 'type', value: 'type', title: tt('Type'), sortable: true, nowrap: true });
|
||||
headers.push({ key: 'secondaryCategoryName', value: 'secondaryCategoryName', title: tt('Category'), sortable: true, nowrap: true });
|
||||
headers.push({ key: 'sourceAmount', value: 'sourceAmount', title: tt('Amount'), sortable: true, nowrap: true });
|
||||
headers.push({ key: 'sourceAccountName', value: 'sourceAccountName', title: tt('Account'), sortable: true, nowrap: true });
|
||||
headers.push({ key: 'comment', value: 'comment', title: tt('Description'), sortable: true, nowrap: true });
|
||||
return headers;
|
||||
});
|
||||
|
||||
function getDisplayDateTime(transaction: TransactionInsightDataItem): string {
|
||||
return formatUnixTimeToLongDateTime(transaction.time, transaction.utcOffset, currentTimezoneOffsetMinutes.value);
|
||||
}
|
||||
|
||||
function getDisplayTimezone(transaction: TransactionInsightDataItem): string {
|
||||
return `UTC${getUtcOffsetByUtcOffsetMinutes(transaction.utcOffset)}`;
|
||||
}
|
||||
|
||||
function getDisplayTransactionType(transaction: TransactionInsightDataItem): string {
|
||||
if (transaction.type === TransactionType.ModifyBalance) {
|
||||
return tt('Modify Balance');
|
||||
} else if (transaction.type === TransactionType.Income) {
|
||||
return tt('Income');
|
||||
} else if (transaction.type === TransactionType.Expense) {
|
||||
return tt('Expense');
|
||||
} else if (transaction.type === TransactionType.Transfer) {
|
||||
return tt('Transfer');
|
||||
} else {
|
||||
return tt('Unknown');
|
||||
}
|
||||
}
|
||||
|
||||
function getTransactionTypeColor(transaction: TransactionInsightDataItem): string | undefined {
|
||||
if (transaction.type === TransactionType.ModifyBalance) {
|
||||
return 'secondary';
|
||||
} else if (transaction.type === TransactionType.Income) {
|
||||
return undefined;
|
||||
} else if (transaction.type === TransactionType.Expense) {
|
||||
return undefined;
|
||||
} else if (transaction.type === TransactionType.Transfer) {
|
||||
return 'primary';
|
||||
} else {
|
||||
return 'default';
|
||||
}
|
||||
}
|
||||
|
||||
function getDisplaySourceAmount(transaction: TransactionInsightDataItem): string {
|
||||
let currency = defaultCurrency.value;
|
||||
|
||||
if (transaction.sourceAccount) {
|
||||
currency = transaction.sourceAccount.currency;
|
||||
}
|
||||
|
||||
return formatAmountToLocalizedNumeralsWithCurrency(transaction.sourceAmount, currency);
|
||||
}
|
||||
|
||||
function getDisplayDestinationAmount(transaction: TransactionInsightDataItem): string {
|
||||
let currency = defaultCurrency.value;
|
||||
|
||||
if (transaction.destinationAccount) {
|
||||
currency = transaction.destinationAccount.currency;
|
||||
}
|
||||
|
||||
return formatAmountToLocalizedNumeralsWithCurrency(transaction.destinationAmount, currency);
|
||||
}
|
||||
|
||||
function buildExportResults(): { headers: string[], data: string[][] } | undefined {
|
||||
if (!filteredTransactions.value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
headers: [
|
||||
tt('Transaction Time'),
|
||||
tt('Type'),
|
||||
tt('Category'),
|
||||
tt('Amount'),
|
||||
tt('Account'),
|
||||
tt('Description')
|
||||
],
|
||||
data: filteredTransactions.value
|
||||
.map(transaction => {
|
||||
const transactionTime = parseDateTimeFromUnixTime(transaction.time, transaction.utcOffset, currentTimezoneOffsetMinutes.value).getUnixTime();
|
||||
const type = getDisplayTransactionType(transaction);
|
||||
|
||||
let categoryName = transaction.secondaryCategoryName;
|
||||
let displayAmount = formatAmountToWesternArabicNumeralsWithoutDigitGrouping(transaction.sourceAmount);
|
||||
let displayAccountName = transaction.sourceAccountName;
|
||||
|
||||
if (transaction.type === TransactionType.ModifyBalance) {
|
||||
categoryName = tt('Modify Balance');
|
||||
} else if (transaction.type === TransactionType.Transfer && transaction.sourceAccount?.id !== transaction.destinationAccount?.id && getDisplaySourceAmount(transaction) !== getDisplayDestinationAmount(transaction)) {
|
||||
displayAmount = displayAmount + ' → ' + formatAmountToWesternArabicNumeralsWithoutDigitGrouping(transaction.destinationAmount);
|
||||
}
|
||||
|
||||
if (transaction.type === TransactionType.Transfer && transaction.destinationAccount) {
|
||||
displayAccountName = displayAccountName + ' → ' + (transaction.destinationAccount?.name || '');
|
||||
}
|
||||
|
||||
const description = transaction.comment || '';
|
||||
|
||||
return [
|
||||
formatUnixTimeToGregorianDefaultDateTime(transactionTime),
|
||||
type,
|
||||
categoryName,
|
||||
displayAmount,
|
||||
displayAccountName,
|
||||
description
|
||||
];
|
||||
}
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
buildExportResults
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.v-table.insights-explore-table > .v-table__wrapper > table {
|
||||
th:not(:last-child),
|
||||
td:not(:last-child) {
|
||||
width: auto !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
th:last-child,
|
||||
td:last-child {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.v-table.insights-explore-table.loading-skeleton tr.v-data-table-rows-no-data > td {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,611 @@
|
||||
<template>
|
||||
<v-card-text class="pt-0">
|
||||
<div class="d-flex gap-2">
|
||||
<v-btn color="primary" variant="outlined"
|
||||
:disabled="loading"
|
||||
@click="addQuery">{{ tt('Add Query') }}</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn color="secondary" variant="tonal"
|
||||
:disabled="loading || queries.length < 1"
|
||||
@click="clearAllQueries">{{ tt('Clear All') }}</v-btn>
|
||||
</div>
|
||||
|
||||
<div :key="queryIndex" v-for="(query, queryIndex) in queries">
|
||||
<v-card class="mt-4" variant="outlined">
|
||||
<v-card-title class="d-flex align-center py-2 px-4">
|
||||
<span class="text-subtitle-1">{{ tt('Query') }} {{ `#${queryIndex + 1}` }}</span>
|
||||
<v-spacer />
|
||||
<v-switch class="bidirectional-switch ms-2" color="secondary"
|
||||
:label="tt('Expression')"
|
||||
v-model="showExpression"
|
||||
@click="showExpression = !showExpression">
|
||||
<template #prepend>
|
||||
<span>{{ tt('Editor') }}</span>
|
||||
</template>
|
||||
</v-switch>
|
||||
<v-btn class="ms-2" density="compact" color="default" variant="text" size="small"
|
||||
:icon="true" :disabled="loading || queries.length < 1"
|
||||
@click="removeQuery(queryIndex)">
|
||||
<v-icon :icon="mdiClose" size="18" />
|
||||
<v-tooltip activator="parent">{{ tt('Remove Query') }}</v-tooltip>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<div class="text-center py-4" v-if="!query.conditions || query.conditions.length < 1">
|
||||
{{ tt('No conditions defined. All transactions will match.') }}
|
||||
</div>
|
||||
|
||||
<div v-if="!showExpression">
|
||||
<div :key="conditionIndex" v-for="(conditionWithRelation, conditionIndex) in query.conditions">
|
||||
<div class="d-flex align-center gap-2 mb-4">
|
||||
<v-select
|
||||
disabled
|
||||
class="flex-0-0"
|
||||
width="120px"
|
||||
density="compact"
|
||||
item-title="displayName"
|
||||
item-value="value"
|
||||
:items="[{ value: TransactionExploreConditionRelation.First, displayName: tt('WHERE') }]"
|
||||
:model-value="TransactionExploreConditionRelation.First"
|
||||
v-if="conditionIndex < 1"
|
||||
/>
|
||||
|
||||
<v-select
|
||||
class="flex-0-0"
|
||||
width="120px"
|
||||
density="compact"
|
||||
item-title="displayName"
|
||||
item-value="value"
|
||||
:disabled="loading"
|
||||
:items="[
|
||||
{ value: TransactionExploreConditionRelation.And, displayName: tt('AND') },
|
||||
{ value: TransactionExploreConditionRelation.Or, displayName: tt('OR') }
|
||||
]"
|
||||
v-model="conditionWithRelation.relation"
|
||||
v-else-if="conditionIndex >= 1"
|
||||
/>
|
||||
|
||||
<v-select
|
||||
class="flex-0-0"
|
||||
density="compact"
|
||||
item-title="name"
|
||||
item-value="value"
|
||||
:disabled="loading"
|
||||
:items="allTransactionExploreConditionFields"
|
||||
@update:model-value="updateConditionField(queryIndex, conditionIndex, TransactionExploreConditionField.valueOf($event))"
|
||||
v-model="conditionWithRelation.condition.field"
|
||||
/>
|
||||
|
||||
<v-select
|
||||
class="flex-0-0"
|
||||
density="compact"
|
||||
item-title="name"
|
||||
item-value="value"
|
||||
:disabled="loading"
|
||||
:items="getAllTransactionExploreConditionOperators(conditionWithRelation.getSupportedOperators())"
|
||||
v-model="conditionWithRelation.condition.operator"
|
||||
/>
|
||||
|
||||
<div class="d-flex w-100 flex-1-1">
|
||||
<v-select
|
||||
multiple chips closable-chips
|
||||
density="compact"
|
||||
item-title="displayName"
|
||||
item-value="type"
|
||||
:disabled="loading"
|
||||
:placeholder="tt('None')"
|
||||
:items="[
|
||||
{ type: TransactionType.Expense, displayName: tt('Expense') },
|
||||
{ type: TransactionType.Income, displayName: tt('Income') },
|
||||
{ type: TransactionType.Transfer, displayName: tt('Transfer') }
|
||||
]"
|
||||
v-model="conditionWithRelation.condition.value"
|
||||
v-if="conditionWithRelation.condition.field === TransactionExploreConditionField.TransactionType.value"
|
||||
>
|
||||
<template #item="{ props, item }">
|
||||
<v-list-item :value="item.value" v-bind="props">
|
||||
<template #title>
|
||||
<v-list-item-title>
|
||||
<div class="d-flex align-center">{{ item.title }}</div>
|
||||
</v-list-item-title>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-select>
|
||||
|
||||
<v-text-field
|
||||
class="always-cursor-pointer"
|
||||
density="compact"
|
||||
item-title="displayName"
|
||||
item-value="type"
|
||||
persistent-placeholder
|
||||
:readonly="true"
|
||||
:disabled="loading || !hasAnyTransactionCategory"
|
||||
:placeholder="tt('None')"
|
||||
:model-value="getFilteredTransactionCategoriesDisplayContent(arrayItemToObjectField(conditionWithRelation.condition.value as string[], true))"
|
||||
@click="currentCondition = conditionWithRelation.condition; showFilterTransactionCategoriesDialog = true"
|
||||
v-else-if="conditionWithRelation.condition.field === TransactionExploreConditionField.TransactionCategory.value"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
class="always-cursor-pointer"
|
||||
density="compact"
|
||||
item-title="displayName"
|
||||
item-value="type"
|
||||
persistent-placeholder
|
||||
:readonly="true"
|
||||
:disabled="loading || !hasAnyAccount"
|
||||
:placeholder="tt('None')"
|
||||
:model-value="getFilteredAccountsDisplayContent(arrayItemToObjectField(conditionWithRelation.condition.value as string[], true))"
|
||||
@click="currentCondition = conditionWithRelation.condition; showFilterSourceAccountsDialog = true"
|
||||
v-else-if="conditionWithRelation.condition.field === TransactionExploreConditionField.SourceAccount.value"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
class="always-cursor-pointer"
|
||||
density="compact"
|
||||
item-title="displayName"
|
||||
item-value="type"
|
||||
persistent-placeholder
|
||||
:readonly="true"
|
||||
:disabled="loading || !hasAnyAccount"
|
||||
:placeholder="tt('None')"
|
||||
:model-value="getFilteredAccountsDisplayContent(arrayItemToObjectField(conditionWithRelation.condition.value as string[], true))"
|
||||
@click="currentCondition = conditionWithRelation.condition; showFilterDestinationAccountsDialog = true"
|
||||
v-else-if="conditionWithRelation.condition.field === TransactionExploreConditionField.DestinationAccount.value"
|
||||
/>
|
||||
|
||||
<div class="d-flex w-100 align-center gap-2"
|
||||
v-else-if="conditionWithRelation.condition.field === TransactionExploreConditionField.SourceAmount.value ||
|
||||
conditionWithRelation.condition.field === TransactionExploreConditionField.DestinationAmount.value">
|
||||
<amount-input density="compact"
|
||||
:currency="defaultCurrency"
|
||||
:disabled="loading"
|
||||
v-model="conditionWithRelation.condition.value[0]"
|
||||
/>
|
||||
<span class="ms-2 me-2"
|
||||
v-if="conditionWithRelation.condition.operator === TransactionExploreConditionOperator.Between.value ||
|
||||
conditionWithRelation.condition.operator === TransactionExploreConditionOperator.NotBetween.value">~</span>
|
||||
<amount-input density="compact"
|
||||
:currency="defaultCurrency"
|
||||
:disabled="loading"
|
||||
v-model="conditionWithRelation.condition.value[1]"
|
||||
v-if="conditionWithRelation.condition.operator === TransactionExploreConditionOperator.Between.value ||
|
||||
conditionWithRelation.condition.operator === TransactionExploreConditionOperator.NotBetween.value"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="d-flex w-100" v-else-if="conditionWithRelation.condition.field === TransactionExploreConditionField.TransactionTag.value">
|
||||
<v-text-field
|
||||
disabled
|
||||
persistent-placeholder
|
||||
density="compact"
|
||||
:placeholder="tt('None')"
|
||||
v-if="conditionWithRelation.condition.field === TransactionExploreConditionField.TransactionTag.value &&
|
||||
(conditionWithRelation.condition.operator === TransactionExploreConditionOperator.IsEmpty.value || conditionWithRelation.condition.operator === TransactionExploreConditionOperator.IsNotEmpty.value)"
|
||||
/>
|
||||
|
||||
<v-autocomplete
|
||||
density="compact"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
auto-select-first
|
||||
persistent-placeholder
|
||||
multiple
|
||||
chips
|
||||
closable-chips
|
||||
:disabled="loading"
|
||||
:placeholder="tt('None')"
|
||||
:items="allTags"
|
||||
v-model="conditionWithRelation.condition.value"
|
||||
v-model:search="tagSearchContent"
|
||||
v-else-if="conditionWithRelation.condition.operator !== TransactionExploreConditionOperator.IsEmpty.value && conditionWithRelation.condition.operator !== TransactionExploreConditionOperator.IsNotEmpty.value"
|
||||
>
|
||||
<template #chip="{ props, item }">
|
||||
<v-chip :prepend-icon="mdiPound" :text="item.title" v-bind="props"/>
|
||||
</template>
|
||||
|
||||
<template #item="{ props, item }">
|
||||
<v-list-item :value="item.value" v-bind="props" v-if="!item.raw.hidden">
|
||||
<template #title>
|
||||
<v-list-item-title>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="20" start :icon="mdiPound"/>
|
||||
<span>{{ item.title }}</span>
|
||||
</div>
|
||||
</v-list-item-title>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item :disabled="true" v-bind="props"
|
||||
v-if="item.raw.hidden && item.raw.name.toLowerCase().indexOf(tagSearchContent.toLowerCase()) >= 0 && isAllFilteredTagHidden">
|
||||
<template #title>
|
||||
<v-list-item-title>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="20" start :icon="mdiPound"/>
|
||||
<span>{{ item.title }}</span>
|
||||
</div>
|
||||
</v-list-item-title>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<template #no-data>
|
||||
<v-list class="py-0">
|
||||
<v-list-item>{{ tt('No available tag') }}</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</div>
|
||||
|
||||
<v-text-field disabled density="compact"
|
||||
:placeholder="tt('None')"
|
||||
v-else-if="conditionWithRelation.condition.field === TransactionExploreConditionField.Description.value &&
|
||||
conditionWithRelation.condition.operator === TransactionExploreConditionOperator.IsEmpty.value || conditionWithRelation.condition.operator === TransactionExploreConditionOperator.IsNotEmpty.value"
|
||||
/>
|
||||
|
||||
<v-text-field density="compact"
|
||||
:disabled="loading"
|
||||
:placeholder="tt('None')"
|
||||
v-model="conditionWithRelation.condition.value"
|
||||
v-else-if="conditionWithRelation.condition.field === TransactionExploreConditionField.Description.value &&
|
||||
conditionWithRelation.condition.operator !== TransactionExploreConditionOperator.IsEmpty.value && conditionWithRelation.condition.operator !== TransactionExploreConditionOperator.IsNotEmpty.value"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<v-btn color="default" density="compact"
|
||||
variant="text" size="small"
|
||||
:icon="true"
|
||||
:disabled="loading"
|
||||
@click="removeCondition(queryIndex, conditionIndex)">
|
||||
<v-icon :icon="mdiClose" size="18" />
|
||||
<v-tooltip activator="parent">{{ tt('Remove Condition') }}</v-tooltip>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="showExpression">
|
||||
<div class="w-100 code-container">
|
||||
<v-textarea class="w-100 always-cursor-text mb-4" :readonly="true"
|
||||
:value="getExpression(queryIndex)"></v-textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-btn color="primary" density="comfortable"
|
||||
variant="text" size="small"
|
||||
:prepend-icon="mdiPlus"
|
||||
:disabled="loading || showExpression"
|
||||
@click="addCondition(queryIndex)">
|
||||
{{ tt('Add Condition') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<div class="query-group-separator d-flex align-center justify-center my-4"
|
||||
v-if="queries.length > 1 && queryIndex < queries.length - 1">
|
||||
<v-chip color="primary" variant="outlined" size="small">
|
||||
{{ tt('or') }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-dialog width="800" v-model="showFilterSourceAccountsDialog">
|
||||
<account-filter-settings-card type="custom" :dialog-mode="true"
|
||||
:selected-account-ids="isArray(currentCondition?.value) ? currentCondition?.value as string[] : []"
|
||||
@settings:change="updateSourceAccount" />
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog width="800" v-model="showFilterDestinationAccountsDialog">
|
||||
<account-filter-settings-card type="custom" :dialog-mode="true"
|
||||
:selected-account-ids="isArray(currentCondition?.value) ? currentCondition?.value as string[] : []"
|
||||
@settings:change="updateDestinationAccount" />
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog width="800" v-model="showFilterTransactionCategoriesDialog">
|
||||
<category-filter-settings-card type="custom" :dialog-mode="true"
|
||||
:selected-category-ids="isArray(currentCondition?.value) ? currentCondition?.value as string[] : []"
|
||||
@settings:change="updateTransactionCategories" />
|
||||
</v-dialog>
|
||||
|
||||
<snack-bar ref="snackbar" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AccountFilterSettingsCard from '@/views/desktop/common/cards/AccountFilterSettingsCard.vue';
|
||||
import CategoryFilterSettingsCard from '@/views/desktop/common/cards/CategoryFilterSettingsCard.vue';
|
||||
|
||||
import SnackBar from '@/components/desktop/SnackBar.vue';
|
||||
|
||||
import { ref, computed, useTemplateRef } from 'vue';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
|
||||
import { useUserStore } from '@/stores/user.ts';
|
||||
import { useAccountsStore } from '@/stores/account.ts';
|
||||
import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts';
|
||||
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
|
||||
import { useExploresStore } from '@/stores/explore.ts';
|
||||
|
||||
import { type NameValue, values } from '@/core/base.ts';
|
||||
import { AccountType } from '@/core/account.ts';
|
||||
import { TransactionType } from '@/core/transaction.ts';
|
||||
import {
|
||||
TransactionExploreConditionRelation,
|
||||
TransactionExploreConditionField,
|
||||
TransactionExploreConditionOperator
|
||||
} from '@/core/explore.ts';
|
||||
|
||||
import {
|
||||
type TransactionTag
|
||||
} from '@/models/transaction_tag.ts';
|
||||
|
||||
import {
|
||||
type TransactionExploreCondition,
|
||||
TransactionExploreQuery
|
||||
} from '@/models/explore.ts';
|
||||
|
||||
import {
|
||||
isArray,
|
||||
isObjectEmpty,
|
||||
arrayItemToObjectField
|
||||
} from '@/lib/common.ts';
|
||||
|
||||
import logger from '@/lib/logger.ts';
|
||||
|
||||
import {
|
||||
mdiPlus,
|
||||
mdiClose,
|
||||
mdiPound
|
||||
} from '@mdi/js';
|
||||
|
||||
interface ExploreQueryTabProps {
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
type SnackBarType = InstanceType<typeof SnackBar>;
|
||||
|
||||
const props = defineProps<ExploreQueryTabProps>();
|
||||
|
||||
const {
|
||||
tt,
|
||||
joinMultiText,
|
||||
getAllTransactionExploreConditionFields,
|
||||
getAllTransactionExploreConditionOperators
|
||||
} = useI18n();
|
||||
|
||||
const userStore = useUserStore();
|
||||
const accountsStore = useAccountsStore();
|
||||
const transactionCategoriesStore = useTransactionCategoriesStore();
|
||||
const transactionTagsStore = useTransactionTagsStore();
|
||||
const exploresStore = useExploresStore();
|
||||
|
||||
const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
||||
|
||||
const currentCondition = ref<TransactionExploreCondition | undefined>(undefined);
|
||||
const showExpression = ref<boolean>(false);
|
||||
const showFilterSourceAccountsDialog = ref<boolean>(false);
|
||||
const showFilterDestinationAccountsDialog = ref<boolean>(false);
|
||||
const showFilterTransactionCategoriesDialog = ref<boolean>(false);
|
||||
const tagSearchContent = ref<string>('');
|
||||
|
||||
const queries = computed<TransactionExploreQuery[]>(() => exploresStore.transactionExploreFilter.query);
|
||||
|
||||
const defaultCurrency = computed<string>(() => userStore.currentUserDefaultCurrency);
|
||||
const hasAnyAccount = computed<boolean>(() => accountsStore.allPlainAccounts.length > 0);
|
||||
const hasAnyTransactionCategory = computed<boolean>(() => !isObjectEmpty(transactionCategoriesStore.allTransactionCategoriesMap));
|
||||
const allTags = computed<TransactionTag[]>(() => transactionTagsStore.allTransactionTags);
|
||||
|
||||
const allTransactionExploreConditionFields = computed<NameValue[]>(() => getAllTransactionExploreConditionFields());
|
||||
|
||||
const isAllFilteredTagHidden = computed<boolean>(() => {
|
||||
const lowerCaseTagSearchContent = tagSearchContent.value.toLowerCase();
|
||||
let hiddenCount = 0;
|
||||
|
||||
for (const tag of allTags.value) {
|
||||
if (!lowerCaseTagSearchContent || tag.name.toLowerCase().indexOf(lowerCaseTagSearchContent) >= 0) {
|
||||
if (!tag.hidden) {
|
||||
return false;
|
||||
}
|
||||
|
||||
hiddenCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return hiddenCount > 0;
|
||||
});
|
||||
|
||||
function getFilteredAccountsDisplayContent(filterAccountIds?: Record<string, boolean>): string {
|
||||
if ((props.loading && !hasAnyAccount.value) || !accountsStore.allVisiblePlainAccounts || !accountsStore.allVisiblePlainAccounts.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!filterAccountIds) {
|
||||
return tt('All');
|
||||
}
|
||||
|
||||
let allAccountSelected = true;
|
||||
const selectedAccountNames: string[] = [];
|
||||
|
||||
for (const account of accountsStore.allPlainAccounts) {
|
||||
if (account.type === AccountType.MultiSubAccounts.type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!filterAccountIds[account.id]) {
|
||||
allAccountSelected = false;
|
||||
} else {
|
||||
selectedAccountNames.push(account.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (allAccountSelected) {
|
||||
return tt('All');
|
||||
} else if (selectedAccountNames.length < 1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return joinMultiText(selectedAccountNames);
|
||||
}
|
||||
|
||||
function getFilteredTransactionCategoriesDisplayContent(filterTransactionCategoryIds?: Record<string, boolean>): string {
|
||||
if ((props.loading && !hasAnyTransactionCategory.value) || !transactionCategoriesStore.allTransactionCategoriesMap) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!filterTransactionCategoryIds) {
|
||||
return tt('All');
|
||||
}
|
||||
|
||||
let allCategorySelected = true;
|
||||
const selectedCategoryNames: string[] = [];
|
||||
|
||||
for (const transactionCategory of values(transactionCategoriesStore.allTransactionCategoriesMap)) {
|
||||
if (!transactionCategory.parentId || transactionCategory.parentId === '0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!filterTransactionCategoryIds[transactionCategory.id]) {
|
||||
allCategorySelected = false;
|
||||
} else {
|
||||
selectedCategoryNames.push(transactionCategory.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (allCategorySelected) {
|
||||
return tt('All');
|
||||
} else if (selectedCategoryNames.length < 1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return joinMultiText(selectedCategoryNames);
|
||||
}
|
||||
|
||||
function addQuery(): void {
|
||||
queries.value.push(TransactionExploreQuery.create());
|
||||
}
|
||||
|
||||
function removeQuery(queryIndex: number): void {
|
||||
if (queries.value.length > 0) {
|
||||
queries.value.splice(queryIndex, 1);
|
||||
}
|
||||
|
||||
if (queries.value.length < 1) {
|
||||
queries.value.push(TransactionExploreQuery.create());
|
||||
}
|
||||
}
|
||||
|
||||
function clearAllQueries(): void {
|
||||
queries.value.length = 0;
|
||||
queries.value.push(TransactionExploreQuery.create());
|
||||
}
|
||||
|
||||
function addCondition(queryIndex: number): void {
|
||||
const query = queries.value[queryIndex];
|
||||
|
||||
if (!query) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newCondition = query.addNewCondition(TransactionExploreConditionField.TransactionType, query.conditions.length < 1);
|
||||
query.conditions.push(newCondition);
|
||||
}
|
||||
|
||||
function removeCondition(queryIndex: number, conditionIndex: number): void {
|
||||
const query = queries.value[queryIndex];
|
||||
|
||||
if (!query) {
|
||||
return;
|
||||
}
|
||||
|
||||
query.conditions.splice(conditionIndex, 1);
|
||||
|
||||
if (conditionIndex === 0 && query.conditions.length > 0) {
|
||||
const newFirstCondition = query.conditions[0];
|
||||
|
||||
if (newFirstCondition) {
|
||||
newFirstCondition.relation = TransactionExploreConditionRelation.First;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateConditionField(queryIndex: number, conditionIndex: number, newField: TransactionExploreConditionField | undefined): void {
|
||||
if (!newField) {
|
||||
return;
|
||||
}
|
||||
|
||||
const query = queries.value[queryIndex];
|
||||
|
||||
if (!query) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldConditionWithRelation = query.conditions[conditionIndex];
|
||||
|
||||
if (!oldConditionWithRelation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newConditionWithRelation = query.addNewCondition(newField, conditionIndex < 1);
|
||||
oldConditionWithRelation.condition = newConditionWithRelation.condition;
|
||||
}
|
||||
|
||||
function updateSourceAccount(changed: boolean, selectedAccountIds?: string[]): void {
|
||||
if (!changed || !currentCondition.value || currentCondition.value.field !== TransactionExploreConditionField.SourceAccount.value) {
|
||||
showFilterSourceAccountsDialog.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
currentCondition.value.value = selectedAccountIds || [];
|
||||
currentCondition.value = undefined;
|
||||
showFilterSourceAccountsDialog.value = false;
|
||||
}
|
||||
|
||||
function updateDestinationAccount(changed: boolean, selectedAccountIds?: string[]): void {
|
||||
if (!changed || !currentCondition.value || currentCondition.value.field !== TransactionExploreConditionField.DestinationAccount.value) {
|
||||
showFilterDestinationAccountsDialog.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
currentCondition.value.value = selectedAccountIds || [];
|
||||
currentCondition.value = undefined;
|
||||
showFilterDestinationAccountsDialog.value = false;
|
||||
}
|
||||
|
||||
function updateTransactionCategories(changed: boolean, selectedCategoryIds?: string[]): void {
|
||||
if (!changed || !currentCondition.value || currentCondition.value.field !== TransactionExploreConditionField.TransactionCategory.value) {
|
||||
showFilterTransactionCategoriesDialog.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
currentCondition.value.value = selectedCategoryIds || [];
|
||||
currentCondition.value = undefined;
|
||||
showFilterTransactionCategoriesDialog.value = false;
|
||||
}
|
||||
|
||||
function getExpression(queryIndex: number): string {
|
||||
const query = queries.value[queryIndex];
|
||||
|
||||
if (!query) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
return query.toExpression(transactionCategoriesStore.allTransactionCategoriesMap, accountsStore.allAccountsMap, transactionTagsStore.allTransactionTagsMap);
|
||||
} catch (ex) {
|
||||
logger.error('failed to generate expression for explore query#' + queryIndex, ex);
|
||||
snackbar.value?.showError(tt('Failed to generate expression'));
|
||||
return tt('Failed to generate expression');
|
||||
}
|
||||
}
|
||||
|
||||
if (queries.value.length === 0) {
|
||||
queries.value.push(TransactionExploreQuery.create());
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user