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..22071f53 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 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 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 !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("%d_%d", 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 @@