diff --git a/cmd/webserver.go b/cmd/webserver.go index 173b73dc..79ac3bc7 100644 --- a/cmd/webserver.go +++ b/cmd/webserver.go @@ -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)) diff --git a/pkg/api/transactions.go b/pkg/api/transactions.go index 4aff8f4f..cc6e7ed3 100644 --- a/pkg/api/transactions.go +++ b/pkg/api/transactions.go @@ -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 diff --git a/pkg/models/transaction.go b/pkg/models/transaction.go index 42e983bd..29b7c56d 100644 --- a/pkg/models/transaction.go +++ b/pkg/models/transaction.go @@ -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"` diff --git a/pkg/models/user_app_cloud_setting.go b/pkg/models/user_app_cloud_setting.go index 9f6f44d6..239033c7 100644 --- a/pkg/models/user_app_cloud_setting.go +++ b/pkg/models/user_app_cloud_setting.go @@ -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 diff --git a/src/core/base.ts b/src/core/base.ts index aaa4dfac..5df1a301 100644 --- a/src/core/base.ts +++ b/src/core/base.ts @@ -21,7 +21,7 @@ export function* reversedItemAndIndex(arr: T[]): Iterable<[T, number]> { } } -export function* entries(obj: Record): Iterable<[string, V]> { +export function* entries(obj: Record | PartialRecord): Iterable<[string, V]> { for (const key in obj) { if (!Object.prototype.hasOwnProperty.call(obj, key)) { continue; @@ -31,7 +31,7 @@ export function* entries(obj: Record(obj: Record): Iterable { +export function* keys(obj: Record | PartialRecord): Iterable { for (const key in obj) { if (!Object.prototype.hasOwnProperty.call(obj, key)) { continue; @@ -41,7 +41,7 @@ export function* keys(obj: Record): } } -export function* keysIfValueEquals(obj: Record, value: V): Iterable { +export function* keysIfValueEquals(obj: Record | PartialRecord, value: V): Iterable { for (const key in obj) { if (!Object.prototype.hasOwnProperty.call(obj, key)) { continue; @@ -55,7 +55,7 @@ export function* keysIfValueEquals(obj: R } } -export function* values(obj: Record): Iterable { +export function* values(obj: Record | PartialRecord): Iterable { for (const key in obj) { if (!Object.prototype.hasOwnProperty.call(obj, key)) { continue; diff --git a/src/core/datetime.ts b/src/core/datetime.ts index 79844219..72946c8c 100644 --- a/src/core/datetime.ts +++ b/src/core/datetime.ts @@ -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 = {}; // 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; diff --git a/src/core/explore.ts b/src/core/explore.ts new file mode 100644 index 00000000..9dc77f14 --- /dev/null +++ b/src/core/explore.ts @@ -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.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 = {}; + + 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 = {}; + + 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; diff --git a/src/core/setting.ts b/src/core/setting.ts index 500b4a80..cf9dc28b 100644 --- a/src/core/setting.ts +++ b/src/core/setting.ts @@ -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; // Exchange Rates Data Page @@ -111,6 +115,9 @@ export const ALL_ALLOWED_CLOUD_SYNC_APP_SETTING_KEY_TYPES: Record>(`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 => { + return axios.get>(`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 => { return axios.get>(`v1/transactions/reconciliation_statements.json?account_id=${req.accountId}&start_time=${req.startTime}&end_time=${req.endTime}`); }, diff --git a/src/locales/de.json b/src/locales/de.json index 777f9d0e..42a620ab 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -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", diff --git a/src/locales/en.json b/src/locales/en.json index 9e7f8cc0..21e26c81 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -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", diff --git a/src/locales/es.json b/src/locales/es.json index ebe55baf..a9ba5673 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -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", diff --git a/src/locales/fr.json b/src/locales/fr.json index f0905d74..2b8d28d5 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -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", diff --git a/src/locales/helpers.ts b/src/locales/helpers.ts index 78a67b2c..f866943b 100644 --- a/src/locales/helpers.ts +++ b/src/locales/helpers.ts @@ -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, diff --git a/src/locales/it.json b/src/locales/it.json index 5826e2e5..78f67eb8 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -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", diff --git a/src/locales/ja.json b/src/locales/ja.json index d595f2d1..d587c13c 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -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": "為替レートデータページ", diff --git a/src/locales/kn.json b/src/locales/kn.json index e6653113..5a414c95 100644 --- a/src/locales/kn.json +++ b/src/locales/kn.json @@ -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": "ವಿನಿಮಯ ದರಗಳ ಪುಟ", diff --git a/src/locales/ko.json b/src/locales/ko.json index 3e3b4031..cb7cb877 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -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": "환율 데이터 페이지", diff --git a/src/locales/nl.json b/src/locales/nl.json index 5fd5cc38..8ff54416 100644 --- a/src/locales/nl.json +++ b/src/locales/nl.json @@ -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", diff --git a/src/locales/pt_BR.json b/src/locales/pt_BR.json index f883a70b..68ba7ded 100644 --- a/src/locales/pt_BR.json +++ b/src/locales/pt_BR.json @@ -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", diff --git a/src/locales/ru.json b/src/locales/ru.json index 15a8b9b8..aefa83bb 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -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": "Страница данных о курсах валют", diff --git a/src/locales/th.json b/src/locales/th.json index 31041407..06f41794 100644 --- a/src/locales/th.json +++ b/src/locales/th.json @@ -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": "หน้าข้อมูลอัตราแลกเปลี่ยน", diff --git a/src/locales/tr.json b/src/locales/tr.json index 1fe7425b..594f8b76 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -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ı", diff --git a/src/locales/uk.json b/src/locales/uk.json index 81c22ba4..44904612 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -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": "Сторінка курсів валют", diff --git a/src/locales/vi.json b/src/locales/vi.json index 1863ec32..ede1a532 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -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", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index fae2077d..2994f6b4 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -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": "汇率数据页面", diff --git a/src/locales/zh_Hant.json b/src/locales/zh_Hant.json index 1114cd33..bc2b92f0 100644 --- a/src/locales/zh_Hant.json +++ b/src/locales/zh_Hant.json @@ -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": "匯率資料頁面", diff --git a/src/models/explore.ts b/src/models/explore.ts new file mode 100644 index 00000000..810f4940 --- /dev/null +++ b/src/models/explore.ts @@ -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, allAccountsMap: Record, allTagsMap: Record): 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 = {}; + + 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 { + readonly field: T; + readonly operator: TransactionExploreConditionOperatorType; + value: V; + + getValueForStore(): V; + match(transaction: TransactionInsightDataItem): boolean; + toExpression(allCategoriesMap: Record, allAccountsMap: Record, allTagsMap: Record): string; +} + +export class TransactionExploreTransactionTypeCondition implements TransactionExploreCondition { + public static readonly supportedOperators: PartialRecord = { + [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 { + public static readonly supportedOperators: PartialRecord = { + [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 { + 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 { + public static readonly supportedOperators: PartialRecord = { + [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, allAccountsMap: Record): 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 { + public static readonly supportedOperators: PartialRecord = { + [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, allAccountsMap: Record): 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 implements TransactionExploreCondition { + public static readonly supportedOperators: PartialRecord = { + [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, allAccountsMap: Record, allTagsMap: Record): 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 { + 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 { + 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 { + public static readonly supportedOperators: PartialRecord = { + [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 = {}; + + 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, allAccountsMap: Record, allTagsMap: Record): 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 { + public static readonly supportedOperators: PartialRecord = { + [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 ''; + } +} diff --git a/src/models/transaction.ts b/src/models/transaction.ts index 0ba42b5f..c3ad03f7 100644 --- a/src/models/transaction.ts +++ b/src/models/transaction.ts @@ -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; export interface TransactionAmountsResponseItem { diff --git a/src/router/desktop.ts b/src/router/desktop.ts index 3425515e..f653b34b 100644 --- a/src/router/desktop.ts +++ b/src/router/desktop.ts @@ -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, diff --git a/src/stores/explore.ts b/src/stores/explore.ts new file mode 100644 index 00000000..f73aa5ee --- /dev/null +++ b/src/stores/explore.ts @@ -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({ + dateRangeType: DEFAULT_TRANSACTION_EXPLORE_DATE_RANGE.type, + startTime: 0, + endTime: 0, + query: [] + }); + + const transactionExploreAllData = ref([]); + const transactionExploreStateInvalid = ref(true); + + const allTransactions = computed(() => { + 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(() => { + 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 { + 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 + }; +}); diff --git a/src/stores/index.ts b/src/stores/index.ts index 9d962093..7a822f16 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -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(null); @@ -60,6 +62,7 @@ export const useRootStore = defineStore('root', () => { setNotificationContent(null); + exploresStore.resetTransactionExplores(); statisticsStore.resetTransactionStatistics(); overviewStore.resetTransactionOverview(); transactionsStore.resetTransactions(); diff --git a/src/stores/setting.ts b/src/stores/setting.ts index 703edb89..8844c888 100644 --- a/src/stores/setting.ts +++ b/src/stores/setting.ts @@ -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): 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 diff --git a/src/views/base/settings/AccountFilterSettingPageBase.ts b/src/views/base/settings/AccountFilterSettingPageBase.ts index 95526daf..49969ce5 100644 --- a/src/views/base/settings/AccountFilterSettingPageBase.ts +++ b/src/views/base/settings/AccountFilterSettingPageBase.ts @@ -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(() => { - return type === 'statisticsDefault' || type === 'statisticsCurrent' || type === 'homePageOverview' || type === 'transactionListCurrent'; + return type === 'statisticsDefault' || type === 'statisticsCurrent' || type === 'homePageOverview' || type === 'transactionListCurrent' || type === 'custom'; }); const allCategorizedAccounts = computed>(() => 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 = {}; 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 { diff --git a/src/views/base/settings/AppCloudSyncPageBase.ts b/src/views/base/settings/AppCloudSyncPageBase.ts index a36a2f16..1d304082 100644 --- a/src/views/base/settings/AppCloudSyncPageBase.ts +++ b/src/views/base/settings/AppCloudSyncPageBase.ts @@ -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: [ diff --git a/src/views/base/settings/CategoryFilterSettingPageBase.ts b/src/views/base/settings/CategoryFilterSettingPageBase.ts index 89094a54..f2ad35cd 100644 --- a/src/views/base/settings/CategoryFilterSettingPageBase.ts +++ b/src/views/base/settings/CategoryFilterSettingPageBase.ts @@ -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 = {}; 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 { diff --git a/src/views/desktop/MainLayout.vue b/src/views/desktop/MainLayout.vue index b6749b25..50ccb98c 100644 --- a/src/views/desktop/MainLayout.vue +++ b/src/views/desktop/MainLayout.vue @@ -44,6 +44,12 @@ {{ tt('Statistics & Analysis') }} +