From f4530e12e6dd82fb4cde68b2627e322551eb00c9 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Fri, 12 Apr 2024 09:51:01 +0800 Subject: [PATCH] support setting timezone type for the time range of statistical data --- pkg/api/transactions.go | 18 +- pkg/models/transaction.go | 8 +- pkg/services/transactions.go | 170 ++++++++++++++++-- pkg/utils/datetimes.go | 28 +++ pkg/utils/datetimes_test.go | 32 ++++ src/consts/timezone.js | 11 +- src/lib/i18n.js | 16 ++ src/lib/services.js | 8 +- src/lib/settings.js | 19 ++ src/locales/en.js | 4 + src/locales/zh_Hans.js | 4 + src/stores/overview.js | 4 + src/stores/setting.js | 10 ++ src/stores/statistics.js | 4 +- .../app/settings/tabs/AppBasicSettingTab.vue | 24 +++ .../settings/tabs/AppStatisticsSettingTab.vue | 23 +++ .../mobile/settings/PageSettingsPage.vue | 25 ++- src/views/mobile/statistics/SettingsPage.vue | 21 +++ 18 files changed, 398 insertions(+), 31 deletions(-) diff --git a/pkg/api/transactions.go b/pkg/api/transactions.go index 4383f284..f3c3315a 100644 --- a/pkg/api/transactions.go +++ b/pkg/api/transactions.go @@ -238,8 +238,15 @@ func (a *TransactionsApi) TransactionStatisticsHandler(c *core.Context) (any, *e return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) } + utcOffset, err := c.GetClientTimezoneOffset() + + if err != nil { + log.WarnfWithRequestId(c, "[transactions.TransactionStatisticsHandler] cannot get client timezone offset, because %s", err.Error()) + return nil, errs.ErrClientTimezoneOffsetInvalid + } + uid := c.GetCurrentUid() - totalAmounts, err := a.transactions.GetAccountsAndCategoriesTotalIncomeAndExpense(c, uid, statisticReq.StartTime, statisticReq.EndTime) + totalAmounts, err := a.transactions.GetAccountsAndCategoriesTotalIncomeAndExpense(c, uid, statisticReq.StartTime, statisticReq.EndTime, utcOffset, statisticReq.UseTransactionTimezone) if err != nil { log.ErrorfWithRequestId(c, "[transactions.TransactionStatisticsHandler] failed to get accounts and categories total income and expense for user \"uid:%d\", because %s", uid, err.Error()) @@ -292,6 +299,13 @@ func (a *TransactionsApi) TransactionAmountsHandler(c *core.Context) (any, *errs return nil, errs.ErrQueryItemsTooMuch } + utcOffset, err := c.GetClientTimezoneOffset() + + if err != nil { + log.WarnfWithRequestId(c, "[transactions.TransactionAmountsHandler] cannot get client timezone offset, because %s", err.Error()) + return nil, errs.ErrClientTimezoneOffsetInvalid + } + uid := c.GetCurrentUid() accounts, err := a.accounts.GetAllAccountsByUid(c, uid) @@ -307,7 +321,7 @@ func (a *TransactionsApi) TransactionAmountsHandler(c *core.Context) (any, *errs for i := 0; i < len(requestItems); i++ { requestItem := requestItems[i] - incomeAmounts, expenseAmounts, err := a.transactions.GetAccountsTotalIncomeAndExpense(c, uid, requestItem.StartTime, requestItem.EndTime) + incomeAmounts, expenseAmounts, err := a.transactions.GetAccountsTotalIncomeAndExpense(c, uid, requestItem.StartTime, requestItem.EndTime, utcOffset, transactionAmountsReq.UseTransactionTimezone) if err != nil { log.ErrorfWithRequestId(c, "[transactions.TransactionAmountsHandler] failed to get transaction amounts item for user \"uid:%d\", because %s", uid, err.Error()) diff --git a/pkg/models/transaction.go b/pkg/models/transaction.go index 0ee525fa..85a0644b 100644 --- a/pkg/models/transaction.go +++ b/pkg/models/transaction.go @@ -135,13 +135,15 @@ type TransactionListInMonthByPageRequest struct { // TransactionStatisticRequest represents all parameters of transaction statistic request type TransactionStatisticRequest struct { - StartTime int64 `form:"start_time" binding:"min=0"` - EndTime int64 `form:"end_time" binding:"min=0"` + StartTime int64 `form:"start_time" binding:"min=0"` + EndTime int64 `form:"end_time" binding:"min=0"` + UseTransactionTimezone bool `form:"use_transaction_timezone"` } // TransactionAmountsRequest represents all parameters of transaction amounts request type TransactionAmountsRequest struct { - Query string `form:"query"` + Query string `form:"query"` + UseTransactionTimezone bool `form:"use_transaction_timezone"` } // TransactionAmountsRequestItem represents an item of transaction amounts request diff --git a/pkg/services/transactions.go b/pkg/services/transactions.go index 98078c4d..e035c44c 100644 --- a/pkg/services/transactions.go +++ b/pkg/services/transactions.go @@ -15,6 +15,8 @@ import ( "github.com/mayswind/ezbookkeeping/pkg/uuid" ) +const pageCountForLoadTransactionAmounts = 1000 + // TransactionService represents transaction service type TransactionService struct { ServiceUsingDB @@ -1004,45 +1006,112 @@ func (s *TransactionService) GetRelatedTransferTransaction(originalTransaction * } // GetAccountsTotalIncomeAndExpense returns the every accounts total income and expense amount by specific date range -func (s *TransactionService) GetAccountsTotalIncomeAndExpense(c *core.Context, uid int64, startUnixTime int64, endUnixTime int64) (map[int64]int64, map[int64]int64, error) { +func (s *TransactionService) GetAccountsTotalIncomeAndExpense(c *core.Context, uid int64, startUnixTime int64, endUnixTime int64, utcOffset int16, useTransactionTimezone bool) (map[int64]int64, map[int64]int64, error) { if uid <= 0 { return nil, nil, errs.ErrUserIdInvalid } + clientLocation := time.FixedZone("Client Timezone", int(utcOffset)*60) + startLocalDateTime := utils.FormatUnixTimeToNumericLocalDateTime(startUnixTime, clientLocation) + endLocalDateTime := utils.FormatUnixTimeToNumericLocalDateTime(endUnixTime, clientLocation) + + startUnixTime = utils.GetMinUnixTimeWithSameLocalDateTime(startUnixTime, utcOffset) + endUnixTime = utils.GetMaxUnixTimeWithSameLocalDateTime(endUnixTime, utcOffset) + startTransactionTime := utils.GetMinTransactionTimeFromUnixTime(startUnixTime) endTransactionTime := utils.GetMaxTransactionTimeFromUnixTime(endUnixTime) - var transactionTotalAmounts []*models.Transaction - err := s.UserDataDB(uid).NewSession(c).Select("type, account_id, SUM(amount) as amount").Where("uid=? AND deleted=? AND (type=? OR type=?) AND transaction_time>=? AND transaction_time<=?", uid, false, models.TRANSACTION_DB_TYPE_INCOME, models.TRANSACTION_DB_TYPE_EXPENSE, startTransactionTime, endTransactionTime).GroupBy("type, account_id").Find(&transactionTotalAmounts) + condition := "uid=? AND deleted=? AND (type=? OR type=?) AND transaction_time>=? AND transaction_time<=?" + conditionParams := make([]any, 0, 4) + conditionParams = append(conditionParams, uid) + conditionParams = append(conditionParams, false) + conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_INCOME) + conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_EXPENSE) - if err != nil { - return nil, nil, err + minTransactionTime := startTransactionTime + maxTransactionTime := endTransactionTime + var allTransactions []*models.Transaction + + for maxTransactionTime > 0 { + var transactions []*models.Transaction + + finalConditionParams := make([]any, 0, 6) + finalConditionParams = append(finalConditionParams, conditionParams...) + finalConditionParams = append(finalConditionParams, minTransactionTime) + finalConditionParams = append(finalConditionParams, maxTransactionTime) + + err := s.UserDataDB(uid).NewSession(c).Select("type, account_id, transaction_time, timezone_utc_offset, amount").Where(condition, finalConditionParams...).Limit(pageCountForLoadTransactionAmounts, 0).OrderBy("transaction_time desc").Find(&transactions) + + if err != nil { + return nil, nil, err + } + + allTransactions = append(allTransactions, transactions...) + + if len(transactions) < pageCountForLoadTransactionAmounts { + maxTransactionTime = 0 + break + } + + maxTransactionTime = transactions[len(transactions)-1].TransactionTime - 1 } incomeAmounts := make(map[int64]int64) expenseAmounts := make(map[int64]int64) - for i := 0; i < len(transactionTotalAmounts); i++ { - transactionTotalAmount := transactionTotalAmounts[i] + for i := 0; i < len(allTransactions); i++ { + transaction := allTransactions[i] + timeZone := clientLocation + + if useTransactionTimezone { + timeZone = time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60) + } + + localDateTime := utils.FormatUnixTimeToNumericLocalDateTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), timeZone) + + if localDateTime < startLocalDateTime || localDateTime > endLocalDateTime { + continue + } + + var amountsMap map[int64]int64 + + if transaction.Type == models.TRANSACTION_DB_TYPE_INCOME { + amountsMap = incomeAmounts + } else if transaction.Type == models.TRANSACTION_DB_TYPE_EXPENSE { + amountsMap = expenseAmounts + } + + totalAmounts, exists := amountsMap[transaction.AccountId] - if transactionTotalAmount.Type == models.TRANSACTION_DB_TYPE_INCOME { - incomeAmounts[transactionTotalAmount.AccountId] = transactionTotalAmount.Amount - } else if transactionTotalAmount.Type == models.TRANSACTION_DB_TYPE_EXPENSE { - expenseAmounts[transactionTotalAmount.AccountId] = transactionTotalAmount.Amount + if !exists { + totalAmounts = 0 } + + totalAmounts += transaction.Amount + amountsMap[transaction.AccountId] = totalAmounts } return incomeAmounts, expenseAmounts, nil } // GetAccountsAndCategoriesTotalIncomeAndExpense returns the every accounts and categories total income and expense amount by specific date range -func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(c *core.Context, uid int64, startUnixTime int64, endUnixTime int64) ([]*models.Transaction, error) { +func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(c *core.Context, uid int64, startUnixTime int64, endUnixTime int64, utcOffset int16, useTransactionTimezone bool) ([]*models.Transaction, error) { if uid <= 0 { return nil, errs.ErrUserIdInvalid } + clientLocation := time.FixedZone("Client Timezone", int(utcOffset)*60) + startLocalDateTime := utils.FormatUnixTimeToNumericLocalDateTime(startUnixTime, clientLocation) + endLocalDateTime := utils.FormatUnixTimeToNumericLocalDateTime(endUnixTime, clientLocation) + + startUnixTime = utils.GetMinUnixTimeWithSameLocalDateTime(startUnixTime, utcOffset) + endUnixTime = utils.GetMaxUnixTimeWithSameLocalDateTime(endUnixTime, utcOffset) + + startTransactionTime := utils.GetMinTransactionTimeFromUnixTime(startUnixTime) + endTransactionTime := utils.GetMaxTransactionTimeFromUnixTime(endUnixTime) + condition := "uid=? AND deleted=? AND (type=? OR type=?)" - conditionParams := make([]any, 0, 8) + conditionParams := make([]any, 0, 4) conditionParams = append(conditionParams, uid) conditionParams = append(conditionParams, false) conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_INCOME) @@ -1050,19 +1119,82 @@ func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(c *co if startUnixTime > 0 { condition = condition + " AND transaction_time>=?" - conditionParams = append(conditionParams, utils.GetMinTransactionTimeFromUnixTime(startUnixTime)) } if endUnixTime > 0 { condition = condition + " AND transaction_time<=?" - conditionParams = append(conditionParams, utils.GetMaxTransactionTimeFromUnixTime(endUnixTime)) } - var transactionTotalAmounts []*models.Transaction - err := s.UserDataDB(uid).NewSession(c).Select("category_id, account_id, SUM(amount) as amount").Where(condition, conditionParams...).GroupBy("category_id, account_id").Find(&transactionTotalAmounts) + minTransactionTime := startTransactionTime + maxTransactionTime := endTransactionTime + var allTransactions []*models.Transaction - if err != nil { - return nil, err + for maxTransactionTime > 0 { + var transactions []*models.Transaction + + finalConditionParams := make([]any, 0, 6) + finalConditionParams = append(finalConditionParams, conditionParams...) + + if startUnixTime > 0 { + finalConditionParams = append(finalConditionParams, minTransactionTime) + } + + if endUnixTime > 0 { + finalConditionParams = append(finalConditionParams, maxTransactionTime) + } + + err := s.UserDataDB(uid).NewSession(c).Select("category_id, account_id, transaction_time, timezone_utc_offset, amount").Where(condition, finalConditionParams...).Limit(pageCountForLoadTransactionAmounts, 0).OrderBy("transaction_time desc").Find(&transactions) + + if err != nil { + return nil, err + } + + allTransactions = append(allTransactions, transactions...) + + if len(transactions) < pageCountForLoadTransactionAmounts { + maxTransactionTime = 0 + break + } + + maxTransactionTime = transactions[len(transactions)-1].TransactionTime - 1 + } + + transactionTotalAmountsMap := make(map[string]*models.Transaction) + + for i := 0; i < len(allTransactions); i++ { + transaction := allTransactions[i] + timeZone := clientLocation + + if useTransactionTimezone { + timeZone = time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60) + } + + localDateTime := utils.FormatUnixTimeToNumericLocalDateTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), timeZone) + + if localDateTime < startLocalDateTime || localDateTime > endLocalDateTime { + continue + } + + groupKey := fmt.Sprintf("%s_%s", transaction.CategoryId, transaction.AccountId) + totalAmounts, exists := transactionTotalAmountsMap[groupKey] + + if !exists { + totalAmounts = &models.Transaction{ + CategoryId: transaction.CategoryId, + AccountId: transaction.AccountId, + Amount: 0, + } + + transactionTotalAmountsMap[groupKey] = totalAmounts + } + + totalAmounts.Amount += transaction.Amount + } + + transactionTotalAmounts := make([]*models.Transaction, 0, len(transactionTotalAmountsMap)) + + for _, totalAmounts := range transactionTotalAmountsMap { + transactionTotalAmounts = append(transactionTotalAmounts, totalAmounts) } return transactionTotalAmounts, nil diff --git a/pkg/utils/datetimes.go b/pkg/utils/datetimes.go index 538ce587..40022d01 100644 --- a/pkg/utils/datetimes.go +++ b/pkg/utils/datetimes.go @@ -44,6 +44,34 @@ func FormatUnixTimeToYearMonth(unixTime int64, timezone *time.Location) string { return t.Format(yearMonthDateTimeFormat) } +// FormatUnixTimeToNumericLocalDateTime returns numeric year, month, day, hour, minute and second of specified unix time +func FormatUnixTimeToNumericLocalDateTime(unixTime int64, timezone *time.Location) int64 { + t := parseFromUnixTime(unixTime) + + if timezone != nil { + t = t.In(timezone) + } + + localDateTime := int64(t.Year()) + localDateTime = localDateTime*100 + int64(t.Month()) + localDateTime = localDateTime*100 + int64(t.Day()) + localDateTime = localDateTime*100 + int64(t.Hour()) + localDateTime = localDateTime*100 + int64(t.Minute()) + localDateTime = localDateTime*100 + int64(t.Second()) + + return localDateTime +} + +// GetMinUnixTimeWithSameLocalDateTime returns the minimum UnixTime for date with the same local date +func GetMinUnixTimeWithSameLocalDateTime(unixTime int64, currentUtcOffset int16) int64 { + return unixTime + int64(currentUtcOffset)*60 - easternmostTimezoneUtcOffset*60 +} + +// GetMaxUnixTimeWithSameLocalDateTime returns the maximum UnixTime for date with the same local date +func GetMaxUnixTimeWithSameLocalDateTime(unixTime int64, currentUtcOffset int16) int64 { + return unixTime + int64(currentUtcOffset)*60 - westernmostTimezoneUtcOffset*60 +} + // ParseFromLongDateTimeToMinUnixTime parses a formatted string in long date time format to minimal unix time (the westernmost timezone) func ParseFromLongDateTimeToMinUnixTime(t string) (time.Time, error) { timezone := time.FixedZone("Timezone", easternmostTimezoneUtcOffset*60) diff --git a/pkg/utils/datetimes_test.go b/pkg/utils/datetimes_test.go index f2af3c12..37ac4557 100644 --- a/pkg/utils/datetimes_test.go +++ b/pkg/utils/datetimes_test.go @@ -35,6 +35,38 @@ func TestFormatUnixTimeToYearMonth(t *testing.T) { assert.Equal(t, expectedValue, actualValue) } +func TestFormatUnixTimeToNumericLocalDateTime(t *testing.T) { + unixTime := int64(1617228083) + utcTimezone := time.FixedZone("Test Timezone", 0) // UTC + utc8Timezone := time.FixedZone("Test Timezone", 28800) // UTC+8 + + expectedValue := int64(20210331220123) + actualValue := FormatUnixTimeToNumericLocalDateTime(unixTime, utcTimezone) + assert.Equal(t, expectedValue, actualValue) + + expectedValue = int64(20210401060123) + actualValue = FormatUnixTimeToNumericLocalDateTime(unixTime, utc8Timezone) + assert.Equal(t, expectedValue, actualValue) +} + +func TestGetMinUnixTimeWithSameLocalDateTime(t *testing.T) { + expectedValue := int64(1690797600) + actualValue := GetMinUnixTimeWithSameLocalDateTime(1690819200, 480) + assert.Equal(t, expectedValue, actualValue) + + actualValue = GetMinUnixTimeWithSameLocalDateTime(1690873200, -420) + assert.Equal(t, expectedValue, actualValue) +} + +func TestGetMaxUnixTimeWithSameLocalDateTime(t *testing.T) { + expectedValue := int64(1690891200) + actualValue := GetMaxUnixTimeWithSameLocalDateTime(1690819200, 480) + assert.Equal(t, expectedValue, actualValue) + + actualValue = GetMaxUnixTimeWithSameLocalDateTime(1690873200, -420) + assert.Equal(t, expectedValue, actualValue) +} + func TestParseFromLongDateTimeToMinUnixTime(t *testing.T) { expectedValue := int64(1690797600) actualTime, err := ParseFromLongDateTimeToMinUnixTime("2023-08-01 00:00:00") diff --git a/src/consts/timezone.js b/src/consts/timezone.js index 9cd45f48..c2aeea19 100644 --- a/src/consts/timezone.js +++ b/src/consts/timezone.js @@ -592,7 +592,16 @@ const allAvailableTimezones = [ } ]; +const allTimezoneTypesUsedForStatistics = { + ApplicationTimezone: 0, + TransactionTimezone: 1 +}; + +const defaultTimezoneTypesUsedForStatistics = allTimezoneTypesUsedForStatistics.ApplicationTimezone; + export default { all: allAvailableTimezones, - utcTimezoneName: 'Etc/GMT' + utcTimezoneName: 'Etc/GMT', + allTimezoneTypesUsedForStatistics: allTimezoneTypesUsedForStatistics, + defaultTimezoneTypesUsedForStatistics: defaultTimezoneTypesUsedForStatistics }; diff --git a/src/lib/i18n.js b/src/lib/i18n.js index e532f723..a80c1b8f 100644 --- a/src/lib/i18n.js +++ b/src/lib/i18n.js @@ -762,6 +762,21 @@ function getDateRangeDisplayName(userStore, dateType, startTime, endTime, transl return `${displayStartTime} ~ ${displayEndTime}`; } +function getAllTimezoneTypesUsedForStatistics(currentTimezone, translateFn) { + const currentTimezoneOffset = getTimezoneOffset(currentTimezone); + + return [ + { + displayName: translateFn('Application Timezone') + ` (UTC${currentTimezoneOffset})`, + type: timezone.allTimezoneTypesUsedForStatistics.ApplicationTimezone + }, + { + displayName: translateFn('Transaction Timezone'), + type: timezone.allTimezoneTypesUsedForStatistics.TransactionTimezone + } + ]; +} + function getAllAccountCategories(translateFn) { const allAccountCategories = []; @@ -1333,6 +1348,7 @@ export function i18nFunctions(i18nGlobal) { getAllDateRanges: (includeCustom) => getAllDateRanges(includeCustom, i18nGlobal.t), getAllRecentMonthDateRanges: (userStore, includeAll, includeCustom) => getAllRecentMonthDateRanges(userStore, includeAll, includeCustom, i18nGlobal.t), getDateRangeDisplayName: (userStore, dateType, startTime, endTime) => getDateRangeDisplayName(userStore, dateType, startTime, endTime, i18nGlobal.t), + getAllTimezoneTypesUsedForStatistics: (currentTimezone) => getAllTimezoneTypesUsedForStatistics(currentTimezone, i18nGlobal.t), getAllAccountCategories: () => getAllAccountCategories(i18nGlobal.t), getAllAccountTypes: () => getAllAccountTypes(i18nGlobal.t), getAllStatisticsChartDataTypes: () => getAllStatisticsChartDataTypes(i18nGlobal.t), diff --git a/src/lib/services.js b/src/lib/services.js index 84b05015..1276408b 100644 --- a/src/lib/services.js +++ b/src/lib/services.js @@ -283,7 +283,7 @@ export default { keyword = encodeURIComponent(keyword); return axios.get(`v1/transactions/list/by_month.json?year=${year}&month=${month}&type=${type}&category_id=${categoryId}&account_id=${accountId}&keyword=${keyword}&trim_account=true&trim_category=true&trim_tag=true`); }, - getTransactionStatistics: ({ startTime, endTime }) => { + getTransactionStatistics: ({ startTime, endTime, useTransactionTimezone }) => { const queryParams = []; if (startTime) { @@ -294,9 +294,9 @@ export default { queryParams.push(`end_time=${endTime}`); } - return axios.get('v1/transactions/statistics.json' + (queryParams.length ? '?' + queryParams.join('&') : '')); + return axios.get(`v1/transactions/statistics.json?use_transaction_timezone=${useTransactionTimezone}` + (queryParams.length ? '&' + queryParams.join('&') : '')); }, - getTransactionAmounts: ({ today, thisWeek, thisMonth, thisYear, lastMonth, monthBeforeLastMonth, monthBeforeLast2Months, monthBeforeLast3Months, monthBeforeLast4Months, monthBeforeLast5Months, monthBeforeLast6Months, monthBeforeLast7Months, monthBeforeLast8Months, monthBeforeLast9Months, monthBeforeLast10Months }) => { + getTransactionAmounts: ({ useTransactionTimezone, today, thisWeek, thisMonth, thisYear, lastMonth, monthBeforeLastMonth, monthBeforeLast2Months, monthBeforeLast3Months, monthBeforeLast4Months, monthBeforeLast5Months, monthBeforeLast6Months, monthBeforeLast7Months, monthBeforeLast8Months, monthBeforeLast9Months, monthBeforeLast10Months }) => { const queryParams = []; if (today) { @@ -359,7 +359,7 @@ export default { queryParams.push(`monthBeforeLast10Months_${monthBeforeLast10Months.startTime}_${monthBeforeLast10Months.endTime}`); } - return axios.get('v1/transactions/amounts.json' + (queryParams.length ? '?query=' + queryParams.join('|') : '')); + return axios.get(`v1/transactions/amounts.json?use_transaction_timezone=${useTransactionTimezone}` + (queryParams.length ? '&query=' + queryParams.join('|') : '')); }, getTransaction: ({ id }) => { return axios.get(`v1/transactions/get.json?id=${id}&trim_account=true&trim_category=true&trim_tag=true`); diff --git a/src/lib/settings.js b/src/lib/settings.js index ae7d9094..ecd3ed4a 100644 --- a/src/lib/settings.js +++ b/src/lib/settings.js @@ -1,4 +1,5 @@ import currencyConstants from '@/consts/currency.js'; +import timezoneConstants from '@/consts/timezone.js'; import statisticsConstants from '@/consts/statistics.js'; const settingsLocalStorageKey = 'ebk_app_settings'; @@ -15,6 +16,7 @@ const defaultSettings = { thousandsSeparator: true, currencyDisplayMode: currencyConstants.defaultCurrencyDisplayMode, showAmountInHomePage: true, + timezoneUsedForStatisticsInHomePage: timezoneConstants.defaultTimezoneTypesUsedForStatistics, itemsCountInTransactionListPage: 15, showTotalAmountInTransactionListPage: true, showAccountBalance: true, @@ -22,6 +24,7 @@ const defaultSettings = { defaultChartType: statisticsConstants.defaultChartType, defaultChartDataType: statisticsConstants.defaultChartDataType, defaultDataRangeType: statisticsConstants.defaultDataRangeType, + defaultTimezoneType: timezoneConstants.defaultTimezoneTypesUsedForStatistics, defaultAccountFilter: {}, defaultTransactionCategoryFilter: {}, defaultSortingType: statisticsConstants.defaultSortingType @@ -186,6 +189,14 @@ export function setShowAmountInHomePage(value) { setOption('showAmountInHomePage', value); } +export function getTimezoneUsedForStatisticsInHomePage() { + return getOption('timezoneUsedForStatisticsInHomePage'); +} + +export function setTimezoneUsedForStatisticsInHomePage(value) { + setOption('timezoneUsedForStatisticsInHomePage', value); +} + export function getItemsCountInTransactionListPage() { return getOption('itemsCountInTransactionListPage'); } @@ -230,6 +241,14 @@ export function getStatisticsDefaultDateRange() { return getSubOption('statistics', 'defaultDataRangeType'); } +export function getStatisticsDefaultTimezoneType() { + return getSubOption('statistics', 'defaultTimezoneType'); +} + +export function setStatisticsDefaultTimezoneType(value) { + setSubOption('statistics', 'defaultTimezoneType', value); +} + export function setStatisticsDefaultDateRange(value) { setSubOption('statistics', 'defaultDataRangeType', value); } diff --git a/src/locales/en.js b/src/locales/en.js index 65e39ad2..54ac026a 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -1019,6 +1019,7 @@ export default { 'Default Transaction Category Filter': 'Default Transaction Category Filter', 'Sort Order': 'Sort Order', 'Default Sort Order': 'Default Sort Order', + 'Timezone Used for Date Range': 'Timezone Used for Date Range', 'Amount': 'Amount', 'Display Order': 'Display Order', 'Name': 'Name', @@ -1047,6 +1048,9 @@ export default { 'Hide Account Balance': 'Hide Account Balance', 'Page Settings': 'Page Settings', 'Overview Page': 'Overview Page', + 'Timezone Used for Statistics': 'Timezone Used for Statistics', + 'Timezone Type': 'Timezone Type', + 'Application Timezone': 'Application Timezone', 'Transaction List Page': 'Transaction List Page', 'Transactions Per Page': 'Transactions Per Page', 'Show Monthly Total Amount': 'Show Monthly Total Amount', diff --git a/src/locales/zh_Hans.js b/src/locales/zh_Hans.js index 1e4322d8..e8fecd7b 100644 --- a/src/locales/zh_Hans.js +++ b/src/locales/zh_Hans.js @@ -1019,6 +1019,7 @@ export default { 'Default Transaction Category Filter': '默认交易分类过滤', 'Sort Order': '排序方式', 'Default Sort Order': '默认排序方式', + 'Timezone Used for Date Range': '时间范围使用的时区', 'Amount': '金额', 'Display Order': '显示顺序', 'Name': '名称', @@ -1047,6 +1048,9 @@ export default { 'Hide Account Balance': '隐藏账户余额', 'Page Settings': '页面设置', 'Overview Page': '总览页面', + 'Timezone Used for Statistics': '统计时使用的时区', + 'Timezone Type': '时区类型', + 'Application Timezone': '应用时区', 'Transaction List Page': '交易列表页面', 'Transactions Per Page': '每页交易数', 'Show Monthly Total Amount': '显示月度总金额', diff --git a/src/stores/overview.js b/src/stores/overview.js index 9d4e186d..5f35676e 100644 --- a/src/stores/overview.js +++ b/src/stores/overview.js @@ -1,5 +1,6 @@ import { defineStore } from 'pinia'; +import { useSettingsStore } from '@/stores/setting.js'; import { useUserStore } from './user.js'; import { useExchangeRatesStore } from './exchangeRates.js'; @@ -219,6 +220,8 @@ export const useOverviewStore = defineStore('overview', { this.transactionOverviewStateInvalid = true; }, loadTransactionOverview({ force, loadLast11Months }) { + const settingsStore = useSettingsStore(); + const self = this; let dateChanged = false; let rangeChanged = false; @@ -239,6 +242,7 @@ export const useOverviewStore = defineStore('overview', { } const requestParams = { + useTransactionTimezone: settingsStore.appSettings.timezoneUsedForStatisticsInHomePage, today: self.transactionDataRange.today, thisWeek: self.transactionDataRange.thisWeek, thisMonth: self.transactionDataRange.thisMonth, diff --git a/src/stores/setting.js b/src/stores/setting.js index 5bf7147e..5439b88f 100644 --- a/src/stores/setting.js +++ b/src/stores/setting.js @@ -17,6 +17,7 @@ export const useSettingsStore = defineStore('settings', { thousandsSeparator: settings.isEnableThousandsSeparator(), currencyDisplayMode: settings.getCurrencyDisplayMode(), showAmountInHomePage: settings.isShowAmountInHomePage(), + timezoneUsedForStatisticsInHomePage: settings.getTimezoneUsedForStatisticsInHomePage(), itemsCountInTransactionListPage: settings.getItemsCountInTransactionListPage(), showTotalAmountInTransactionListPage: settings.isShowTotalAmountInTransactionListPage(), showAccountBalance: settings.isShowAccountBalance(), @@ -24,6 +25,7 @@ export const useSettingsStore = defineStore('settings', { defaultChartType: settings.getStatisticsDefaultChartType(), defaultChartDataType: settings.getStatisticsDefaultChartDataType(), defaultDataRangeType: settings.getStatisticsDefaultDateRange(), + defaultTimezoneType: settings.getStatisticsDefaultTimezoneType(), defaultAccountFilter: settings.getStatisticsDefaultAccountFilter(), defaultTransactionCategoryFilter: settings.getStatisticsDefaultTransactionCategoryFilter(), defaultSortingType: settings.getStatisticsSortingType() @@ -76,6 +78,10 @@ export const useSettingsStore = defineStore('settings', { settings.setShowAmountInHomePage(value); this.appSettings.showAmountInHomePage = value; }, + setTimezoneUsedForStatisticsInHomePage(value) { + settings.setTimezoneUsedForStatisticsInHomePage(value); + this.appSettings.timezoneUsedForStatisticsInHomePage = value; + }, setItemsCountInTransactionListPage(value) { settings.setItemsCountInTransactionListPage(value); this.appSettings.itemsCountInTransactionListPage = value; @@ -100,6 +106,10 @@ export const useSettingsStore = defineStore('settings', { settings.setStatisticsDefaultDateRange(value); this.appSettings.statistics.defaultDataRangeType = value; }, + setStatisticsDefaultTimezoneType(value) { + settings.setStatisticsDefaultTimezoneType(value); + this.appSettings.statistics.defaultTimezoneType = value; + }, setStatisticsDefaultAccountFilter(value) { settings.setStatisticsDefaultAccountFilter(value); this.appSettings.statistics.defaultAccountFilter = value; diff --git a/src/stores/statistics.js b/src/stores/statistics.js index b33f6bce..a6fa2214 100644 --- a/src/stores/statistics.js +++ b/src/stores/statistics.js @@ -580,11 +580,13 @@ export const useStatisticsStore = defineStore('statistics', { }, loadTransactionStatistics({ force }) { const self = this; + const settingsStore = useSettingsStore(); return new Promise((resolve, reject) => { services.getTransactionStatistics({ startTime: self.transactionStatisticsFilter.startTime, - endTime: self.transactionStatisticsFilter.endTime + endTime: self.transactionStatisticsFilter.endTime, + useTransactionTimezone: settingsStore.appSettings.statistics.defaultTimezoneType }).then(response => { const data = response.data; diff --git a/src/views/desktop/app/settings/tabs/AppBasicSettingTab.vue b/src/views/desktop/app/settings/tabs/AppBasicSettingTab.vue index 1b1d2cf8..cef8e5a9 100644 --- a/src/views/desktop/app/settings/tabs/AppBasicSettingTab.vue +++ b/src/views/desktop/app/settings/tabs/AppBasicSettingTab.vue @@ -109,6 +109,18 @@ v-model="showAmountInHomePage" /> + + + + @@ -197,6 +209,9 @@ export default { allCurrencyDisplayModes() { return currencyConstants.allCurrencyDisplayModes; }, + allTimezoneTypesUsedForStatistics() { + return this.$locale.getAllTimezoneTypesUsedForStatistics(this.timeZone); + }, theme: { get: function () { return this.settingsStore.appSettings.theme; @@ -265,6 +280,15 @@ export default { this.settingsStore.setShowAmountInHomePage(value); } }, + timezoneUsedForStatisticsInHomePage: { + get: function () { + return this.settingsStore.appSettings.timezoneUsedForStatisticsInHomePage; + }, + set: function (value) { + this.settingsStore.setTimezoneUsedForStatisticsInHomePage(value); + this.overviewStore.updateTransactionOverviewInvalidState(true); + } + }, showTotalAmountInTransactionListPage: { get: function () { return this.settingsStore.appSettings.showTotalAmountInTransactionListPage; diff --git a/src/views/desktop/app/settings/tabs/AppStatisticsSettingTab.vue b/src/views/desktop/app/settings/tabs/AppStatisticsSettingTab.vue index abd077d3..a954508e 100644 --- a/src/views/desktop/app/settings/tabs/AppStatisticsSettingTab.vue +++ b/src/views/desktop/app/settings/tabs/AppStatisticsSettingTab.vue @@ -44,6 +44,18 @@ /> + + + + {{ $t('Show Amount') }} + + + + {{ $t('Transaction List Page') }} @@ -31,10 +41,14 @@