diff --git a/cmd/webserver.go b/cmd/webserver.go index 2388d4e5..ccf40d25 100644 --- a/cmd/webserver.go +++ b/cmd/webserver.go @@ -385,6 +385,7 @@ func startWebServer(c *core.CliContext) error { 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)) + apiV1Route.GET("/transactions/statistics/asset_trends.json", bindApi(api.Transactions.TransactionStatisticsAssetTrendsHandler)) apiV1Route.GET("/transactions/amounts.json", bindApi(api.Transactions.TransactionAmountsHandler)) apiV1Route.GET("/transactions/get.json", bindApi(api.Transactions.TransactionGetHandler)) apiV1Route.POST("/transactions/add.json", bindApi(api.Transactions.TransactionCreateHandler)) diff --git a/pkg/api/transactions.go b/pkg/api/transactions.go index fa82fb73..aa74e87c 100644 --- a/pkg/api/transactions.go +++ b/pkg/api/transactions.go @@ -340,7 +340,7 @@ func (a *TransactionsApi) TransactionReconciliationStatementHandler(c *core.WebC minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(reconciliationStatementRequest.StartTime) } - transactionsWithAccountBalance, totalInflows, totalOutflows, openingBalance, closingBalance, err := a.transactions.GetAllTransactionsWithAccountBalanceByMaxTime(c, uid, pageCountForAccountStatement, maxTransactionTime, minTransactionTime, reconciliationStatementRequest.AccountId, account.Category) + transactionsWithAccountBalance, totalInflows, totalOutflows, openingBalance, closingBalance, err := a.transactions.GetAllTransactionsInOneAccountWithAccountBalanceByMaxTime(c, uid, pageCountForAccountStatement, maxTransactionTime, minTransactionTime, reconciliationStatementRequest.AccountId, account.Category) if err != nil { log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] failed to get transactions from \"%d\" to \"%d\" for user \"uid:%d\", because %s", reconciliationStatementRequest.StartTime, reconciliationStatementRequest.EndTime, uid, err.Error()) @@ -532,6 +532,71 @@ func (a *TransactionsApi) TransactionStatisticsTrendsHandler(c *core.WebContext) return statisticTrendsResp, nil } +// TransactionStatisticsAssetTrendsHandler returns transaction statistics asset trends of current user +func (a *TransactionsApi) TransactionStatisticsAssetTrendsHandler(c *core.WebContext) (any, *errs.Error) { + var statisticAssetTrendsReq models.TransactionStatisticAssetTrendsRequest + err := c.ShouldBindQuery(&statisticAssetTrendsReq) + + if err != nil { + log.Warnf(c, "[transactions.TransactionStatisticsAssetTrendsHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + utcOffset, err := c.GetClientTimezoneOffset() + + if err != nil { + log.Warnf(c, "[transactions.TransactionStatisticsAssetTrendsHandler] cannot get client timezone offset, because %s", err.Error()) + return nil, errs.ErrClientTimezoneOffsetInvalid + } + + uid := c.GetCurrentUid() + + maxTransactionTime := int64(0) + + if statisticAssetTrendsReq.EndTime > 0 { + maxTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(statisticAssetTrendsReq.EndTime) + } + + minTransactionTime := int64(0) + + if statisticAssetTrendsReq.StartTime > 0 { + minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(statisticAssetTrendsReq.StartTime) + } + + accountDailyBalances, err := a.transactions.GetAllAccountsDailyOpeningAndClosingBalance(c, uid, maxTransactionTime, minTransactionTime, utcOffset) + + if err != nil { + log.Errorf(c, "[transactions.TransactionStatisticsAssetTrendsHandler] failed to get transactions from \"%d\" to \"%d\" for user \"uid:%d\", because %s", statisticAssetTrendsReq.StartTime, statisticAssetTrendsReq.EndTime, uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + statisticAssetTrendsResp := make(models.TransactionStatisticAssetTrendsResponseItemSlice, 0) + + for yearMonthDay, dailyAccountBalances := range accountDailyBalances { + dailyStatisticResp := &models.TransactionStatisticAssetTrendsResponseItem{ + Year: yearMonthDay / 10000, + Month: (yearMonthDay % 10000) / 100, + Day: yearMonthDay % 100, + Items: make([]*models.TransactionStatisticAssetTrendsResponseDataItem, len(dailyAccountBalances)), + } + + for i := 0; i < len(dailyAccountBalances); i++ { + accountBalance := dailyAccountBalances[i] + dailyStatisticResp.Items[i] = &models.TransactionStatisticAssetTrendsResponseDataItem{ + AccountId: accountBalance.AccountId, + AccountOpeningBalance: accountBalance.AccountOpeningBalance, + AccountClosingBalance: accountBalance.AccountClosingBalance, + } + } + + statisticAssetTrendsResp = append(statisticAssetTrendsResp, dailyStatisticResp) + } + + sort.Sort(statisticAssetTrendsResp) + + return statisticAssetTrendsResp, nil +} + // TransactionAmountsHandler returns transaction amounts of current user func (a *TransactionsApi) TransactionAmountsHandler(c *core.WebContext) (any, *errs.Error) { var transactionAmountsReq models.TransactionAmountsRequest diff --git a/pkg/models/transaction.go b/pkg/models/transaction.go index 5409a853..9a47e672 100644 --- a/pkg/models/transaction.go +++ b/pkg/models/transaction.go @@ -275,6 +275,12 @@ type TransactionStatisticTrendsRequest struct { UseTransactionTimezone bool `form:"use_transaction_timezone"` } +// TransactionStatisticAssetTrendsRequest represents all parameters of transaction statistic asset trends request +type TransactionStatisticAssetTrendsRequest struct { + StartTime int64 `form:"start_time"` + EndTime int64 `form:"end_time"` +} + // TransactionAmountsRequest represents all parameters of transaction amounts request type TransactionAmountsRequest struct { Query string `form:"query"` @@ -403,6 +409,21 @@ type TransactionStatisticTrendsResponseItem struct { Items []*TransactionStatisticResponseItem `json:"items"` } +// TransactionStatisticAssetTrendsResponseItem represents the data within each statistic interval +type TransactionStatisticAssetTrendsResponseItem struct { + Year int32 `json:"year"` + Month int32 `json:"month"` + Day int32 `json:"day"` + Items []*TransactionStatisticAssetTrendsResponseDataItem `json:"items"` +} + +// TransactionStatisticAssetTrendsResponseDataItem represents an asset trends data item +type TransactionStatisticAssetTrendsResponseDataItem struct { + AccountId int64 `json:"accountId,string"` + AccountOpeningBalance int64 `json:"accountOpeningBalance"` + AccountClosingBalance int64 `json:"accountClosingBalance"` +} + // TransactionAmountsResponseItem represents an item of transaction amounts type TransactionAmountsResponseItem struct { StartTime int64 `json:"startTime"` @@ -600,6 +621,32 @@ func (s TransactionStatisticTrendsResponseItemSlice) Less(i, j int) bool { return s[i].Month < s[j].Month } +// TransactionStatisticAssetTrendsResponseItemSlice represents the slice data structure of TransactionStatisticAssetTrendsResponseItem +type TransactionStatisticAssetTrendsResponseItemSlice []*TransactionStatisticAssetTrendsResponseItem + +// Len returns the count of items +func (s TransactionStatisticAssetTrendsResponseItemSlice) Len() int { + return len(s) +} + +// Swap swaps two items +func (s TransactionStatisticAssetTrendsResponseItemSlice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Less reports whether the first item is less than the second one +func (s TransactionStatisticAssetTrendsResponseItemSlice) Less(i, j int) bool { + if s[i].Year != s[j].Year { + return s[i].Year < s[j].Year + } + + if s[i].Month != s[j].Month { + return s[i].Month < s[j].Month + } + + return s[i].Day < s[j].Day +} + // TransactionAmountsResponseItemAmountInfoSlice represents the slice data structure of TransactionAmountsResponseItemAmountInfo type TransactionAmountsResponseItemAmountInfoSlice []*TransactionAmountsResponseItemAmountInfo diff --git a/pkg/models/transaction_test.go b/pkg/models/transaction_test.go index 7b32c8ec..078908d7 100644 --- a/pkg/models/transaction_test.go +++ b/pkg/models/transaction_test.go @@ -164,6 +164,61 @@ func TestTransactionStatisticTrendsResponseItemSliceLess(t *testing.T) { assert.Equal(t, int32(9), transactionTrendsSlice[4].Month) } +func TestTransactionStatisticAssetTrendsResponseItemSliceLess(t *testing.T) { + var transactionTrendsSlice TransactionStatisticAssetTrendsResponseItemSlice + transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticAssetTrendsResponseItem{ + Year: 2024, + Month: 9, + Day: 1, + }) + transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticAssetTrendsResponseItem{ + Year: 2024, + Month: 9, + Day: 2, + }) + transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticAssetTrendsResponseItem{ + Year: 2024, + Month: 10, + Day: 1, + }) + transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticAssetTrendsResponseItem{ + Year: 2022, + Month: 10, + Day: 1, + }) + transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticAssetTrendsResponseItem{ + Year: 2023, + Month: 1, + Day: 1, + }) + transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticAssetTrendsResponseItem{ + Year: 2024, + Month: 2, + Day: 2, + }) + + sort.Sort(transactionTrendsSlice) + + assert.Equal(t, int32(2022), transactionTrendsSlice[0].Year) + assert.Equal(t, int32(10), transactionTrendsSlice[0].Month) + assert.Equal(t, int32(1), transactionTrendsSlice[0].Day) + assert.Equal(t, int32(2023), transactionTrendsSlice[1].Year) + assert.Equal(t, int32(1), transactionTrendsSlice[1].Month) + assert.Equal(t, int32(1), transactionTrendsSlice[1].Day) + assert.Equal(t, int32(2024), transactionTrendsSlice[2].Year) + assert.Equal(t, int32(2), transactionTrendsSlice[2].Month) + assert.Equal(t, int32(2), transactionTrendsSlice[2].Day) + assert.Equal(t, int32(2024), transactionTrendsSlice[3].Year) + assert.Equal(t, int32(9), transactionTrendsSlice[3].Month) + assert.Equal(t, int32(1), transactionTrendsSlice[3].Day) + assert.Equal(t, int32(2024), transactionTrendsSlice[4].Year) + assert.Equal(t, int32(9), transactionTrendsSlice[4].Month) + assert.Equal(t, int32(2), transactionTrendsSlice[4].Day) + assert.Equal(t, int32(2024), transactionTrendsSlice[5].Year) + assert.Equal(t, int32(10), transactionTrendsSlice[5].Month) + assert.Equal(t, int32(1), transactionTrendsSlice[5].Day) +} + func TestTransactionAmountsResponseItemAmountInfoSliceLess(t *testing.T) { var amountInfoSlice TransactionAmountsResponseItemAmountInfoSlice amountInfoSlice = append(amountInfoSlice, &TransactionAmountsResponseItemAmountInfo{ diff --git a/pkg/models/user_app_cloud_setting.go b/pkg/models/user_app_cloud_setting.go index 6698a6ac..9f6f44d6 100644 --- a/pkg/models/user_app_cloud_setting.go +++ b/pkg/models/user_app_cloud_setting.go @@ -43,6 +43,8 @@ var ALL_ALLOWED_CLOUD_SYNC_APP_SETTING_KEY_TYPES = map[string]UserApplicationClo "statistics.defaultCategoricalChartDataRangeType": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER, "statistics.defaultTrendChartType": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER, "statistics.defaultTrendChartDataRangeType": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER, + "statistics.defaultAssetTrendsChartType": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER, + "statistics.defaultAssetTrendsChartDataRangeType": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER, } // UserApplicationCloudSetting represents user application cloud setting stored in database diff --git a/pkg/services/transactions.go b/pkg/services/transactions.go index 1e70f711..fb8438e4 100644 --- a/pkg/services/transactions.go +++ b/pkg/services/transactions.go @@ -107,8 +107,8 @@ func (s *TransactionService) GetAllSpecifiedTransactions(c core.Context, uid int return allTransactions, nil } -// GetAllTransactionsWithAccountBalanceByMaxTime returns account statement within time range -func (s *TransactionService) GetAllTransactionsWithAccountBalanceByMaxTime(c core.Context, uid int64, pageCount int32, maxTransactionTime int64, minTransactionTime int64, accountId int64, accountCategory models.AccountCategory) ([]*models.TransactionWithAccountBalance, int64, int64, int64, int64, error) { +// GetAllTransactionsInOneAccountWithAccountBalanceByMaxTime returns account statement within time range +func (s *TransactionService) GetAllTransactionsInOneAccountWithAccountBalanceByMaxTime(c core.Context, uid int64, pageCount int32, maxTransactionTime int64, minTransactionTime int64, accountId int64, accountCategory models.AccountCategory) ([]*models.TransactionWithAccountBalance, int64, int64, int64, int64, error) { if maxTransactionTime <= 0 { maxTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(time.Now().Unix()) } @@ -158,7 +158,7 @@ func (s *TransactionService) GetAllTransactionsWithAccountBalanceByMaxTime(c cor } else if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN { accumulatedBalance = accumulatedBalance + transaction.Amount } else { - log.Errorf(c, "[transactions.GetAllTransactionsWithAccountBalanceByMaxTime] trasaction type (%d) is invalid (id:%d)", transaction.TransactionId, transaction.Type) + log.Errorf(c, "[transactions.GetAllTransactionsInOneAccountWithAccountBalanceByMaxTime] trasaction type (%d) is invalid (id:%d)", transaction.TransactionId, transaction.Type) return nil, 0, 0, 0, 0, errs.ErrTransactionTypeInvalid } @@ -197,6 +197,132 @@ func (s *TransactionService) GetAllTransactionsWithAccountBalanceByMaxTime(c cor return allTransactionsAndAccountBalance, totalInflows, totalOutflows, openingBalance, accumulatedBalance, nil } +// GetAllAccountsDailyOpeningAndClosingBalance returns daily opening and closing balance of all accounts within time range +func (s *TransactionService) GetAllAccountsDailyOpeningAndClosingBalance(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, utcOffset int16) (map[int32][]*models.TransactionWithAccountBalance, error) { + if maxTransactionTime <= 0 { + maxTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(time.Now().Unix()) + } + + clientLocation := time.FixedZone("Client Timezone", int(utcOffset)*60) + var allTransactions []*models.Transaction + + for maxTransactionTime > 0 { + transactions, err := s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, nil, nil, false, models.TRANSACTION_TAG_FILTER_HAS_ANY, "", "", 1, pageCountForLoadTransactionAmounts, false, false) + + if err != nil { + return nil, err + } + + allTransactions = append(allTransactions, transactions...) + + if len(transactions) < pageCountForLoadTransactionAmounts { + maxTransactionTime = 0 + break + } + + maxTransactionTime = transactions[len(transactions)-1].TransactionTime - 1 + } + + accountDailyLastBalances := make(map[string]*models.TransactionWithAccountBalance) + accountDailyBalances := make(map[int32][]*models.TransactionWithAccountBalance) + + if len(allTransactions) < 1 { + return accountDailyBalances, nil + } + + accumulatedBalances := make(map[int64]int64) + accumulatedBalancesBeforeStartTime := make(map[int64]int64) + + for i := len(allTransactions) - 1; i >= 0; i-- { + transaction := allTransactions[i] + accumulatedBalance := accumulatedBalances[transaction.AccountId] + lastAccumulatedBalance := accumulatedBalances[transaction.AccountId] + + if transaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE { + accumulatedBalance = accumulatedBalance + transaction.RelatedAccountAmount + } else if transaction.Type == models.TRANSACTION_DB_TYPE_INCOME { + accumulatedBalance = accumulatedBalance + transaction.Amount + } else if transaction.Type == models.TRANSACTION_DB_TYPE_EXPENSE { + accumulatedBalance = accumulatedBalance - transaction.Amount + } else if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT { + accumulatedBalance = accumulatedBalance - transaction.Amount + } else if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN { + accumulatedBalance = accumulatedBalance + transaction.Amount + } else { + log.Errorf(c, "[transactions.GetAllTransactionsWithAccountBalanceByMaxTime] trasaction type (%d) is invalid (id:%d)", transaction.TransactionId, transaction.Type) + return nil, errs.ErrTransactionTypeInvalid + } + + accumulatedBalances[transaction.AccountId] = accumulatedBalance + + if transaction.TransactionTime < minTransactionTime { + accumulatedBalancesBeforeStartTime[transaction.AccountId] = accumulatedBalance + continue + } + + yearMonthDay := utils.FormatUnixTimeToNumericYearMonthDay(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), clientLocation) + groupKey := fmt.Sprintf("%d_%d", yearMonthDay, transaction.AccountId) + dailyAccountBalance, exists := accountDailyLastBalances[groupKey] + + if exists { + dailyAccountBalance.AccountClosingBalance = accumulatedBalance + } else { + dailyAccountBalance = &models.TransactionWithAccountBalance{ + Transaction: &models.Transaction{ + AccountId: transaction.AccountId, + }, + AccountOpeningBalance: lastAccumulatedBalance, + AccountClosingBalance: accumulatedBalance, + } + accountDailyLastBalances[groupKey] = dailyAccountBalance + } + } + + firstTransactionTime := allTransactions[len(allTransactions)-1].TransactionTime + + if minTransactionTime > firstTransactionTime { + firstTransactionTime = minTransactionTime + } + + firstYearMonthDay := utils.FormatUnixTimeToNumericYearMonthDay(utils.GetUnixTimeFromTransactionTime(firstTransactionTime), clientLocation) + + // fill in the opening balance for accounts that do not have transactions on the first day + for accountId, accumulatedBalance := range accumulatedBalancesBeforeStartTime { + if accumulatedBalance == 0 { + continue + } + + groupKey := fmt.Sprintf("%d_%d", firstYearMonthDay, accountId) + + if _, exists := accountDailyLastBalances[groupKey]; exists { + continue + } + + accountDailyLastBalances[groupKey] = &models.TransactionWithAccountBalance{ + Transaction: &models.Transaction{ + AccountId: accountId, + }, + AccountOpeningBalance: accumulatedBalance, + AccountClosingBalance: accumulatedBalance, + } + } + + for groupKey, transactionWithAccountBalance := range accountDailyLastBalances { + groupKeyParts := strings.Split(groupKey, "_") + yearMonthDay, _ := utils.StringToInt32(groupKeyParts[0]) + dailyAccountBalances, exists := accountDailyBalances[yearMonthDay] + + if !exists { + dailyAccountBalances = make([]*models.TransactionWithAccountBalance, 0) + } + + dailyAccountBalances = append(dailyAccountBalances, transactionWithAccountBalance) + accountDailyBalances[yearMonthDay] = dailyAccountBalances + } + + return accountDailyBalances, nil +} + // GetTransactionsByMaxTime returns transactions before given time func (s *TransactionService) GetTransactionsByMaxTime(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagIds []int64, noTags bool, tagFilterType models.TransactionTagFilterType, amountFilter string, keyword string, page int32, count int32, needOneMoreItem bool, noDuplicated bool) ([]*models.Transaction, error) { if uid <= 0 { diff --git a/pkg/utils/datetimes.go b/pkg/utils/datetimes.go index 960bb2ce..d64b6e58 100644 --- a/pkg/utils/datetimes.go +++ b/pkg/utils/datetimes.go @@ -155,6 +155,17 @@ func FormatUnixTimeToNumericYearMonth(unixTime int64, timezone *time.Location) i return int32(t.Year())*100 + int32(t.Month()) } +// FormatUnixTimeToNumericYearMonthDay returns numeric year, month and day of specified unix time +func FormatUnixTimeToNumericYearMonthDay(unixTime int64, timezone *time.Location) int32 { + t := parseFromUnixTime(unixTime) + + if timezone != nil { + t = t.In(timezone) + } + + return int32(t.Year())*10000 + int32(t.Month())*100 + int32(t.Day()) +} + // 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) diff --git a/pkg/utils/datetimes_test.go b/pkg/utils/datetimes_test.go index e1dda405..3f5d4d4c 100644 --- a/pkg/utils/datetimes_test.go +++ b/pkg/utils/datetimes_test.go @@ -133,6 +133,20 @@ func TestFormatUnixTimeToNumericYearMonth(t *testing.T) { assert.Equal(t, expectedValue, actualValue) } +func TestFormatUnixTimeToNumericYearMonthDay(t *testing.T) { + unixTime := int64(1617228083) + utcTimezone := time.FixedZone("Test Timezone", 0) // UTC + utc8Timezone := time.FixedZone("Test Timezone", 28800) // UTC+8 + + expectedValue := int32(20210331) + actualValue := FormatUnixTimeToNumericYearMonthDay(unixTime, utcTimezone) + assert.Equal(t, expectedValue, actualValue) + + expectedValue = int32(20210401) + actualValue = FormatUnixTimeToNumericYearMonthDay(unixTime, utc8Timezone) + assert.Equal(t, expectedValue, actualValue) +} + func TestFormatUnixTimeToNumericLocalDateTime(t *testing.T) { unixTime := int64(1617228083) utcTimezone := time.FixedZone("Test Timezone", 0) // UTC diff --git a/src/components/base/AccountBalanceTrendsChartBase.ts b/src/components/base/AccountBalanceTrendsChartBase.ts index e0ded109..87b7c44c 100644 --- a/src/components/base/AccountBalanceTrendsChartBase.ts +++ b/src/components/base/AccountBalanceTrendsChartBase.ts @@ -14,7 +14,7 @@ import { ChartDateAggregationType } from '@/core/statistics.ts'; import type { AccountInfoResponse } from '@/models/account.ts'; import type { TransactionReconciliationStatementResponseItem } from '@/models/transaction.ts'; -import { isDefined, isArray } from '@/lib/common.ts'; +import { isArray } from '@/lib/common.ts'; import { sumAmounts } from '@/lib/numeral.ts'; import { getGregorianCalendarYearAndMonthFromUnixTime, @@ -45,7 +45,7 @@ export interface AccountBalanceTrendsChartItem { export interface CommonAccountBalanceTrendsChartProps { items: TransactionReconciliationStatementResponseItem[] | undefined; - dateAggregationType?: number; + dateAggregationType: number; fiscalYearStart: number; account: AccountInfoResponse; } @@ -100,7 +100,7 @@ export function useAccountBalanceTrendsChartBase(props: CommonAccountBalanceTren return []; } - if (!isDefined(props.dateAggregationType)) { + if (props.dateAggregationType === ChartDateAggregationType.Day.type) { return getAllDaysStartAndEndUnixTimes(dataDateRange.value.minUnixTime, dataDateRange.value.maxUnixTime); } else { const startYearMonth = getGregorianCalendarYearAndMonthFromUnixTime(dataDateRange.value.minUnixTime); @@ -129,8 +129,10 @@ export function useAccountBalanceTrendsChartBase(props: CommonAccountBalanceTren dateRangeMinUnixTime = getQuarterFirstUnixTimeBySpecifiedUnixTime(dateItem.time); } else if (props.dateAggregationType === ChartDateAggregationType.Month.type) { dateRangeMinUnixTime = getMonthFirstUnixTimeBySpecifiedUnixTime(dateItem.time); - } else { + } else if (props.dateAggregationType === ChartDateAggregationType.Day.type) { dateRangeMinUnixTime = getDayFirstUnixTimeBySpecifiedUnixTime(dateItem.time); + } else { + return ret; } const dataItems: TransactionReconciliationStatementResponseItem[] = dayDataItemsMap[dateRangeMinUnixTime] || []; @@ -159,8 +161,10 @@ export function useAccountBalanceTrendsChartBase(props: CommonAccountBalanceTren displayDate = formatUnixTimeToGregorianLikeYearQuarter(dateRange.minUnixTime); } else if (props.dateAggregationType === ChartDateAggregationType.Month.type) { displayDate = formatUnixTimeToGregorianLikeShortYearMonth(dateRange.minUnixTime); - } else { + } else if (props.dateAggregationType === ChartDateAggregationType.Day.type) { displayDate = formatUnixTimeToShortDate(dateRange.minUnixTime); + } else { + return ret; } if (isArray(dataItems)) { diff --git a/src/components/base/TrendsChartBase.ts b/src/components/base/TrendsChartBase.ts new file mode 100644 index 00000000..5aa459be --- /dev/null +++ b/src/components/base/TrendsChartBase.ts @@ -0,0 +1,136 @@ +import { computed } from 'vue'; + +import { useI18n } from '@/locales/helpers.ts'; + +import type { + TextualYearMonth, + Year1BasedMonth, + YearMonthDay, + TimeRangeAndDateType, + YearUnixTime, + YearQuarterUnixTime, + YearMonthUnixTime, + YearMonthDayUnixTime +} from '@/core/datetime.ts'; +import type { FiscalYearUnixTime } from '@/core/fiscalyear.ts'; +import { ChartDataAggregationType, ChartDateAggregationType } from '@/core/statistics.ts'; +import type { YearMonthItems, YearMonthDayItems } from '@/models/transaction.ts'; + +import { + getYearMonthDayDateTime, + getGregorianCalendarYearAndMonthFromUnixTime, + getAllDaysStartAndEndUnixTimes +} from '@/lib/datetime.ts'; +import { + getAllDateRangesFromItems, + getAllDateRangesByYearMonthRange +} from '@/lib/statistics.ts'; + +export type TrendsChartDateType = 'daily' | 'monthly'; + +interface TrendsChartTypes { + daily: { + ItemsType: YearMonthDayItems; + DateTimeRangeType: number; + MonthRangeType: undefined; + }; + monthly: { + ItemsType: YearMonthItems; + DateTimeRangeType: undefined; + MonthRangeType: TextualYearMonth | ''; + }; +} + +export interface CommonTrendsChartProps { + chartMode: T; + items: TrendsChartTypes[T]['ItemsType'][]; + stacked?: boolean; + startTime: TrendsChartTypes[T]['DateTimeRangeType']; + endTime: TrendsChartTypes[T]['DateTimeRangeType']; + startYearMonth: TrendsChartTypes[T]['MonthRangeType']; + endYearMonth: TrendsChartTypes[T]['MonthRangeType']; + fiscalYearStart: number; + sortingType: number; + dataAggregationType: ChartDataAggregationType; + dateAggregationType: number; + idField?: string; + nameField: string; + valueField: string; + colorField?: string; + hiddenField?: string; + displayOrdersField?: string; + translateName?: boolean; + defaultCurrency?: string; + enableClickItem?: boolean; +} + +export interface TrendsBarChartClickEvent { + itemId: string; + dateRange: TimeRangeAndDateType; +} + +function buildDailyAllDateRanges(props: CommonTrendsChartProps<'daily'>): YearUnixTime[] | FiscalYearUnixTime[] | YearQuarterUnixTime[] | YearMonthUnixTime[] | YearMonthDayUnixTime[] { + let startTime: number = props.startTime; + let endTime: number = props.endTime; + + if ((!startTime || !endTime) && props.items && props.items.length) { + let minUnixTime = Number.MAX_SAFE_INTEGER, maxUnixTime = 0; + + for (const accountItem of props.items) { + for (const dataItem of accountItem.items) { + const dateTime = getYearMonthDayDateTime(dataItem.year, dataItem.month, dataItem.day); + const unixTime = dateTime.getUnixTime(); + + if (unixTime < minUnixTime) { + minUnixTime = unixTime; + } + + if (unixTime > maxUnixTime) { + maxUnixTime = unixTime; + } + } + } + + if (minUnixTime < Number.MAX_SAFE_INTEGER && maxUnixTime > 0) { + startTime = minUnixTime; + endTime = maxUnixTime; + } + } + + if (props.dateAggregationType === ChartDateAggregationType.Day.type) { + return getAllDaysStartAndEndUnixTimes(startTime, endTime); + } else { + const startYearMonth = getGregorianCalendarYearAndMonthFromUnixTime(startTime); + const endYearMonth = getGregorianCalendarYearAndMonthFromUnixTime(endTime); + return getAllDateRangesByYearMonthRange(startYearMonth, endYearMonth, props.fiscalYearStart, props.dateAggregationType); + } +} + +function buildMonthlyAllDateRanges(props: CommonTrendsChartProps<'monthly'>): YearUnixTime[] | FiscalYearUnixTime[] | YearQuarterUnixTime[] | YearMonthUnixTime[] { + return getAllDateRangesFromItems(props.items, props.startYearMonth, props.endYearMonth, props.fiscalYearStart, props.dateAggregationType); +} + +export function useTrendsChartBase(props: CommonTrendsChartProps) { + const { tt } = useI18n(); + + const allDateRanges = computed(() => { + if (props.chartMode === 'daily') { + return buildDailyAllDateRanges(props as CommonTrendsChartProps<'daily'>); + } else if (props.chartMode === 'monthly') { + return buildMonthlyAllDateRanges(props as CommonTrendsChartProps<'monthly'>); + } else { + return []; + } + }); + + function getItemName(name: string): string { + return props.translateName ? tt(name) : name; + } + + return { + // computed states + allDateRanges, + // functions + getItemName + }; +} diff --git a/src/components/desktop/PieChart.vue b/src/components/desktop/PieChart.vue index c64f0b5e..bd88b0d8 100644 --- a/src/components/desktop/PieChart.vue +++ b/src/components/desktop/PieChart.vue @@ -263,7 +263,7 @@ function onLegendSelectChanged(e: { selected: Record }): void { @media (min-width: 600px) { .pie-chart-container { - height: 610px; + height: 650px; } } diff --git a/src/components/desktop/RadarChart.vue b/src/components/desktop/RadarChart.vue index bbd7a39f..05322605 100644 --- a/src/components/desktop/RadarChart.vue +++ b/src/components/desktop/RadarChart.vue @@ -193,7 +193,7 @@ const chartOptions = computed(() => { @media (min-width: 600px) { .radar-chart-container { - height: 610px; + height: 650px; } } diff --git a/src/components/desktop/MonthlyTrendsChart.vue b/src/components/desktop/TrendsChart.vue similarity index 74% rename from src/components/desktop/MonthlyTrendsChart.vue rename to src/components/desktop/TrendsChart.vue index 52ea4c18..1f2bad3c 100644 --- a/src/components/desktop/MonthlyTrendsChart.vue +++ b/src/components/desktop/TrendsChart.vue @@ -1,5 +1,5 @@ @@ -10,20 +10,33 @@ import type { ECElementEvent } from 'echarts/core'; import type { CallbackDataParams } from 'echarts/types/dist/shared'; import { useI18n } from '@/locales/helpers.ts'; -import { type CommonMonthlyTrendsChartProps, type MonthlyTrendsBarChartClickEvent, useMonthlyTrendsChartBase } from '@/components/base/MonthlyTrendsChartBase.ts' +import { + type TrendsChartDateType, + type CommonTrendsChartProps, + type TrendsBarChartClickEvent, + useTrendsChartBase +} from '@/components/base/TrendsChartBase.ts' import { useUserStore } from '@/stores/user.ts'; import { itemAndIndex } from '@/core/base.ts'; import { TextDirection } from '@/core/text.ts'; -import { type Year1BasedMonth, DateRangeScene } from '@/core/datetime.ts'; +import { + type Year1BasedMonth, + type YearMonthDay, + DateRangeScene +} from '@/core/datetime.ts'; import type { ColorStyleValue } from '@/core/color.ts'; import { ThemeType } from '@/core/theme.ts'; -import { TrendChartType, ChartDateAggregationType } from '@/core/statistics.ts'; +import { + ChartDataAggregationType, + TrendChartType, + ChartDateAggregationType +} from '@/core/statistics.ts'; import { DEFAULT_CHART_COLORS } from '@/consts/color.ts'; -import type { YearMonthDataItem, SortableTransactionStatisticDataItem } from '@/models/transaction.ts'; +import type { SortableTransactionStatisticDataItem } from '@/models/transaction.ts'; import { isArray, @@ -42,14 +55,14 @@ import { sortStatisticsItems } from '@/lib/statistics.ts'; -interface DesktopMonthlyTrendsChartProps extends CommonMonthlyTrendsChartProps { +interface DesktopTrendsChartProps extends CommonTrendsChartProps { skeleton?: boolean; type?: number; showValue?: boolean; showTotalAmountInTooltip?: boolean; } -interface MonthlyTrendsChartDataItem { +interface TrendsChartDataItem { id: string; name: string; itemStyle: { @@ -64,17 +77,17 @@ interface MonthlyTrendsChartDataItem { data: number[]; } -interface MonthlyTrendsChartTooltipItem extends SortableTransactionStatisticDataItem { +interface TrendsChartTooltipItem extends SortableTransactionStatisticDataItem { readonly name: string; readonly color: unknown; readonly displayOrders: number[]; readonly totalAmount: number; } -const props = defineProps>(); +const props = defineProps>(); const emit = defineEmits<{ - (e: 'click', value: MonthlyTrendsBarChartClickEvent): void; + (e: 'click', value: TrendsBarChartClickEvent): void; }>(); const theme = useTheme(); @@ -82,6 +95,7 @@ const theme = useTheme(); const { tt, getCurrentLanguageTextDirection, + formatUnixTimeToShortDate, formatUnixTimeToGregorianLikeShortYear, formatUnixTimeToGregorianLikeShortYearMonth, formatYearQuarterToGregorianLikeYearQuarter, @@ -90,7 +104,7 @@ const { formatAmountToLocalizedNumeralsWithCurrency } = useI18n(); -const { allDateRanges, getItemName } = useMonthlyTrendsChartBase(props); +const { allDateRanges, getItemName } = useTrendsChartBase(props); const userStore = useUserStore(); @@ -143,16 +157,18 @@ const allDisplayDateRanges = computed(() => { allDisplayDateRanges.push(formatUnixTimeToGregorianLikeFiscalYear(dateRange.minUnixTime)); } else if (props.dateAggregationType === ChartDateAggregationType.Quarter.type && 'quarter' in dateRange) { allDisplayDateRanges.push(formatYearQuarterToGregorianLikeYearQuarter(dateRange.year, dateRange.quarter)); - } else { // if (props.dateAggregationType === ChartDateAggregationType.Month.type) { + } else if (props.dateAggregationType === ChartDateAggregationType.Month.type) { allDisplayDateRanges.push(formatUnixTimeToGregorianLikeShortYearMonth(dateRange.minUnixTime)); + } else if (props.dateAggregationType === ChartDateAggregationType.Day.type && props.chartMode === 'daily') { + allDisplayDateRanges.push(formatUnixTimeToShortDate(dateRange.minUnixTime)); } } return allDisplayDateRanges; }); -const allSeries = computed(() => { - const allSeries: MonthlyTrendsChartDataItem[] = []; +const allSeries = computed(() => { + const allSeries: TrendsChartDataItem[] = []; let maxAmount: number = 0; for (const [item, index] of itemAndIndex(props.items)) { @@ -161,23 +177,41 @@ const allSeries = computed(() => { } const allAmounts: number[] = []; - const dateRangeAmountMap: Record = {}; + const dateRangeAmountMap: Record = {}; for (const dataItem of item.items) { let dateRangeKey = ''; - if (props.dateAggregationType === ChartDateAggregationType.Year.type) { - dateRangeKey = dataItem.year.toString(); - } else if (props.dateAggregationType === ChartDateAggregationType.FiscalYear.type) { - const fiscalYear = getFiscalYearFromUnixTime( - getYearMonthFirstUnixTime({ year: dataItem.year, month1base: dataItem.month1base }), - props.fiscalYearStart - ); - dateRangeKey = fiscalYear.toString(); - } else if (props.dateAggregationType === ChartDateAggregationType.Quarter.type) { - dateRangeKey = `${dataItem.year}-${Math.floor((dataItem.month1base - 1) / 3) + 1}`; - } else { // if (props.dateAggregationType === ChartDateAggregationType.Month.type) { - dateRangeKey = `${dataItem.year}-${dataItem.month1base}`; + if (props.chartMode === 'daily' && 'month' in dataItem) { + if (props.dateAggregationType === ChartDateAggregationType.Year.type) { + dateRangeKey = dataItem.year.toString(); + } else if (props.dateAggregationType === ChartDateAggregationType.FiscalYear.type) { + const fiscalYear = getFiscalYearFromUnixTime( + getYearMonthFirstUnixTime({ year: dataItem.year, month1base: dataItem.month }), + props.fiscalYearStart + ); + dateRangeKey = fiscalYear.toString(); + } else if (props.dateAggregationType === ChartDateAggregationType.Quarter.type) { + dateRangeKey = `${dataItem.year}-${Math.floor((dataItem.month - 1) / 3) + 1}`; + } else if (props.dateAggregationType === ChartDateAggregationType.Month.type) { + dateRangeKey = `${dataItem.year}-${dataItem.month}`; + } else { // if (props.dateAggregationType === ChartDateAggregationType.Day.type) { + dateRangeKey = `${dataItem.year}-${dataItem.month}-${dataItem.day}`; + } + } else if (props.chartMode === 'monthly' && 'month1base' in dataItem) { + if (props.dateAggregationType === ChartDateAggregationType.Year.type) { + dateRangeKey = dataItem.year.toString(); + } else if (props.dateAggregationType === ChartDateAggregationType.FiscalYear.type) { + const fiscalYear = getFiscalYearFromUnixTime( + getYearMonthFirstUnixTime({ year: dataItem.year, month1base: dataItem.month1base }), + props.fiscalYearStart + ); + dateRangeKey = fiscalYear.toString(); + } else if (props.dateAggregationType === ChartDateAggregationType.Quarter.type) { + dateRangeKey = `${dataItem.year}-${Math.floor((dataItem.month1base - 1) / 3) + 1}`; + } else { // if (props.dateAggregationType === ChartDateAggregationType.Month.type) { + dateRangeKey = `${dataItem.year}-${dataItem.month1base}`; + } } const dataItems = dateRangeAmountMap[dateRangeKey] || []; @@ -197,6 +231,8 @@ const allSeries = computed(() => { dateRangeKey = `${dateRange.year}-${dateRange.quarter}`; } else if (props.dateAggregationType === ChartDateAggregationType.Month.type && 'month0base' in dateRange) { dateRangeKey = `${dateRange.year}-${dateRange.month0base + 1}`; + } else if (props.dateAggregationType === ChartDateAggregationType.Day.type && 'day' in dateRange && props.chartMode === 'daily') { + dateRangeKey = `${dateRange.year}-${dateRange.month}-${dateRange.day}`; } let amount = 0; @@ -204,8 +240,14 @@ const allSeries = computed(() => { if (isArray(dataItems)) { for (const dataItem of dataItems) { - if (isNumber(dataItem[props.valueField])) { - amount += dataItem[props.valueField] as number; + const value = (dataItem as unknown as Record)[props.valueField]; + + if (isNumber(value)) { + if (props.dataAggregationType === ChartDataAggregationType.Sum) { + amount += value; + } else if (props.dataAggregationType === ChartDataAggregationType.Last) { + amount = value; + } } } } @@ -217,7 +259,7 @@ const allSeries = computed(() => { allAmounts.push(amount); } - const finalItem: MonthlyTrendsChartDataItem = { + const finalItem: TrendsChartDataItem = { id: (props.idField && item[props.idField]) ? item[props.idField] as string : getItemName(item[props.nameField] as string), name: (props.idField && item[props.idField]) ? item[props.idField] as string : getItemName(item[props.nameField] as string), itemStyle: { @@ -317,7 +359,7 @@ const chartOptions = computed(() => { let tooltip = ''; let totalAmount = 0; let actualDisplayItemCount = 0; - const displayItems: MonthlyTrendsChartTooltipItem[] = []; + const displayItems: TrendsChartTooltipItem[] = []; for (const param of params) { const id = param.seriesId as string; @@ -435,19 +477,33 @@ function clickItem(e: ECElementEvent): void { let minUnixTime = dateRange.minUnixTime; let maxUnixTime = dateRange.maxUnixTime; - if (props.startYearMonth) { - const startMinUnixTime = getYearMonthFirstUnixTime(props.startYearMonth); - - if (startMinUnixTime > minUnixTime) { - minUnixTime = startMinUnixTime; + if (props.chartMode === 'daily') { + if (props.startTime) { + if (props.startTime > minUnixTime) { + minUnixTime = props.startTime; + } } - } - if (props.endYearMonth) { - const endMaxUnixTime = getYearMonthLastUnixTime(props.endYearMonth); + if (props.endTime) { + if (props.endTime < maxUnixTime) { + maxUnixTime = props.endTime; + } + } + } else if (props.chartMode === 'monthly') { + if (props.startYearMonth) { + const startMinUnixTime = getYearMonthFirstUnixTime(props.startYearMonth); - if (endMaxUnixTime < maxUnixTime) { - maxUnixTime = endMaxUnixTime; + if (startMinUnixTime > minUnixTime) { + minUnixTime = startMinUnixTime; + } + } + + if (props.endYearMonth) { + const endMaxUnixTime = getYearMonthLastUnixTime(props.endYearMonth); + + if (endMaxUnixTime < maxUnixTime) { + maxUnixTime = endMaxUnixTime; + } } } @@ -498,15 +554,15 @@ defineExpose({ diff --git a/src/components/mobile/MonthlyTrendsBarChart.vue b/src/components/mobile/TrendsBarChart.vue similarity index 60% rename from src/components/mobile/MonthlyTrendsBarChart.vue rename to src/components/mobile/TrendsBarChart.vue index 5816a996..561ec30e 100644 --- a/src/components/mobile/MonthlyTrendsBarChart.vue +++ b/src/components/mobile/TrendsBarChart.vue @@ -30,23 +30,31 @@ - +
-
+
+ + diff --git a/src/core/datetime.ts b/src/core/datetime.ts index 2295bd75..5ca8fef8 100644 --- a/src/core/datetime.ts +++ b/src/core/datetime.ts @@ -28,6 +28,7 @@ export interface DateTime { getSecond(): number; getDisplayAMPM(options: DateTimeFormatOptions): string; getTimezoneUtcOffsetMinutes(): number; + getDateTimeAfterDays(day: number): DateTime; toGregorianCalendarYearMonthDay(): YearMonthDay; toGregorianCalendarYear0BasedMonth(): Year0BasedMonth; format(format: string, options: DateTimeFormatOptions): string; @@ -584,7 +585,8 @@ export class ShortTimeFormat implements TimeFormat { export enum DateRangeScene { Normal = 0, - TrendAnalysis = 1 + TrendAnalysis = 1, + AssetTrends = 2 } export class DateRange implements TypeAndName { @@ -592,38 +594,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); + public static readonly All = new DateRange(0, 'All', false, false, DateRangeScene.Normal, DateRangeScene.TrendAnalysis, DateRangeScene.AssetTrends); // 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); - public static readonly LastThirtyDays = new DateRange(4, 'Recent 30 days', false, false, DateRangeScene.Normal); - public static readonly ThisWeek = new DateRange(5, 'This week', false, false, DateRangeScene.Normal); - public static readonly LastWeek = new DateRange(6, 'Last week', false, false, DateRangeScene.Normal); - public static readonly ThisMonth = new DateRange(7, 'This month', false, false, DateRangeScene.Normal); - public static readonly LastMonth = new DateRange(8, 'Last month', 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); // Date ranges for normal and trend analysis scene - public static readonly ThisYear = new DateRange(9, 'This year', false, false, DateRangeScene.Normal, DateRangeScene.TrendAnalysis); - public static readonly LastYear = new DateRange(10, 'Last year', false, false, DateRangeScene.Normal, DateRangeScene.TrendAnalysis); - public static readonly ThisFiscalYear = new DateRange(11, 'This fiscal year', false, true, DateRangeScene.Normal, DateRangeScene.TrendAnalysis); - public static readonly LastFiscalYear = new DateRange(12, 'Last fiscal year', false, true, DateRangeScene.Normal, DateRangeScene.TrendAnalysis); + 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); // 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); - public static readonly RecentTwentyFourMonths = new DateRange(102, 'Recent 24 months', false, false, DateRangeScene.TrendAnalysis); - public static readonly RecentThirtySixMonths = new DateRange(103, 'Recent 36 months', false, false, DateRangeScene.TrendAnalysis); - public static readonly RecentTwoYears = new DateRange(104, 'Recent 2 years', false, false, DateRangeScene.TrendAnalysis); - public static readonly RecentThreeYears = new DateRange(105, 'Recent 3 years', false, false, DateRangeScene.TrendAnalysis); - public static readonly RecentFiveYears = new DateRange(106, 'Recent 5 years', false, false, DateRangeScene.TrendAnalysis); + 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); // Custom date range - public static readonly Custom = new DateRange(255, 'Custom Date', false, true, DateRangeScene.Normal, DateRangeScene.TrendAnalysis); + public static readonly Custom = new DateRange(255, 'Custom Date', false, true, DateRangeScene.Normal, DateRangeScene.TrendAnalysis, DateRangeScene.AssetTrends); public readonly type: number; public readonly name: string; diff --git a/src/core/setting.ts b/src/core/setting.ts index 8012bd49..500b4a80 100644 --- a/src/core/setting.ts +++ b/src/core/setting.ts @@ -7,7 +7,8 @@ import { ChartDataType, ChartSortingType, DEFAULT_CATEGORICAL_CHART_DATA_RANGE, - DEFAULT_TREND_CHART_DATA_RANGE + DEFAULT_TREND_CHART_DATA_RANGE, + DEFAULT_ASSET_TRENDS_CHART_DATA_RANGE } from './statistics.ts'; import { DEFAULT_CURRENCY_CODE } from '@/consts/currency.ts'; @@ -63,6 +64,8 @@ export interface ApplicationSettings extends BaseApplicationSetting { defaultCategoricalChartDataRangeType: number; defaultTrendChartType: number; defaultTrendChartDataRangeType: number; + defaultAssetTrendsChartType: number; + defaultAssetTrendsChartDataRangeType: number; }; } @@ -122,6 +125,8 @@ export const ALL_ALLOWED_CLOUD_SYNC_APP_SETTING_KEY_TYPES: Record = {}; - public static readonly Month = new ChartDateAggregationType(0, 'Monthly', 'Aggregate by Month'); - public static readonly Quarter = new ChartDateAggregationType(1, 'Quarterly', 'Aggregate by Quarter'); - public static readonly Year = new ChartDateAggregationType(2, 'Yearly', 'Aggregate by Year'); - public static readonly FiscalYear = new ChartDateAggregationType(3, 'FiscalYearly', 'Aggregate by Fiscal Year'); + public static readonly Day = new ChartDateAggregationType(4, 'Daily', 'Aggregate by Day', StatisticsAnalysisType.AssetTrends); + public static readonly Month = new ChartDateAggregationType(0, 'Monthly', 'Aggregate by Month', StatisticsAnalysisType.TrendAnalysis, StatisticsAnalysisType.AssetTrends); + public static readonly Quarter = new ChartDateAggregationType(1, 'Quarterly', 'Aggregate by Quarter', StatisticsAnalysisType.TrendAnalysis, StatisticsAnalysisType.AssetTrends); + public static readonly Year = new ChartDateAggregationType(2, 'Yearly', 'Aggregate by Year', StatisticsAnalysisType.TrendAnalysis, StatisticsAnalysisType.AssetTrends); + public static readonly FiscalYear = new ChartDateAggregationType(3, 'FiscalYearly', 'Aggregate by Fiscal Year', StatisticsAnalysisType.TrendAnalysis, StatisticsAnalysisType.AssetTrends); public static readonly Default = ChartDateAggregationType.Month; public readonly type: number; public readonly shortName: string; public readonly fullName: string; + private readonly availableAnalysisTypes: Record; - private constructor(type: number, shortName: string, fullName: string) { + private constructor(type: number, shortName: string, fullName: string, ...availableAnalysisTypes: StatisticsAnalysisType[]) { this.type = type; this.shortName = shortName; this.fullName = fullName; + this.availableAnalysisTypes = {}; + + if (availableAnalysisTypes) { + for (const analysisType of availableAnalysisTypes) { + this.availableAnalysisTypes[analysisType] = true; + } + } ChartDateAggregationType.allInstances.push(this); ChartDateAggregationType.allInstancesByType[type] = this; } - public static values(): ChartDateAggregationType[] { - return ChartDateAggregationType.allInstances; + public isAvailableAnalysisType(analysisType: StatisticsAnalysisType): boolean { + return this.availableAnalysisTypes[analysisType] || false; + } + + public static values(analysisType?: StatisticsAnalysisType): ChartDateAggregationType[] { + const availableInstances: ChartDateAggregationType[] = ChartDateAggregationType.allInstances; + + if (analysisType === undefined) { + return availableInstances; + } + + const ret: ChartDateAggregationType[] = []; + + for (const chartDataType of availableInstances) { + if (chartDataType.isAvailableAnalysisType(analysisType)) { + ret.push(chartDataType); + } + } + + return ret; } public static valueOf(type: number): ChartDateAggregationType | undefined { @@ -252,3 +287,4 @@ export class ChartDateAggregationType { export const DEFAULT_CATEGORICAL_CHART_DATA_RANGE: DateRange = DateRange.ThisMonth; export const DEFAULT_TREND_CHART_DATA_RANGE: DateRange = DateRange.ThisYear; +export const DEFAULT_ASSET_TRENDS_CHART_DATA_RANGE: DateRange = DateRange.ThisYear; diff --git a/src/desktop-main.ts b/src/desktop-main.ts index 4b089f5a..dc4ac400 100644 --- a/src/desktop-main.ts +++ b/src/desktop-main.ts @@ -100,7 +100,7 @@ import ConfirmDialog from '@/components/desktop/ConfirmDialog.vue'; import SnackBar from '@/components/desktop/SnackBar.vue'; import PieChartComponent from '@/components/desktop/PieChart.vue'; import RadarChartComponent from '@/components/desktop/RadarChart.vue'; -import MonthlyTrendsChart from '@/components/desktop/MonthlyTrendsChart.vue'; +import TrendsChart from '@/components/desktop/TrendsChart.vue'; import DateRangeSelectionDialog from '@/components/desktop/DateRangeSelectionDialog.vue'; import MonthSelectionDialog from '@/components/desktop/MonthSelectionDialog.vue'; import MonthRangeSelectionDialog from '@/components/desktop/MonthRangeSelectionDialog.vue'; @@ -542,7 +542,7 @@ app.component('ConfirmDialog', ConfirmDialog); app.component('SnackBar', SnackBar); app.component('PieChart', PieChartComponent); app.component('RadarChart', RadarChartComponent); -app.component('MonthlyTrendsChart', MonthlyTrendsChart); +app.component('TrendsChart', TrendsChart); app.component('DateRangeSelectionDialog', DateRangeSelectionDialog); app.component('MonthSelectionDialog', MonthSelectionDialog); app.component('MonthRangeSelectionDialog', MonthRangeSelectionDialog); diff --git a/src/lib/datetime.ts b/src/lib/datetime.ts index 5ec4b174..b3a97064 100644 --- a/src/lib/datetime.ts +++ b/src/lib/datetime.ts @@ -282,6 +282,10 @@ class MomentDateTime implements DateTime { return this.instance.utcOffset(); } + public getDateTimeAfterDays(days: number): DateTime { + return MomentDateTime.of(this.instance.clone().add(days, 'days')); + } + public toGregorianCalendarYearMonthDay(): YearMonthDay { return { year: this.instance.year(), diff --git a/src/lib/services.ts b/src/lib/services.ts index 31368911..aabbbba6 100644 --- a/src/lib/services.ts +++ b/src/lib/services.ts @@ -75,6 +75,8 @@ import type { TransactionStatisticResponse, TransactionStatisticTrendsRequest, TransactionStatisticTrendsResponseItem, + TransactionStatisticAssetTrendsRequest, + TransactionStatisticAssetTrendsResponseItem, TransactionAmountsRequestParams, TransactionAmountsResponse } from '@/models/transaction.ts'; @@ -536,6 +538,19 @@ export default { return axios.get>(`v1/transactions/statistics/trends.json?use_transaction_timezone=${req.useTransactionTimezone}` + (queryParams.length ? '&' + queryParams.join('&') : '')); }, + getTransactionStatisticsAssetTrends: (req: TransactionStatisticAssetTrendsRequest): ApiResponsePromise => { + const queryParams = []; + + if (req.startTime) { + queryParams.push(`start_time=${req.startTime}`); + } + + if (req.endTime) { + queryParams.push(`end_time=${req.endTime}`); + } + + return axios.get>('v1/transactions/statistics/asset_trends.json' + (queryParams.length ? '?' + queryParams.join('&') : '')); + }, getTransactionAmounts: (params: TransactionAmountsRequestParams, excludeAccountIds: string[], excludeCategoryIds: string[]): ApiResponsePromise => { const req = TransactionAmountsRequest.of(params); let queryParams = req.buildQuery(); diff --git a/src/locales/de.json b/src/locales/de.json index cb7314c6..33c778db 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -1982,6 +1982,7 @@ "Unable to retrieve transaction statistics": "Transaktionsstatistiken können nicht abgerufen werden", "Categorical Analysis": "Kategorische Analyse", "Trend Analysis": "Trendanalyse", + "Asset Trends": "Asset Trends", "Total Amount": "Gesamtbetrag", "Total Assets": "Gesamtvermögen", "Total Liabilities": "Gesamtverbindlichkeiten", @@ -1990,6 +1991,7 @@ "Total Outflows": "Total Outflows", "Total Inflows": "Total Inflows", "Net Income": "Net Income", + "Net Worth": "Net Worth", "Net Cash Flow": "Net Cash Flow", "Total Transactions": "Total Transactions", "Opening Balance": "Opening Balance", @@ -2012,6 +2014,7 @@ "Common Settings": "Allgemeine Einstellungen", "Categorical Analysis Settings": "Einstellungen für kategorische Analyse", "Trend Analysis Settings": "Einstellungen für Trendanalyse", + "Asset Trends Settings": "Asset Trends Settings", "Chart Type": "Diagrammtyp", "Default Chart Type": "Standarddiagrammtyp", "Chart Data Type": "Diagrammdatentyp", @@ -2033,6 +2036,7 @@ "Sort by Display Order": "Nach Anzeigereihenfolge sortieren", "Sort by Name": "Nach Name sortieren", "Time Granularity": "Time Granularity", + "Aggregate by Day": "Aggregate by Day", "Aggregate by Month": "Nach Monat aggregieren", "Aggregate by Quarter": "Nach Quartal aggregieren", "Aggregate by Year": "Nach Jahr aggregieren", diff --git a/src/locales/en.json b/src/locales/en.json index f4367316..7320d31b 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1982,6 +1982,7 @@ "Unable to retrieve transaction statistics": "Unable to retrieve transaction statistics", "Categorical Analysis": "Categorical Analysis", "Trend Analysis": "Trend Analysis", + "Asset Trends": "Asset Trends", "Total Amount": "Total Amount", "Total Assets": "Total Assets", "Total Liabilities": "Total Liabilities", @@ -1990,6 +1991,7 @@ "Total Outflows": "Total Outflows", "Total Inflows": "Total Inflows", "Net Income": "Net Income", + "Net Worth": "Net Worth", "Net Cash Flow": "Net Cash Flow", "Total Transactions": "Total Transactions", "Opening Balance": "Opening Balance", @@ -2012,6 +2014,7 @@ "Common Settings": "Common Settings", "Categorical Analysis Settings": "Categorical Analysis Settings", "Trend Analysis Settings": "Trend Analysis Settings", + "Asset Trends Settings": "Asset Trends Settings", "Chart Type": "Chart Type", "Default Chart Type": "Default Chart Type", "Chart Data Type": "Chart Data Type", @@ -2033,6 +2036,7 @@ "Sort by Display Order": "Sort by Display Order", "Sort by Name": "Sort by Name", "Time Granularity": "Time Granularity", + "Aggregate by Day": "Aggregate by Day", "Aggregate by Month": "Aggregate by Month", "Aggregate by Quarter": "Aggregate by Quarter", "Aggregate by Year": "Aggregate by Year", diff --git a/src/locales/es.json b/src/locales/es.json index 208af00f..301113ac 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -1982,6 +1982,7 @@ "Unable to retrieve transaction statistics": "No se pueden recuperar estadísticas de transacciones", "Categorical Analysis": "Análisis categórico", "Trend Analysis": "Análisis de tendencias", + "Asset Trends": "Asset Trends", "Total Amount": "Importe Total", "Total Assets": "Activos totales", "Total Liabilities": "Pasivos totales", @@ -1990,6 +1991,7 @@ "Total Outflows": "Total Outflows", "Total Inflows": "Total Inflows", "Net Income": "Net Income", + "Net Worth": "Net Worth", "Net Cash Flow": "Net Cash Flow", "Total Transactions": "Total Transactions", "Opening Balance": "Opening Balance", @@ -2012,6 +2014,7 @@ "Common Settings": "Configuraciones comunes", "Categorical Analysis Settings": "Configuración de análisis categórico", "Trend Analysis Settings": "Configuración de análisis de tendencias", + "Asset Trends Settings": "Asset Trends Settings", "Chart Type": "Tipo de gráfico", "Default Chart Type": "Tipo de gráfico predeterminado", "Chart Data Type": "Tipo de datos del gráfico", @@ -2033,6 +2036,7 @@ "Sort by Display Order": "Ordenar por orden de visualización", "Sort by Name": "Ordenar por Nombre", "Time Granularity": "Time Granularity", + "Aggregate by Day": "Aggregate by Day", "Aggregate by Month": "Agregado por mes", "Aggregate by Quarter": "Agregado por trimestre", "Aggregate by Year": "Agregado por año", diff --git a/src/locales/fr.json b/src/locales/fr.json index 93e395c5..4a1fd9a7 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -1982,6 +1982,7 @@ "Unable to retrieve transaction statistics": "Impossible de récupérer les statistiques de transaction", "Categorical Analysis": "Analyse catégorielle", "Trend Analysis": "Analyse de tendance", + "Asset Trends": "Asset Trends", "Total Amount": "Montant total", "Total Assets": "Total des actifs", "Total Liabilities": "Total des passifs", @@ -1990,6 +1991,7 @@ "Total Outflows": "Total des sorties", "Total Inflows": "Total des entrées", "Net Income": "Revenus nets", + "Net Worth": "Net Worth", "Net Cash Flow": "Flux de trésorerie net", "Total Transactions": "Total des transactions", "Opening Balance": "Solde d'ouverture", @@ -2012,6 +2014,7 @@ "Common Settings": "Paramètres communs", "Categorical Analysis Settings": "Paramètres d'analyse catégorielle", "Trend Analysis Settings": "Paramètres d'analyse de tendance", + "Asset Trends Settings": "Asset Trends Settings", "Chart Type": "Type de graphique", "Default Chart Type": "Type de graphique par défaut", "Chart Data Type": "Type de données du graphique", @@ -2033,6 +2036,7 @@ "Sort by Display Order": "Trier par ordre d'affichage", "Sort by Name": "Trier par nom", "Time Granularity": "Granularité temporelle", + "Aggregate by Day": "Aggregate by Day", "Aggregate by Month": "Agréger par mois", "Aggregate by Quarter": "Agréger par trimestre", "Aggregate by Year": "Agréger par année", diff --git a/src/locales/helpers.ts b/src/locales/helpers.ts index f09fc200..8828d8b6 100644 --- a/src/locales/helpers.ts +++ b/src/locales/helpers.ts @@ -598,9 +598,9 @@ export function useI18n() { return ret; } - function getLocalizedChartDateAggregationTypeAndDisplayName(fullName: boolean): TypeAndDisplayName[] { + function getLocalizedChartDateAggregationTypeAndDisplayName(analysisType: StatisticsAnalysisType, fullName: boolean): TypeAndDisplayName[] { const ret: TypeAndDisplayName[] = []; - const allTypes: ChartDateAggregationType[] = ChartDateAggregationType.values(); + const allTypes: ChartDateAggregationType[] = ChartDateAggregationType.values(analysisType); for (const type of allTypes) { ret.push({ @@ -2354,8 +2354,8 @@ export function useI18n() { getAllAccountBalanceTrendChartTypes: () => getLocalizedDisplayNameAndType(AccountBalanceTrendChartType.values()), getAllStatisticsChartDataTypes: (analysisType: StatisticsAnalysisType, withDesktopOnlyChart?: boolean) => getLocalizedDisplayNameAndType(ChartDataType.values(analysisType, withDesktopOnlyChart)), getAllStatisticsSortingTypes: () => getLocalizedDisplayNameAndType(ChartSortingType.values()), - getAllStatisticsDateAggregationTypes: () => getLocalizedChartDateAggregationTypeAndDisplayName(true), - getAllStatisticsDateAggregationTypesWithShortName: () => getLocalizedChartDateAggregationTypeAndDisplayName(false), + getAllStatisticsDateAggregationTypes: (analysisType: StatisticsAnalysisType) => getLocalizedChartDateAggregationTypeAndDisplayName(analysisType, true), + getAllStatisticsDateAggregationTypesWithShortName: (analysisType: StatisticsAnalysisType) => getLocalizedChartDateAggregationTypeAndDisplayName(analysisType, false), getAllTransactionEditScopeTypes: () => getLocalizedDisplayNameAndType(TransactionEditScopeType.values()), getAllTransactionTagFilterTypes: () => getLocalizedDisplayNameAndType(TransactionTagFilterType.values()), getAllTransactionScheduledFrequencyTypes: () => getLocalizedDisplayNameAndType(ScheduledTemplateFrequencyType.values()), diff --git a/src/locales/it.json b/src/locales/it.json index 8ba91360..19d3a6e0 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -1982,6 +1982,7 @@ "Unable to retrieve transaction statistics": "Impossibile recuperare le statistiche delle transazioni", "Categorical Analysis": "Analisi per categoria", "Trend Analysis": "Analisi dell'andamento", + "Asset Trends": "Asset Trends", "Total Amount": "Importo totale", "Total Assets": "Patrimonio totale", "Total Liabilities": "Passività totali", @@ -1990,6 +1991,7 @@ "Total Outflows": "Total Outflows", "Total Inflows": "Total Inflows", "Net Income": "Net Income", + "Net Worth": "Net Worth", "Net Cash Flow": "Net Cash Flow", "Total Transactions": "Total Transactions", "Opening Balance": "Opening Balance", @@ -2012,6 +2014,7 @@ "Common Settings": "Impostazioni comuni", "Categorical Analysis Settings": "Impostazioni analisi per categoria", "Trend Analysis Settings": "Impostazioni analisi dell'andamento", + "Asset Trends Settings": "Asset Trends Settings", "Chart Type": "Tipo di grafico", "Default Chart Type": "Tipo di grafico predefinito", "Chart Data Type": "Tipo di dati grafico", @@ -2033,6 +2036,7 @@ "Sort by Display Order": "Ordina per ordine di visualizzazione", "Sort by Name": "Ordina per nome", "Time Granularity": "Time Granularity", + "Aggregate by Day": "Aggregate by Day", "Aggregate by Month": "Aggrega per mese", "Aggregate by Quarter": "Aggrega per trimestre", "Aggregate by Year": "Aggrega per anno", diff --git a/src/locales/ja.json b/src/locales/ja.json index 523a1dc8..8c64611a 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -1982,6 +1982,7 @@ "Unable to retrieve transaction statistics": "取引統計を取得できません", "Categorical Analysis": "カテゴリ分析", "Trend Analysis": "傾向分析", + "Asset Trends": "Asset Trends", "Total Amount": "合計金額", "Total Assets": "総資産", "Total Liabilities": "総負債", @@ -1990,6 +1991,7 @@ "Total Outflows": "Total Outflows", "Total Inflows": "Total Inflows", "Net Income": "Net Income", + "Net Worth": "Net Worth", "Net Cash Flow": "Net Cash Flow", "Total Transactions": "Total Transactions", "Opening Balance": "Opening Balance", @@ -2012,6 +2014,7 @@ "Common Settings": "共通設定", "Categorical Analysis Settings": "カテゴリ分析設定", "Trend Analysis Settings": "傾向分析設定", + "Asset Trends Settings": "Asset Trends Settings", "Chart Type": "グラフの種類", "Default Chart Type": "デフォルトのグラフの種類", "Chart Data Type": "グラフデータの種類", @@ -2033,6 +2036,7 @@ "Sort by Display Order": "表示で並べ替え", "Sort by Name": "名前で並べ替え", "Time Granularity": "Time Granularity", + "Aggregate by Day": "Aggregate by Day", "Aggregate by Month": "月ごとに集計", "Aggregate by Quarter": "四半期ごとに集計", "Aggregate by Year": "年ごとに集計", diff --git a/src/locales/ko.json b/src/locales/ko.json index 5dd8d675..82794ad9 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -1982,6 +1982,7 @@ "Unable to retrieve transaction statistics": "거래 통계를 검색할 수 없습니다", "Categorical Analysis": "범주 분석", "Trend Analysis": "추세 분석", + "Asset Trends": "Asset Trends", "Total Amount": "총 금액", "Total Assets": "총 자산", "Total Liabilities": "총 부채", @@ -1990,6 +1991,7 @@ "Total Outflows": "총 유출", "Total Inflows": "총 유입", "Net Income": "순수익", + "Net Worth": "Net Worth", "Net Cash Flow": "순현금흐름", "Total Transactions": "총 거래 수", "Opening Balance": "기초 잔액", @@ -2012,6 +2014,7 @@ "Common Settings": "일반 설정", "Categorical Analysis Settings": "범주 분석 설정", "Trend Analysis Settings": "추세 분석 설정", + "Asset Trends Settings": "Asset Trends Settings", "Chart Type": "차트 유형", "Default Chart Type": "기본 차트 유형", "Chart Data Type": "차트 데이터 유형", @@ -2033,6 +2036,7 @@ "Sort by Display Order": "표시 순서별 정렬", "Sort by Name": "이름별 정렬", "Time Granularity": "시간 세분화", + "Aggregate by Day": "Aggregate by Day", "Aggregate by Month": "월별 집계", "Aggregate by Quarter": "분기별 집계", "Aggregate by Year": "연도별 집계", diff --git a/src/locales/nl.json b/src/locales/nl.json index 2b2be85f..8dcd8f1b 100644 --- a/src/locales/nl.json +++ b/src/locales/nl.json @@ -1982,6 +1982,7 @@ "Unable to retrieve transaction statistics": "Kan transactiestatistieken niet ophalen", "Categorical Analysis": "Categorische analyse", "Trend Analysis": "Trendanalyse", + "Asset Trends": "Asset Trends", "Total Amount": "Totaalbedrag", "Total Assets": "Totaal activa", "Total Liabilities": "Totaal passiva", @@ -1990,6 +1991,7 @@ "Total Outflows": "Totaal uitgaand", "Total Inflows": "Totaal inkomend", "Net Income": "Netto-inkomen", + "Net Worth": "Net Worth", "Net Cash Flow": "Netto-kasstroom", "Total Transactions": "Totaal transacties", "Opening Balance": "Openingssaldo", @@ -2012,6 +2014,7 @@ "Common Settings": "Algemene instellingen", "Categorical Analysis Settings": "Instellingen categorische analyse", "Trend Analysis Settings": "Instellingen trendanalyse", + "Asset Trends Settings": "Asset Trends Settings", "Chart Type": "Diagramtype", "Default Chart Type": "Standaard diagramtype", "Chart Data Type": "Diagram-gegevenstype", @@ -2033,6 +2036,7 @@ "Sort by Display Order": "Sorteren op weergavevolgorde", "Sort by Name": "Sorteren op naam", "Time Granularity": "Tijdgranulariteit", + "Aggregate by Day": "Aggregate by Day", "Aggregate by Month": "Groeperen per maand", "Aggregate by Quarter": "Groeperen per kwartaal", "Aggregate by Year": "Groeperen per jaar", diff --git a/src/locales/pt_BR.json b/src/locales/pt_BR.json index f0c05abd..8aa0497b 100644 --- a/src/locales/pt_BR.json +++ b/src/locales/pt_BR.json @@ -1982,6 +1982,7 @@ "Unable to retrieve transaction statistics": "Não é possível recuperar estatísticas da transação", "Categorical Analysis": "Análise Categórica", "Trend Analysis": "Análise de Tendência", + "Asset Trends": "Asset Trends", "Total Amount": "Montante Total", "Total Assets": "Total de Ativos", "Total Liabilities": "Total de Passivos", @@ -1990,6 +1991,7 @@ "Total Outflows": "Total Outflows", "Total Inflows": "Total Inflows", "Net Income": "Net Income", + "Net Worth": "Net Worth", "Net Cash Flow": "Net Cash Flow", "Total Transactions": "Total Transactions", "Opening Balance": "Opening Balance", @@ -2012,6 +2014,7 @@ "Common Settings": "Configurações Comuns", "Categorical Analysis Settings": "Configurações de Análise Categórica", "Trend Analysis Settings": "Configurações de Análise de Tendência", + "Asset Trends Settings": "Asset Trends Settings", "Chart Type": "Tipo de Gráfico", "Default Chart Type": "Tipo de Gráfico Padrão", "Chart Data Type": "Tipo de Dados do Gráfico", @@ -2033,6 +2036,7 @@ "Sort by Display Order": "Classificar por Ordem de Exibição", "Sort by Name": "Classificar por Nome", "Time Granularity": "Time Granularity", + "Aggregate by Day": "Aggregate by Day", "Aggregate by Month": "Agregado por Mês", "Aggregate by Quarter": "Agregado por Trimestre", "Aggregate by Year": "Agregado por Ano", diff --git a/src/locales/ru.json b/src/locales/ru.json index c5d5a206..a5363352 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1982,6 +1982,7 @@ "Unable to retrieve transaction statistics": "Не удалось получить статистику транзакций", "Categorical Analysis": "Категориальный анализ", "Trend Analysis": "Анализ тенденций", + "Asset Trends": "Asset Trends", "Total Amount": "Общая сумма", "Total Assets": "Общие активы", "Total Liabilities": "Общие обязательства", @@ -1990,6 +1991,7 @@ "Total Outflows": "Total Outflows", "Total Inflows": "Total Inflows", "Net Income": "Net Income", + "Net Worth": "Net Worth", "Net Cash Flow": "Net Cash Flow", "Total Transactions": "Total Transactions", "Opening Balance": "Opening Balance", @@ -2012,6 +2014,7 @@ "Common Settings": "Общие настройки", "Categorical Analysis Settings": "Настройки категориального анализа", "Trend Analysis Settings": "Настройки анализа тенденций", + "Asset Trends Settings": "Asset Trends Settings", "Chart Type": "Тип диаграммы", "Default Chart Type": "Тип диаграммы по умолчанию", "Chart Data Type": "Тип данных диаграммы", @@ -2033,6 +2036,7 @@ "Sort by Display Order": "Сортировать по порядку отображения", "Sort by Name": "Сортировать по имени", "Time Granularity": "Time Granularity", + "Aggregate by Day": "Aggregate by Day", "Aggregate by Month": "Агрегировать по месяцам", "Aggregate by Quarter": "Агрегировать по кварталам", "Aggregate by Year": "Агрегировать по годам", diff --git a/src/locales/th.json b/src/locales/th.json index a6e22529..00454b01 100644 --- a/src/locales/th.json +++ b/src/locales/th.json @@ -1982,6 +1982,7 @@ "Unable to retrieve transaction statistics": "ไม่สามารถดึงสถิติรายการได้", "Categorical Analysis": "วิเคราะห์ตามหมวดหมู่", "Trend Analysis": "วิเคราะห์แนวโน้ม", + "Asset Trends": "Asset Trends", "Total Amount": "จำนวนรวม", "Total Assets": "สินทรัพย์รวม", "Total Liabilities": "หนี้สินรวม", @@ -1990,6 +1991,7 @@ "Total Outflows": "เงินไหลออกรวม", "Total Inflows": "เงินไหลเข้ารวม", "Net Income": "รายได้สุทธิ", + "Net Worth": "Net Worth", "Net Cash Flow": "กระแสเงินสดสุทธิ", "Total Transactions": "จำนวนรายการทั้งหมด", "Opening Balance": "ยอดเริ่มต้น", @@ -2012,6 +2014,7 @@ "Common Settings": "การตั้งค่าทั่วไป", "Categorical Analysis Settings": "การตั้งค่าวิเคราะห์ตามหมวดหมู่", "Trend Analysis Settings": "การตั้งค่าวิเคราะห์แนวโน้ม", + "Asset Trends Settings": "Asset Trends Settings", "Chart Type": "ประเภทกราฟ", "Default Chart Type": "ประเภทกราฟเริ่มต้น", "Chart Data Type": "ประเภทข้อมูลกราฟ", @@ -2033,6 +2036,7 @@ "Sort by Display Order": "เรียงตามลำดับการแสดง", "Sort by Name": "เรียงตามชื่อ", "Time Granularity": "ความละเอียดเวลา", + "Aggregate by Day": "Aggregate by Day", "Aggregate by Month": "รวมตามเดือน", "Aggregate by Quarter": "รวมตามไตรมาส", "Aggregate by Year": "รวมตามปี", diff --git a/src/locales/uk.json b/src/locales/uk.json index 3166401f..858e6ac0 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -1982,6 +1982,7 @@ "Unable to retrieve transaction statistics": "Не вдалося отримати статистику транзакцій", "Categorical Analysis": "Аналіз за категоріями", "Trend Analysis": "Аналіз трендів", + "Asset Trends": "Asset Trends", "Total Amount": "Загальна сума", "Total Assets": "Загальні активи", "Total Liabilities": "Загальні зобов’язання", @@ -1990,6 +1991,7 @@ "Total Outflows": "Total Outflows", "Total Inflows": "Total Inflows", "Net Income": "Net Income", + "Net Worth": "Net Worth", "Net Cash Flow": "Net Cash Flow", "Total Transactions": "Total Transactions", "Opening Balance": "Opening Balance", @@ -2012,6 +2014,7 @@ "Common Settings": "Загальні налаштування", "Categorical Analysis Settings": "Налаштування аналізу за категоріями", "Trend Analysis Settings": "Налаштування аналізу трендів", + "Asset Trends Settings": "Asset Trends Settings", "Chart Type": "Тип діаграми", "Default Chart Type": "Тип діаграми за замовчуванням", "Chart Data Type": "Тип даних діаграми", @@ -2033,6 +2036,7 @@ "Sort by Display Order": "Сортувати за порядком відображення", "Sort by Name": "Сортувати за назвою", "Time Granularity": "Time Granularity", + "Aggregate by Day": "Aggregate by Day", "Aggregate by Month": "Агрегувати за місяцями", "Aggregate by Quarter": "Агрегувати за кварталами", "Aggregate by Year": "Агрегувати за роками", diff --git a/src/locales/vi.json b/src/locales/vi.json index a766daf9..b3369f36 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -1982,6 +1982,7 @@ "Unable to retrieve transaction statistics": "Không thể lấy thống kê giao dịch", "Categorical Analysis": "Phân tích theo danh mục", "Trend Analysis": "Phân tích xu hướng", + "Asset Trends": "Asset Trends", "Total Amount": "Tổng số tiền", "Total Assets": "Tổng tài sản", "Total Liabilities": "Tổng nợ phải trả", @@ -1990,6 +1991,7 @@ "Total Outflows": "Total Outflows", "Total Inflows": "Total Inflows", "Net Income": "Net Income", + "Net Worth": "Net Worth", "Net Cash Flow": "Net Cash Flow", "Total Transactions": "Total Transactions", "Opening Balance": "Opening Balance", @@ -2012,6 +2014,7 @@ "Common Settings": "Cài đặt chung", "Categorical Analysis Settings": "Cài đặt phân tích theo danh mục", "Trend Analysis Settings": "Cài đặt phân tích xu hướng", + "Asset Trends Settings": "Asset Trends Settings", "Chart Type": "Loại biểu đồ", "Default Chart Type": "Loại biểu đồ mặc định", "Chart Data Type": "Loại dữ liệu biểu đồ", @@ -2033,6 +2036,7 @@ "Sort by Display Order": "Sắp xếp theo thứ tự hiển thị", "Sort by Name": "Sắp xếp theo tên", "Time Granularity": "Time Granularity", + "Aggregate by Day": "Aggregate by Day", "Aggregate by Month": "Tổng hợp theo tháng", "Aggregate by Quarter": "Tổng hợp theo quý", "Aggregate by Year": "Tổng hợp theo năm", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index 97bb50f9..9294f74b 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -1982,6 +1982,7 @@ "Unable to retrieve transaction statistics": "无法获取交易统计数据", "Categorical Analysis": "分类分析", "Trend Analysis": "趋势分析", + "Asset Trends": "资产趋势", "Total Amount": "总金额", "Total Assets": "总资产", "Total Liabilities": "总负债", @@ -1990,6 +1991,7 @@ "Total Outflows": "总流出", "Total Inflows": "总流入", "Net Income": "净收入", + "Net Worth": "净资产", "Net Cash Flow": "净现金流", "Total Transactions": "总交易数", "Opening Balance": "期初余额", @@ -2012,6 +2014,7 @@ "Common Settings": "通用设置", "Categorical Analysis Settings": "分类分析设置", "Trend Analysis Settings": "趋势分析设置", + "Asset Trends Settings": "资产趋势设置", "Chart Type": "图表类型", "Default Chart Type": "默认图表类型", "Chart Data Type": "图表数据类型", @@ -2033,6 +2036,7 @@ "Sort by Display Order": "按显示顺序排序", "Sort by Name": "按名称排序", "Time Granularity": "时间粒度", + "Aggregate by Day": "按日聚合", "Aggregate by Month": "按月聚合", "Aggregate by Quarter": "按季度聚合", "Aggregate by Year": "按年聚合", diff --git a/src/locales/zh_Hant.json b/src/locales/zh_Hant.json index d367fe02..5284dfdc 100644 --- a/src/locales/zh_Hant.json +++ b/src/locales/zh_Hant.json @@ -1982,6 +1982,7 @@ "Unable to retrieve transaction statistics": "無法取得交易統計資料", "Categorical Analysis": "分類分析", "Trend Analysis": "趨勢分析", + "Asset Trends": "資產趨勢", "Total Amount": "總金額", "Total Assets": "總資產", "Total Liabilities": "總負債", @@ -1990,6 +1991,7 @@ "Total Outflows": "總流出", "Total Inflows": "總流入", "Net Income": "淨收入", + "Net Worth": "淨資產", "Net Cash Flow": "淨現金流量", "Total Transactions": "總交易數", "Opening Balance": "期初餘額", @@ -2012,6 +2014,7 @@ "Common Settings": "一般設定", "Categorical Analysis Settings": "分類分析設定", "Trend Analysis Settings": "趨勢分析設定", + "Asset Trends Settings": "資產趨勢設定", "Chart Type": "圖表類型", "Default Chart Type": "預設圖表類型", "Chart Data Type": "圖表資料類型", @@ -2033,6 +2036,7 @@ "Sort by Display Order": "依顯示順序排序", "Sort by Name": "依名稱排序", "Time Granularity": "時間粒度", + "Aggregate by Day": "依日彙整", "Aggregate by Month": "依月份彙整", "Aggregate by Quarter": "依季度彙整", "Aggregate by Year": "依年份彙整", diff --git a/src/mobile-main.ts b/src/mobile-main.ts index 8822f19f..a37c05cb 100644 --- a/src/mobile-main.ts +++ b/src/mobile-main.ts @@ -56,7 +56,7 @@ import TransactionCalendar from '@/components/common/TransactionCalendar.vue'; import ItemIcon from '@/components/mobile/ItemIcon.vue'; import LanguageSelectButton from '@/components/mobile/LanguageSelectButton.vue'; import PieChart from '@/components/mobile/PieChart.vue'; -import MonthlyTrendsBarChart from '@/components/mobile/MonthlyTrendsBarChart.vue'; +import TrendsBarChart from '@/components/mobile/TrendsBarChart.vue'; import PinCodeInputSheet from '@/components/mobile/PinCodeInputSheet.vue'; import PasswordInputSheet from '@/components/mobile/PasswordInputSheet.vue'; import PasscodeInputSheet from '@/components/mobile/PasscodeInputSheet.vue'; @@ -150,7 +150,7 @@ app.component('TransactionCalendar', TransactionCalendar); app.component('ItemIcon', ItemIcon); app.component('LanguageSelectButton', LanguageSelectButton); app.component('PieChart', PieChart); -app.component('MonthlyTrendsBarChart', MonthlyTrendsBarChart); +app.component('TrendsBarChart', TrendsBarChart); app.component('PinCodeInputSheet', PinCodeInputSheet); app.component('PasswordInputSheet', PasswordInputSheet); app.component('PasscodeInputSheet', PasscodeInputSheet); diff --git a/src/models/transaction.ts b/src/models/transaction.ts index ec2e60c7..53f8418a 100644 --- a/src/models/transaction.ts +++ b/src/models/transaction.ts @@ -1,5 +1,5 @@ import { type PartialRecord, itemAndIndex } from '@/core/base.ts'; -import type { Year1BasedMonth, TextualYearMonthDay, StartEndTime, WeekDay } from '@/core/datetime.ts'; +import type { TextualYearMonthDay, Year1BasedMonth, YearMonthDay, StartEndTime, WeekDay } from '@/core/datetime.ts'; import { type Coordinate, getNormalizedCoordinate } from '@/core/coordinate.ts'; import { TransactionType } from '@/core/transaction.ts'; @@ -581,6 +581,11 @@ export interface TransactionStatisticTrendsRequest extends YearMonthRangeRequest readonly useTransactionTimezone: boolean; } +export interface TransactionStatisticAssetTrendsRequest { + readonly startTime: number; + readonly endTime: number; +} + export const ALL_TRANSACTION_AMOUNTS_REQUEST_TYPE = [ 'today', 'thisWeek', @@ -710,12 +715,31 @@ export interface TransactionStatisticTrendsResponseItem { readonly items: TransactionStatisticResponseItem[]; } +export interface TransactionStatisticAssetTrendsResponseItem extends YearMonthDay { + readonly year: number; + readonly month: number; // 1-based (1 = January, 12 = December) + readonly day: number; + readonly items: TransactionStatisticAssetTrendsResponseDataItem[]; +} + +export interface TransactionStatisticAssetTrendsResponseDataItem { + readonly accountId: string; + readonly accountOpeningBalance: number; + readonly accountClosingBalance: number; +} + export interface YearMonthDataItem extends Year1BasedMonth, Record {} +export interface YearMonthDayDataItem extends YearMonthDay, Record {} + export interface YearMonthItems extends Record { readonly items: T[]; } +export interface YearMonthDayItems extends Record { + readonly items: T[]; +} + export interface SortableTransactionStatisticDataItem { readonly name: string; readonly displayOrders: number[]; @@ -749,6 +773,13 @@ export interface TransactionStatisticTrendsResponseItemWithInfo { readonly items: TransactionStatisticResponseItemWithInfo[]; } +export interface TransactionStatisticAssetTrendsResponseItemWithInfo { + readonly year: number; + readonly month: number; // 1-based (1 = January, 12 = December) + readonly day: number; + readonly items: TransactionStatisticResponseItemWithInfo[]; +} + export type TransactionStatisticDataItemType = 'category' | 'account' | 'total'; export interface TransactionStatisticDataItemBase extends SortableTransactionStatisticDataItem { @@ -820,6 +851,21 @@ export interface TransactionTrendsAnalysisDataAmount extends Record, TransactionStatisticDataItemBase { + readonly items: TransactionAssetTrendsAnalysisDataAmount[]; +} + +export interface TransactionAssetTrendsAnalysisDataAmount extends Record, YearMonthDay { + readonly year: number; + readonly month: number; + readonly day: number; + readonly totalAmount: number; +} + export type TransactionAmountsResponse = PartialRecord; export interface TransactionAmountsResponseItem { diff --git a/src/router/desktop.ts b/src/router/desktop.ts index 5c363b8b..898c9a79 100644 --- a/src/router/desktop.ts +++ b/src/router/desktop.ts @@ -134,7 +134,8 @@ const router = createRouter({ initTagFilterType: route.query['tagFilterType'], initKeyword: route.query['keyword'], initSortingType: route.query['sortingType'], - initTrendDateAggregationType: route.query['trendDateAggregationType'] + initTrendDateAggregationType: route.query['trendDateAggregationType'], + initAssetTrendsDateAggregationType: route.query['assetTrendsDateAggregationType'] }) }, { diff --git a/src/stores/setting.ts b/src/stores/setting.ts index ef415f18..703edb89 100644 --- a/src/stores/setting.ts +++ b/src/stores/setting.ts @@ -314,6 +314,18 @@ export const useSettingsStore = defineStore('settings', () => { updateUserApplicationCloudSettingValue('statistics.defaultTrendChartDataRangeType', value); } + function setStatisticsDefaultAssetTrendsChartType(value: number): void { + updateApplicationSettingsSubValue('statistics', 'defaultAssetTrendsChartType', value); + appSettings.value.statistics.defaultAssetTrendsChartType = value; + updateUserApplicationCloudSettingValue('statistics.defaultAssetTrendsChartType', value); + } + + function setStatisticsDefaultAssetTrendsChartDateRange(value: number): void { + updateApplicationSettingsSubValue('statistics', 'defaultAssetTrendsChartDataRangeType', value); + appSettings.value.statistics.defaultAssetTrendsChartDataRangeType = value; + updateUserApplicationCloudSettingValue('statistics.defaultAssetTrendsChartDataRangeType', value); + } + function clearAppSettings(): void { clearSettings(); appSettings.value = getApplicationSettings(); @@ -469,6 +481,8 @@ export const useSettingsStore = defineStore('settings', () => { setStatisticsDefaultCategoricalChartDateRange, setStatisticsDefaultTrendChartType, setStatisticsDefaultTrendChartDateRange, + setStatisticsDefaultAssetTrendsChartType, + setStatisticsDefaultAssetTrendsChartDateRange, clearAppSettings, createApplicationCloudSettings, setApplicationSettingsFromCloudSettings, diff --git a/src/stores/statistics.ts b/src/stores/statistics.ts index b663e5b0..508e647e 100644 --- a/src/stores/statistics.ts +++ b/src/stores/statistics.ts @@ -8,7 +8,7 @@ import { useTransactionCategoriesStore } from './transactionCategory.ts'; import { useExchangeRatesStore } from './exchangeRates.ts'; import { entries, values } from '@/core/base.ts'; -import { type TextualYearMonth, type TimeRangeAndDateType, DateRangeScene, DateRange } from '@/core/datetime.ts'; +import { type DateTime, type TextualYearMonth, type TimeRangeAndDateType, DateRangeScene, DateRange } from '@/core/datetime.ts'; import { TimezoneTypeForStatistics } from '@/core/timezone.ts'; import { CategoryType } from '@/core/category.ts'; import { @@ -23,7 +23,8 @@ import { ChartSortingType, ChartDateAggregationType, DEFAULT_CATEGORICAL_CHART_DATA_RANGE, - DEFAULT_TREND_CHART_DATA_RANGE + DEFAULT_TREND_CHART_DATA_RANGE, + DEFAULT_ASSET_TRENDS_CHART_DATA_RANGE } from '@/core/statistics.ts'; import { DEFAULT_ACCOUNT_ICON, DEFAULT_CATEGORY_ICON } from '@/consts/icon.ts'; import { DEFAULT_ACCOUNT_COLOR, DEFAULT_CATEGORY_COLOR } from '@/consts/color.ts'; @@ -32,9 +33,12 @@ import { type TransactionStatisticResponse, type TransactionStatisticResponseItem, type TransactionStatisticTrendsResponseItem, + type TransactionStatisticAssetTrendsResponseItem, + type TransactionStatisticAssetTrendsResponseDataItem, type TransactionStatisticResponseItemWithInfo, type TransactionStatisticResponseWithInfo, type TransactionStatisticTrendsResponseItemWithInfo, + type TransactionStatisticAssetTrendsResponseItemWithInfo, type TransactionStatisticDataItemType, type TransactionStatisticDataItemBase, type TransactionCategoricalOverviewAnalysisData, @@ -44,6 +48,9 @@ import { type TransactionTrendsAnalysisData, type TransactionTrendsAnalysisDataItem, type TransactionTrendsAnalysisDataAmount, + type TransactionAssetTrendsAnalysisData, + type TransactionAssetTrendsAnalysisDataItem, + type TransactionAssetTrendsAnalysisDataAmount, TransactionCategoricalOverviewAnalysisDataItemType } from '@/models/transaction.ts'; @@ -58,7 +65,12 @@ import { isObjectEmpty, objectFieldToArrayItem } from '@/lib/common.ts'; -import { getGregorianCalendarYearAndMonthFromUnixTime, getDateRangeByDateType } from '@/lib/datetime.ts'; +import { + getYearMonthDayDateTime, + getGregorianCalendarYearAndMonthFromUnixTime, + getDayDifference, + getDateRangeByDateType +} from '@/lib/datetime.ts'; import { getFinalAccountIdsByFilteredAccountIds } from '@/lib/account.ts'; import { getFinalCategoryIdsByFilteredCategoryIds } from '@/lib/category.ts'; import { sortStatisticsItems } from '@/lib/statistics.ts'; @@ -83,7 +95,7 @@ interface WritableTransactionCategoricalAnalysisDataItem extends Record { +interface WritableTransactionTrendsAnalysisDataItem extends Record, TransactionTrendsAnalysisDataItem { name: string; type: TransactionStatisticDataItemType; id: string; @@ -95,6 +107,18 @@ interface WritableTransactionTrendsAnalysisDataItem extends Record, TransactionAssetTrendsAnalysisDataItem { + name: string; + type: TransactionStatisticDataItemType; + id: string; + icon: string; + color: string; + hidden: boolean; + displayOrders: number[]; + totalAmount: number; + items: TransactionAssetTrendsAnalysisDataAmount[]; +} + export interface TransactionStatisticsPartialFilter { chartDataType?: number; categoricalChartType?: number; @@ -105,6 +129,10 @@ export interface TransactionStatisticsPartialFilter { trendChartDateType?: number; trendChartStartYearMonth?: TextualYearMonth | ''; trendChartEndYearMonth?: TextualYearMonth | ''; + assetTrendsChartType?: number; + assetTrendsChartDateType?: number; + assetTrendsChartStartTime?: number; + assetTrendsChartEndTime?: number; filterAccountIds?: Record; filterCategoryIds?: Record; tagIds?: string; @@ -123,6 +151,10 @@ export interface TransactionStatisticsFilter extends TransactionStatisticsPartia trendChartDateType: number; trendChartStartYearMonth: TextualYearMonth | ''; trendChartEndYearMonth: TextualYearMonth | ''; + assetTrendsChartType: number; + assetTrendsChartDateType: number; + assetTrendsChartStartTime: number; + assetTrendsChartEndTime: number; filterAccountIds: Record; filterCategoryIds: Record; tagIds: string; @@ -148,6 +180,10 @@ export const useStatisticsStore = defineStore('statistics', () => { trendChartDateType: DEFAULT_TREND_CHART_DATA_RANGE.type, trendChartStartYearMonth: '', trendChartEndYearMonth: '', + assetTrendsChartType: TrendChartType.Default.type, + assetTrendsChartDateType: DEFAULT_ASSET_TRENDS_CHART_DATA_RANGE.type, + assetTrendsChartStartTime: 0, + assetTrendsChartEndTime: 0, filterAccountIds: {}, filterCategoryIds: {}, tagIds: '', @@ -158,6 +194,7 @@ export const useStatisticsStore = defineStore('statistics', () => { const transactionCategoryStatisticsData = ref(null); const transactionCategoryTrendsData = ref([]); + const transactionAssetTrendsData = ref([]); const transactionStatisticsStateInvalid = ref(true); const categoricalAnalysisChartDataCategory = computed(() => { @@ -709,6 +746,227 @@ export const useStatisticsStore = defineStore('statistics', () => { return trendsData; }); + const assetTrendsDataWithAccountInfo = computed(() => { + const assetTrendsData = transactionAssetTrendsData.value; + const finalAssetTrendsData: TransactionStatisticAssetTrendsResponseItemWithInfo[] = []; + + if (!assetTrendsData || !assetTrendsData.length) { + return finalAssetTrendsData; + } + + const firstAssetTrendItem: TransactionStatisticAssetTrendsResponseItem | undefined = assetTrendsData[0]; + + if (!firstAssetTrendItem) { + return finalAssetTrendsData; + } + + const lastAssetTrendItemMap: Record = {}; + let lastAssetTrendItem: TransactionStatisticAssetTrendsResponseItem = firstAssetTrendItem; + + for (const item of firstAssetTrendItem.items) { + lastAssetTrendItemMap[item.accountId] = item; + } + + for (const assetTrendItem of assetTrendsData) { + const statisticResponseItems: TransactionStatisticResponseItem[] = []; + const existedAccountIds: Record = {}; + const missingDays: number = getDayDifference(lastAssetTrendItem, assetTrendItem) - 1; + const lastAssetTrendItemDate: DateTime = getYearMonthDayDateTime(lastAssetTrendItem.year, lastAssetTrendItem.month, lastAssetTrendItem.day); + + // fill in missing days with last known balance + for (let i = 1; i <= missingDays; i++) { + const missingStatisticResponseItems: TransactionStatisticResponseItem[] = []; + const dateTime: DateTime = lastAssetTrendItemDate.getDateTimeAfterDays(i); + + for (const item of values(lastAssetTrendItemMap)) { + const statisticResponseItem: TransactionStatisticResponseItem = { + categoryId: '', + accountId: item.accountId, + amount: item.accountClosingBalance + }; + + missingStatisticResponseItems.push(statisticResponseItem); + } + + const finalAssetTrendItem: TransactionStatisticAssetTrendsResponseItemWithInfo = { + year: dateTime.getGregorianCalendarYear(), + month: dateTime.getGregorianCalendarMonth(), + day: dateTime.getGregorianCalendarDay(), + items: assembleAccountAndCategoryInfo(missingStatisticResponseItems) + }; + + lastAssetTrendItem = assetTrendItem; + finalAssetTrendsData.push(finalAssetTrendItem); + } + + // fill in current day data + for (const item of assetTrendItem.items) { + const statisticResponseItem: TransactionStatisticResponseItem = { + categoryId: '', + accountId: item.accountId, + amount: item.accountClosingBalance + }; + + lastAssetTrendItemMap[item.accountId] = item; + existedAccountIds[item.accountId] = true; + statisticResponseItems.push(statisticResponseItem); + } + + // fill in missing accounts with last known balance + for (const item of values(lastAssetTrendItemMap)) { + if (existedAccountIds[item.accountId]) { + continue; + } + + const statisticResponseItem: TransactionStatisticResponseItem = { + categoryId: '', + accountId: item.accountId, + amount: item.accountClosingBalance + }; + + existedAccountIds[item.accountId] = true; + statisticResponseItems.push(statisticResponseItem); + } + + const finalAssetTrendItem: TransactionStatisticAssetTrendsResponseItemWithInfo = { + year: assetTrendItem.year, + month: assetTrendItem.month, + day: assetTrendItem.day, + items: assembleAccountAndCategoryInfo(statisticResponseItems) + }; + + lastAssetTrendItem = assetTrendItem; + finalAssetTrendsData.push(finalAssetTrendItem); + } + + return finalAssetTrendsData; + }); + + const assetTrendsData = computed(() => { + if (!assetTrendsDataWithAccountInfo.value || !assetTrendsDataWithAccountInfo.value.length) { + return null; + } + + const combinedDataMap: Record = {}; + + for (const dailyData of assetTrendsDataWithAccountInfo.value) { + let dailyTotalAmount: number = 0; + + for (const item of dailyData.items) { + if (!item.primaryAccount || !item.account) { + continue; + } + + if (transactionStatisticsFilter.value.filterAccountIds && transactionStatisticsFilter.value.filterAccountIds[item.account.id]) { + continue; + } + + if (!isNumber(item.amountInDefaultCurrency)) { + continue; + } + + if (transactionStatisticsFilter.value.chartDataType === ChartDataType.AccountTotalAssets.type) { + if (!item.account.isAsset) { + continue; + } + } else if (transactionStatisticsFilter.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) { + if (!item.account.isLiability) { + continue; + } + } else if (transactionStatisticsFilter.value.chartDataType === ChartDataType.NetWorth.type) { + // Do Nothing + } else { + continue; + } + + let amount = item.amountInDefaultCurrency; + + if (item.account.isLiability) { + amount = -amount; + } + + if (transactionStatisticsFilter.value.chartDataType === ChartDataType.AccountTotalAssets.type || + transactionStatisticsFilter.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) { + let data = combinedDataMap[item.account.id]; + + if (data) { + data.totalAmount += amount; + } else { + data = { + name: item.account.name, + type: 'account', + id: item.account.id, + icon: item.account.icon || DEFAULT_ACCOUNT_ICON.icon, + color: item.account.color || DEFAULT_ACCOUNT_COLOR, + hidden: item.primaryAccount.hidden || item.account.hidden, + displayOrders: [item.primaryAccount.category, item.primaryAccount.displayOrder, item.account.displayOrder], + totalAmount: amount, + items: [] + }; + } + + const amountItem: TransactionAssetTrendsAnalysisDataAmount = { + year: dailyData.year, + month: dailyData.month, + day: dailyData.day, + totalAmount: amount + }; + data.items.push(amountItem); + combinedDataMap[item.account.id] = data; + } + + if (item.account.isAsset) { + dailyTotalAmount += amount; + } else if (item.account.isLiability) { + dailyTotalAmount -= amount; + } + } + + if (transactionStatisticsFilter.value.chartDataType === ChartDataType.NetWorth.type) { + let data = combinedDataMap['total']; + + if (data) { + data.totalAmount += dailyTotalAmount; + } else { + data = { + name: ChartDataType.NetWorth.name, + type: 'total', + id: 'total', + icon: '', + color: '', + hidden: false, + displayOrders: [1], + totalAmount: dailyTotalAmount, + items: [] + }; + } + + const amountItem: TransactionAssetTrendsAnalysisDataAmount = { + year: dailyData.year, + month: dailyData.month, + day: dailyData.day, + totalAmount: dailyTotalAmount + }; + data.items.push(amountItem); + combinedDataMap['total'] = data; + } + } + + const allAssetTrendsDataItems: TransactionAssetTrendsAnalysisDataItem[] = []; + + for (const assetTrendsDataItem of values(combinedDataMap)) { + allAssetTrendsDataItems.push(assetTrendsDataItem); + } + + sortCategoryTotalAmountItems(allAssetTrendsDataItems, transactionStatisticsFilter.value); + + const assetTrendsData: TransactionAssetTrendsAnalysisData = { + items: allAssetTrendsDataItems + }; + + return assetTrendsData; + }); + function createNewTransactionCategoricalOverviewAnalysisDataItem(id: string, name: string, type: TransactionCategoricalOverviewAnalysisDataItemType, displayOrders: number[], hidden: boolean): TransactionCategoricalOverviewAnalysisDataItem { const dataItem: TransactionCategoricalOverviewAnalysisDataItem = { id: id, @@ -1062,6 +1320,10 @@ export const useStatisticsStore = defineStore('statistics', () => { transactionStatisticsFilter.value.trendChartDateType = DEFAULT_TREND_CHART_DATA_RANGE.type; transactionStatisticsFilter.value.trendChartStartYearMonth = ''; transactionStatisticsFilter.value.trendChartEndYearMonth = ''; + transactionStatisticsFilter.value.assetTrendsChartType = TrendChartType.Default.type; + transactionStatisticsFilter.value.assetTrendsChartDateType = DEFAULT_ASSET_TRENDS_CHART_DATA_RANGE.type; + transactionStatisticsFilter.value.assetTrendsChartStartTime = 0; + transactionStatisticsFilter.value.assetTrendsChartEndTime = 0; transactionStatisticsFilter.value.filterAccountIds = {}; transactionStatisticsFilter.value.filterCategoryIds = {}; transactionStatisticsFilter.value.tagIds = ''; @@ -1083,8 +1345,13 @@ export const useStatisticsStore = defineStore('statistics', () => { if (!ChartDataType.isAvailableForAnalysisType(transactionStatisticsFilter.value.chartDataType, analysisType)) { transactionStatisticsFilter.value.chartDataType = ChartDataType.Default.type; } + } else if (analysisType === StatisticsAnalysisType.AssetTrends) { + if (!ChartDataType.isAvailableForAnalysisType(transactionStatisticsFilter.value.chartDataType, analysisType)) { + transactionStatisticsFilter.value.chartDataType = ChartDataType.DefaultForAssetTrends.type; + } } + // Categorical Analysis filter initialization if (filter && isInteger(filter.categoricalChartType)) { transactionStatisticsFilter.value.categoricalChartType = filter.categoricalChartType; } else { @@ -1130,6 +1397,7 @@ export const useStatisticsStore = defineStore('statistics', () => { } } + // Trend Analysis filter initialization if (filter && isInteger(filter.trendChartType)) { transactionStatisticsFilter.value.trendChartType = filter.trendChartType; } else { @@ -1175,6 +1443,53 @@ export const useStatisticsStore = defineStore('statistics', () => { } } + // Asset Trends filter initialization + if (filter && isInteger(filter.assetTrendsChartType)) { + transactionStatisticsFilter.value.assetTrendsChartType = filter.assetTrendsChartType; + } else { + transactionStatisticsFilter.value.assetTrendsChartType = settingsStore.appSettings.statistics.defaultAssetTrendsChartType; + } + + if (!TrendChartType.isValidType(transactionStatisticsFilter.value.assetTrendsChartType)) { + transactionStatisticsFilter.value.assetTrendsChartType = TrendChartType.Default.type; + } + + if (filter && isInteger(filter.assetTrendsChartDateType)) { + transactionStatisticsFilter.value.assetTrendsChartDateType = filter.assetTrendsChartDateType; + } else { + transactionStatisticsFilter.value.assetTrendsChartDateType = settingsStore.appSettings.statistics.defaultAssetTrendsChartDataRangeType; + } + + let assetTrendsChartDateTypeValid = true; + + if (!DateRange.isAvailableForScene(transactionStatisticsFilter.value.assetTrendsChartDateType, DateRangeScene.AssetTrends)) { + transactionStatisticsFilter.value.assetTrendsChartDateType = DEFAULT_ASSET_TRENDS_CHART_DATA_RANGE.type; + assetTrendsChartDateTypeValid = false; + } + + if (assetTrendsChartDateTypeValid && transactionStatisticsFilter.value.assetTrendsChartDateType === DateRange.Custom.type) { + if (filter && isInteger(filter.assetTrendsChartStartTime)) { + transactionStatisticsFilter.value.assetTrendsChartStartTime = filter.assetTrendsChartStartTime; + } else { + transactionStatisticsFilter.value.assetTrendsChartStartTime = 0; + } + + if (filter && isInteger(filter.assetTrendsChartEndTime)) { + transactionStatisticsFilter.value.assetTrendsChartEndTime = filter.assetTrendsChartEndTime; + } else { + transactionStatisticsFilter.value.assetTrendsChartEndTime = 0; + } + } else { + const assetTrendsChartDateRange = getDateRangeByDateType(transactionStatisticsFilter.value.assetTrendsChartDateType, userStore.currentUserFirstDayOfWeek, userStore.currentUserFiscalYearStart); + + if (assetTrendsChartDateRange) { + transactionStatisticsFilter.value.assetTrendsChartDateType = assetTrendsChartDateRange.dateType; + transactionStatisticsFilter.value.assetTrendsChartStartTime = assetTrendsChartDateRange.minTime; + transactionStatisticsFilter.value.assetTrendsChartEndTime = assetTrendsChartDateRange.maxTime; + } + } + + // Other filter initialization if (filter && isObject(filter.filterAccountIds)) { transactionStatisticsFilter.value.filterAccountIds = filter.filterAccountIds; } else { @@ -1224,6 +1539,7 @@ export const useStatisticsStore = defineStore('statistics', () => { changed = true; } + // Categorical Analysis filter update if (filter && isInteger(filter.categoricalChartType) && transactionStatisticsFilter.value.categoricalChartType !== filter.categoricalChartType) { transactionStatisticsFilter.value.categoricalChartType = filter.categoricalChartType; changed = true; @@ -1244,6 +1560,7 @@ export const useStatisticsStore = defineStore('statistics', () => { changed = true; } + // Trend Analysis filter update if (filter && isInteger(filter.trendChartType) && transactionStatisticsFilter.value.trendChartType !== filter.trendChartType) { transactionStatisticsFilter.value.trendChartType = filter.trendChartType; changed = true; @@ -1264,6 +1581,28 @@ export const useStatisticsStore = defineStore('statistics', () => { changed = true; } + // Asset Trends filter update + if (filter && isInteger(filter.assetTrendsChartType) && transactionStatisticsFilter.value.assetTrendsChartType !== filter.assetTrendsChartType) { + transactionStatisticsFilter.value.assetTrendsChartType = filter.assetTrendsChartType; + changed = true; + } + + if (filter && isInteger(filter.assetTrendsChartDateType) && transactionStatisticsFilter.value.assetTrendsChartDateType !== filter.assetTrendsChartDateType) { + transactionStatisticsFilter.value.assetTrendsChartDateType = filter.assetTrendsChartDateType; + changed = true; + } + + if (filter && isInteger(filter.assetTrendsChartStartTime) && transactionStatisticsFilter.value.assetTrendsChartStartTime !== filter.assetTrendsChartStartTime) { + transactionStatisticsFilter.value.assetTrendsChartStartTime = filter.assetTrendsChartStartTime; + changed = true; + } + + if (filter && isInteger(filter.assetTrendsChartEndTime) && transactionStatisticsFilter.value.assetTrendsChartEndTime !== filter.assetTrendsChartEndTime) { + transactionStatisticsFilter.value.assetTrendsChartEndTime = filter.assetTrendsChartEndTime; + changed = true; + } + + // Other filter update if (filter && isObject(filter.filterAccountIds) && !isEquals(transactionStatisticsFilter.value.filterAccountIds, filter.filterAccountIds)) { transactionStatisticsFilter.value.filterAccountIds = filter.filterAccountIds; changed = true; @@ -1297,7 +1636,7 @@ export const useStatisticsStore = defineStore('statistics', () => { return changed; } - function getTransactionStatisticsPageParams(analysisType: StatisticsAnalysisType, trendDateAggregationType: number): string { + function getTransactionStatisticsPageParams(analysisType: StatisticsAnalysisType, trendDateAggregationType: number, assetTrendsDateAggregationType: number): string { const querys: string[] = []; querys.push('analysisType=' + analysisType); @@ -1320,9 +1659,21 @@ export const useStatisticsStore = defineStore('statistics', () => { querys.push('endTime=' + transactionStatisticsFilter.value.trendChartEndYearMonth); } - if (trendDateAggregationType !== ChartDateAggregationType.Month.type) { + if (trendDateAggregationType !== ChartDateAggregationType.Default.type) { querys.push('trendDateAggregationType=' + trendDateAggregationType); } + } else if (analysisType === StatisticsAnalysisType.AssetTrends) { + querys.push('chartType=' + transactionStatisticsFilter.value.assetTrendsChartType); + querys.push('chartDateType=' + transactionStatisticsFilter.value.assetTrendsChartDateType); + + if (transactionStatisticsFilter.value.assetTrendsChartDateType === DateRange.Custom.type) { + querys.push('startTime=' + transactionStatisticsFilter.value.assetTrendsChartStartTime); + querys.push('endTime=' + transactionStatisticsFilter.value.assetTrendsChartEndTime); + } + + if (assetTrendsDateAggregationType !== ChartDateAggregationType.Default.type) { + querys.push('assetTrendsDateAggregationType=' + assetTrendsDateAggregationType); + } } if (transactionStatisticsFilter.value.filterAccountIds) { @@ -1414,21 +1765,23 @@ export const useStatisticsStore = defineStore('statistics', () => { } else { querys.push('categoryIds=' + getFinalCategoryIdsByFilteredCategoryIds(transactionCategoriesStore.allTransactionCategoriesMap, transactionStatisticsFilter.value.filterCategoryIds)); } - } else if (itemId && (transactionStatisticsFilter.value.chartDataType === ChartDataType.InflowsByAccount.type - || transactionStatisticsFilter.value.chartDataType === ChartDataType.IncomeByAccount.type - || transactionStatisticsFilter.value.chartDataType === ChartDataType.OutflowsByAccount.type - || transactionStatisticsFilter.value.chartDataType === ChartDataType.ExpenseByAccount.type - || transactionStatisticsFilter.value.chartDataType === ChartDataType.AccountTotalAssets.type - || transactionStatisticsFilter.value.chartDataType === ChartDataType.AccountTotalLiabilities.type)) { + } else if (itemId && (transactionStatisticsFilter.value.chartDataType === ChartDataType.InflowsByAccount.type || + transactionStatisticsFilter.value.chartDataType === ChartDataType.IncomeByAccount.type || + transactionStatisticsFilter.value.chartDataType === ChartDataType.OutflowsByAccount.type || + transactionStatisticsFilter.value.chartDataType === ChartDataType.ExpenseByAccount.type || + transactionStatisticsFilter.value.chartDataType === ChartDataType.AccountTotalAssets.type || + transactionStatisticsFilter.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) + ) { querys.push('accountIds=' + itemId); - if (!isObjectEmpty(transactionStatisticsFilter.value.filterCategoryIds)) { + if ((analysisType === StatisticsAnalysisType.CategoricalAnalysis || analysisType === StatisticsAnalysisType.TrendAnalysis) && !isObjectEmpty(transactionStatisticsFilter.value.filterCategoryIds)) { querys.push('categoryIds=' + getFinalCategoryIdsByFilteredCategoryIds(transactionCategoriesStore.allTransactionCategoriesMap, transactionStatisticsFilter.value.filterCategoryIds)); } - } else if (itemId && (transactionStatisticsFilter.value.chartDataType === ChartDataType.IncomeByPrimaryCategory.type - || transactionStatisticsFilter.value.chartDataType === ChartDataType.IncomeBySecondaryCategory.type - || transactionStatisticsFilter.value.chartDataType === ChartDataType.ExpenseByPrimaryCategory.type - || transactionStatisticsFilter.value.chartDataType === ChartDataType.ExpenseBySecondaryCategory.type)) { + } else if (itemId && (transactionStatisticsFilter.value.chartDataType === ChartDataType.IncomeByPrimaryCategory.type || + transactionStatisticsFilter.value.chartDataType === ChartDataType.IncomeBySecondaryCategory.type || + transactionStatisticsFilter.value.chartDataType === ChartDataType.ExpenseByPrimaryCategory.type || + transactionStatisticsFilter.value.chartDataType === ChartDataType.ExpenseBySecondaryCategory.type) + ) { querys.push('categoryIds=' + itemId); if (!isObjectEmpty(transactionStatisticsFilter.value.filterAccountIds)) { @@ -1444,16 +1797,18 @@ export const useStatisticsStore = defineStore('statistics', () => { } } - if (transactionStatisticsFilter.value.tagIds) { - querys.push('tagIds=' + transactionStatisticsFilter.value.tagIds); - } + if (analysisType === StatisticsAnalysisType.CategoricalAnalysis || analysisType === StatisticsAnalysisType.TrendAnalysis) { + if (transactionStatisticsFilter.value.tagIds) { + querys.push('tagIds=' + transactionStatisticsFilter.value.tagIds); + } - if (transactionStatisticsFilter.value.tagFilterType) { - querys.push('tagFilterType=' + transactionStatisticsFilter.value.tagFilterType); - } + if (transactionStatisticsFilter.value.tagFilterType) { + querys.push('tagFilterType=' + transactionStatisticsFilter.value.tagFilterType); + } - if (transactionStatisticsFilter.value.keyword) { - querys.push('keyword=' + encodeURIComponent(transactionStatisticsFilter.value.keyword)); + if (transactionStatisticsFilter.value.keyword) { + querys.push('keyword=' + encodeURIComponent(transactionStatisticsFilter.value.keyword)); + } } if (analysisType === StatisticsAnalysisType.CategoricalAnalysis @@ -1465,7 +1820,7 @@ export const useStatisticsStore = defineStore('statistics', () => { querys.push('minTime=' + transactionStatisticsFilter.value.categoricalChartStartTime); querys.push('maxTime=' + transactionStatisticsFilter.value.categoricalChartEndTime); } - } else if (analysisType === StatisticsAnalysisType.TrendAnalysis && dateRange) { + } else if ((analysisType === StatisticsAnalysisType.TrendAnalysis || analysisType === StatisticsAnalysisType.AssetTrends) && dateRange) { querys.push('dateType=' + dateRange.dateType); querys.push('minTime=' + dateRange.minTime); querys.push('maxTime=' + dateRange.maxTime); @@ -1560,6 +1915,45 @@ export const useStatisticsStore = defineStore('statistics', () => { }); } + function loadAssetTrends({ force }: { force: boolean }): Promise { + return new Promise((resolve, reject) => { + services.getTransactionStatisticsAssetTrends({ + startTime: transactionStatisticsFilter.value.assetTrendsChartStartTime, + endTime: transactionStatisticsFilter.value.assetTrendsChartEndTime + }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to retrieve transaction statistics' }); + return; + } + + if (transactionStatisticsStateInvalid.value) { + updateTransactionStatisticsInvalidState(false); + } + + if (force && data.result && isEquals(transactionAssetTrendsData.value, data.result)) { + reject({ message: 'Data is up to date', isUpToDate: true }); + return; + } + + transactionAssetTrendsData.value = data.result; + + resolve(data.result); + }).catch(error => { + logger.error('failed to retrieve transaction statistics', 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 transaction statistics' }); + } else { + reject(error); + } + }); + }); + } + return { // states transactionStatisticsFilter, @@ -1571,6 +1965,7 @@ export const useStatisticsStore = defineStore('statistics', () => { categoricalOverviewAnalysisData, categoricalAnalysisData, trendsAnalysisData, + assetTrendsData, // functions updateTransactionStatisticsInvalidState, resetTransactionStatistics, @@ -1579,6 +1974,7 @@ export const useStatisticsStore = defineStore('statistics', () => { getTransactionStatisticsPageParams, getTransactionListPageParams, loadCategoricalAnalysis, - loadTrendAnalysis + loadTrendAnalysis, + loadAssetTrends }; }); diff --git a/src/views/base/accounts/ReconciliationStatementPageBase.ts b/src/views/base/accounts/ReconciliationStatementPageBase.ts index df1359ac..dffee850 100644 --- a/src/views/base/accounts/ReconciliationStatementPageBase.ts +++ b/src/views/base/accounts/ReconciliationStatementPageBase.ts @@ -10,6 +10,7 @@ import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts'; import type { TypeAndDisplayName } from '@/core/base.ts'; import type { WeekDayValue } from '@/core/datetime.ts'; import { TransactionType } from '@/core/transaction.ts'; +import { StatisticsAnalysisType } from '@/core/statistics.ts'; import { KnownFileType } from '@/core/file.ts'; import type { Account } from '@/models/account.ts'; import type { TransactionCategory } from '@/models/transaction_category.ts'; @@ -55,7 +56,7 @@ export function useReconciliationStatementPageBase() { const defaultCurrency = computed(() => userStore.currentUserDefaultCurrency); const allChartTypes = computed(() => getAllAccountBalanceTrendChartTypes()); - const allDateAggregationTypes = computed(() => getAllStatisticsDateAggregationTypesWithShortName()); + const allDateAggregationTypes = computed(() => getAllStatisticsDateAggregationTypesWithShortName(StatisticsAnalysisType.AssetTrends)); const currentAccount = computed(() => allAccountsMap.value[accountId.value]); const currentAccountCurrency = computed(() => currentAccount.value?.currency ?? defaultCurrency.value); diff --git a/src/views/base/settings/AppCloudSyncPageBase.ts b/src/views/base/settings/AppCloudSyncPageBase.ts index 49f59023..a36a2f16 100644 --- a/src/views/base/settings/AppCloudSyncPageBase.ts +++ b/src/views/base/settings/AppCloudSyncPageBase.ts @@ -88,6 +88,14 @@ export const ALL_APPLICATION_CLOUD_SETTINGS: CategorizedApplicationCloudSettingI { settingKey: 'statistics.defaultTrendChartType', settingName: 'Default Chart Type', mobile: false, desktop: true }, { settingKey: 'statistics.defaultTrendChartDataRangeType', settingName: 'Default Date Range', mobile: true, desktop: true } ] + }, + { + categoryName: 'Statistics Settings', + categorySubName: 'Asset Trends Settings', + items: [ + { settingKey: 'statistics.defaultAssetTrendsChartType', settingName: 'Default Chart Type', mobile: false, desktop: true }, + { settingKey: 'statistics.defaultAssetTrendsChartDataRangeType', settingName: 'Default Date Range', mobile: true, desktop: true } + ] } ]; diff --git a/src/views/base/statistics/StatisticsSettingPageBase.ts b/src/views/base/statistics/StatisticsSettingPageBase.ts index fdee7c51..08f74084 100644 --- a/src/views/base/statistics/StatisticsSettingPageBase.ts +++ b/src/views/base/statistics/StatisticsSettingPageBase.ts @@ -27,6 +27,7 @@ export function useStatisticsSettingPageBase() { const allCategoricalChartDateRanges = computed(() => getAllDateRanges(DateRangeScene.Normal, false)); const allTrendChartTypes = computed(() => getAllTrendChartTypes()); const allTrendChartDateRanges = computed(() => getAllDateRanges(DateRangeScene.TrendAnalysis, false)); + const allAssetTrendsChartDateRanges = computed(() => getAllDateRanges(DateRangeScene.AssetTrends, false)); const defaultChartDataType = computed({ get: () => settingsStore.appSettings.statistics.defaultChartDataType, @@ -63,6 +64,16 @@ export function useStatisticsSettingPageBase() { set: (value: number) => settingsStore.setStatisticsDefaultTrendChartDateRange(value) }); + const defaultAssetTrendsChartType = computed({ + get: () => settingsStore.appSettings.statistics.defaultAssetTrendsChartType, + set: (value: number) => settingsStore.setStatisticsDefaultAssetTrendsChartType(value) + }); + + const defaultAssetTrendsChartDateRange = computed({ + get: () => settingsStore.appSettings.statistics.defaultAssetTrendsChartDataRangeType, + set: (value: number) => settingsStore.setStatisticsDefaultAssetTrendsChartDateRange(value) + }); + return { // computed states allChartDataTypes, @@ -72,12 +83,15 @@ export function useStatisticsSettingPageBase() { allCategoricalChartDateRanges, allTrendChartTypes, allTrendChartDateRanges, + allAssetTrendsChartDateRanges, defaultChartDataType, defaultTimezoneType, defaultSortingType, defaultCategoricalChartType, defaultCategoricalChartDateRange, defaultTrendChartType, - defaultTrendChartDateRange + defaultTrendChartDateRange, + defaultAssetTrendsChartType, + defaultAssetTrendsChartDateRange }; } diff --git a/src/views/base/statistics/StatisticsTransactionPageBase.ts b/src/views/base/statistics/StatisticsTransactionPageBase.ts index 297c6545..07ab10f2 100644 --- a/src/views/base/statistics/StatisticsTransactionPageBase.ts +++ b/src/views/base/statistics/StatisticsTransactionPageBase.ts @@ -23,7 +23,8 @@ import type { TransactionCategoricalOverviewAnalysisData, TransactionCategoricalAnalysisData, TransactionCategoricalAnalysisDataItem, - TransactionTrendsAnalysisData + TransactionTrendsAnalysisData, + TransactionAssetTrendsAnalysisData } from '@/models/transaction.ts'; import { limitText, findNameByType, findDisplayNameByType } from '@/lib/common.ts'; @@ -49,6 +50,7 @@ export function useStatisticsTransactionPageBase() { const loading = ref(true); const analysisType = ref(StatisticsAnalysisType.CategoricalAnalysis); const trendDateAggregationType = ref(ChartDateAggregationType.Default.type); + const assetTrendsDateAggregationType = ref(ChartDateAggregationType.Default.type); const showAccountBalance = computed(() => settingsStore.appSettings.showAccountBalance); const defaultCurrency = computed(() => userStore.currentUserDefaultCurrency); @@ -60,12 +62,15 @@ export function useStatisticsTransactionPageBase() { return getAllDateRanges(DateRangeScene.Normal, true); } else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) { return getAllDateRanges(DateRangeScene.TrendAnalysis, true); + } else if (analysisType.value === StatisticsAnalysisType.AssetTrends) { + return getAllDateRanges(DateRangeScene.AssetTrends, true); } else { return []; } }); const allSortingTypes = computed(() => getAllStatisticsSortingTypes()); - const allDateAggregationTypes = computed(() => getAllStatisticsDateAggregationTypes()); + const allTrendAnalysisDateAggregationTypes = computed(() => getAllStatisticsDateAggregationTypes(StatisticsAnalysisType.TrendAnalysis)); + const allAssetTrendsDateAggregationTypes = computed(() => getAllStatisticsDateAggregationTypes(StatisticsAnalysisType.AssetTrends)); const query = computed(() => statisticsStore.transactionStatisticsFilter); const queryChartDataCategory = computed(() => statisticsStore.categoricalAnalysisChartDataCategory); @@ -74,6 +79,8 @@ export function useStatisticsTransactionPageBase() { return query.value.categoricalChartDateType; } else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) { return query.value.trendChartDateType; + } else if (analysisType.value === StatisticsAnalysisType.AssetTrends) { + return query.value.assetTrendsChartDateType; } else { return null; } @@ -84,6 +91,8 @@ export function useStatisticsTransactionPageBase() { return formatUnixTimeToLongDateTime(query.value.categoricalChartStartTime); } else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) { return formatUnixTimeToGregorianLikeLongYearMonth(getYearMonthFirstUnixTime(query.value.trendChartStartYearMonth)); + } else if (analysisType.value === StatisticsAnalysisType.AssetTrends) { + return formatUnixTimeToLongDateTime(query.value.assetTrendsChartStartTime); } else { return ''; } @@ -94,21 +103,25 @@ export function useStatisticsTransactionPageBase() { return formatUnixTimeToLongDateTime(query.value.categoricalChartEndTime); } else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) { return formatUnixTimeToGregorianLikeLongYearMonth(getYearMonthLastUnixTime(query.value.trendChartEndYearMonth)); + } else if (analysisType.value === StatisticsAnalysisType.AssetTrends) { + return formatUnixTimeToLongDateTime(query.value.assetTrendsChartEndTime); } else { return ''; } }); const queryDateRangeName = computed(() => { - if (query.value.chartDataType === ChartDataType.AccountTotalAssets.type || - query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) { - return tt(DateRange.All.name); - } - if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) { + if (query.value.chartDataType === ChartDataType.AccountTotalAssets.type || + query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) { + return tt(DateRange.All.name); + } + return formatDateRange(query.value.categoricalChartDateType, query.value.categoricalChartStartTime, query.value.categoricalChartEndTime); } else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) { return formatDateRange(query.value.trendChartDateType, getYearMonthFirstUnixTime(query.value.trendChartStartYearMonth), getYearMonthLastUnixTime(query.value.trendChartEndYearMonth)); + } else if (analysisType.value === StatisticsAnalysisType.AssetTrends) { + return formatDateRange(query.value.assetTrendsChartDateType, query.value.assetTrendsChartStartTime, query.value.assetTrendsChartEndTime); } else { return ''; } @@ -124,7 +137,8 @@ export function useStatisticsTransactionPageBase() { return tt(querySortingTypeName); }); - const queryTrendDateAggregationTypeName = computed(() => findDisplayNameByType(allDateAggregationTypes.value, trendDateAggregationType.value) || ''); + const queryTrendDateAggregationTypeName = computed(() => findDisplayNameByType(allTrendAnalysisDateAggregationTypes.value, trendDateAggregationType.value) || ''); + const queryAssetTrendsDateAggregationTypeName = computed(() => findDisplayNameByType(allAssetTrendsDateAggregationTypes.value, assetTrendsDateAggregationType.value) || ''); const isQueryDateRangeChanged = computed(() => { if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) { @@ -144,13 +158,31 @@ export function useStatisticsTransactionPageBase() { } return !!query.value.trendChartStartYearMonth || !!query.value.trendChartEndYearMonth; + } else if (analysisType.value === StatisticsAnalysisType.AssetTrends) { + if (query.value.assetTrendsChartDateType === settingsStore.appSettings.statistics.defaultAssetTrendsChartDataRangeType) { + return false; + } + + return !!query.value.assetTrendsChartStartTime || !!query.value.assetTrendsChartEndTime; } else { return false; } }); + const canChangeDateRange = computed(() => { + if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) { + if (query.value.chartDataType === ChartDataType.AccountTotalAssets.type || query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) { + return false; + } + + return true; + } else { + return true; + } + }); + const canShiftDateRange = computed(() => { - if (query.value.chartDataType === ChartDataType.AccountTotalAssets.type || query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) { + if (!canChangeDateRange.value) { return false; } @@ -158,13 +190,31 @@ export function useStatisticsTransactionPageBase() { return query.value.categoricalChartDateType !== DateRange.All.type; } else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) { return query.value.trendChartDateType !== DateRange.All.type; + } else if (analysisType.value === StatisticsAnalysisType.AssetTrends) { + return query.value.assetTrendsChartDateType !== DateRange.All.type; } else { return false; } }); const canUseCategoryFilter = computed(() => { - if (query.value.chartDataType === ChartDataType.AccountTotalAssets.type || query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) { + if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) { + if (query.value.chartDataType === ChartDataType.AccountTotalAssets.type || query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) { + return false; + } + } else if (analysisType.value === StatisticsAnalysisType.AssetTrends) { + return false; + } + + return true; + }); + + const canUseServerCustomFilter = computed(() => { + if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) { + if (query.value.chartDataType === ChartDataType.AccountTotalAssets.type || query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) { + return false; + } + } else if (analysisType.value === StatisticsAnalysisType.AssetTrends) { return false; } @@ -172,25 +222,19 @@ export function useStatisticsTransactionPageBase() { }); const canUseTagFilter = computed(() => { - if (query.value.chartDataType === ChartDataType.AccountTotalAssets.type || query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) { - return false; - } - - return true; + return canUseServerCustomFilter.value; }); const canUseKeywordFilter = computed(() => { - if (query.value.chartDataType === ChartDataType.AccountTotalAssets.type || query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) { - return false; - } - - return true; + return canUseServerCustomFilter.value; }); const showAmountInChart = computed(() => { - if (!showAccountBalance.value - && (query.value.chartDataType === ChartDataType.AccountTotalAssets.type || query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type)) { - return false; + if (!showAccountBalance.value) { + if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis + && (query.value.chartDataType === ChartDataType.AccountTotalAssets.type || query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type)) { + return false; + } } return true; @@ -231,7 +275,8 @@ export function useStatisticsTransactionPageBase() { query.value.chartDataType !== ChartDataType.TotalInflows.type && query.value.chartDataType !== ChartDataType.TotalIncome.type && query.value.chartDataType !== ChartDataType.NetCashFlow.type && - query.value.chartDataType !== ChartDataType.NetIncome.type; + query.value.chartDataType !== ChartDataType.NetIncome.type && + query.value.chartDataType !== ChartDataType.NetWorth.type; }); const showStackedInTrendsChart = computed(() => { @@ -246,18 +291,22 @@ export function useStatisticsTransactionPageBase() { query.value.chartDataType === ChartDataType.TotalInflows.type || query.value.chartDataType === ChartDataType.TotalIncome.type || query.value.chartDataType === ChartDataType.NetCashFlow.type || - query.value.chartDataType === ChartDataType.NetIncome.type; + query.value.chartDataType === ChartDataType.NetIncome.type || + query.value.chartDataType === ChartDataType.NetWorth.type; }); const categoricalOverviewAnalysisData = computed(() => statisticsStore.categoricalOverviewAnalysisData); const categoricalAnalysisData = computed(() => statisticsStore.categoricalAnalysisData); const trendsAnalysisData = computed(() => statisticsStore.trendsAnalysisData); + const assetTrendsData = computed(() => statisticsStore.assetTrendsData); function canShowCustomDateRange(dateRangeType: number): boolean { if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) { return query.value.categoricalChartDateType === dateRangeType && !!query.value.categoricalChartStartTime && !!query.value.categoricalChartEndTime; } else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) { return query.value.trendChartDateType === dateRangeType && !!query.value.trendChartStartYearMonth && !!query.value.trendChartEndYearMonth; + } else if (analysisType.value === StatisticsAnalysisType.AssetTrends) { + return query.value.assetTrendsChartDateType === dateRangeType && !!query.value.assetTrendsChartStartTime && !!query.value.assetTrendsChartEndTime; } else { return false; } @@ -276,11 +325,11 @@ export function useStatisticsTransactionPageBase() { function getDisplayAmount(amount: number, currency: string, textLimit?: number): string { const finalAmount = formatAmountToLocalizedNumeralsWithCurrency(amount, currency); - if (!showAccountBalance.value - && (query.value.chartDataType === ChartDataType.AccountTotalAssets.type - || query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) - ) { - return DISPLAY_HIDDEN_AMOUNT; + if (!showAccountBalance.value) { + if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis + && (query.value.chartDataType === ChartDataType.AccountTotalAssets.type || query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type)) { + return DISPLAY_HIDDEN_AMOUNT; + } } if (textLimit) { @@ -295,6 +344,7 @@ export function useStatisticsTransactionPageBase() { loading, analysisType, trendDateAggregationType, + assetTrendsDateAggregationType, // computed states showAccountBalance, defaultCurrency, @@ -302,7 +352,8 @@ export function useStatisticsTransactionPageBase() { fiscalYearStart, allDateRanges, allSortingTypes, - allDateAggregationTypes, + allTrendAnalysisDateAggregationTypes, + allAssetTrendsDateAggregationTypes, query, queryChartDataCategory, queryDateType, @@ -312,7 +363,9 @@ export function useStatisticsTransactionPageBase() { queryChartDataTypeName, querySortingTypeName, queryTrendDateAggregationTypeName, + queryAssetTrendsDateAggregationTypeName, isQueryDateRangeChanged, + canChangeDateRange, canShiftDateRange, canUseCategoryFilter, canUseTagFilter, @@ -326,6 +379,7 @@ export function useStatisticsTransactionPageBase() { categoricalOverviewAnalysisData, categoricalAnalysisData, trendsAnalysisData, + assetTrendsData, // functions canShowCustomDateRange, getTransactionCategoricalAnalysisDataItemDisplayColor, diff --git a/src/views/desktop/accounts/list/dialogs/ReconciliationStatementDialog.vue b/src/views/desktop/accounts/list/dialogs/ReconciliationStatementDialog.vue index e1d4de44..ee3d7ad7 100644 --- a/src/views/desktop/accounts/list/dialogs/ReconciliationStatementDialog.vue +++ b/src/views/desktop/accounts/list/dialogs/ReconciliationStatementDialog.vue @@ -29,10 +29,6 @@ v-for="type in allChartTypes"> - (1); const countPerPage = ref(10); const showAccountBalanceTrendsCharts = ref(false); const chartType = ref(AccountBalanceTrendChartType.Default.type); -const chartDataDateAggregationType = ref(undefined); +const chartDataDateAggregationType = ref(ChartDateAggregationType.Day.type); let rejectFunc: ((reason?: unknown) => void) | null = null; @@ -453,7 +450,7 @@ function open(options: { accountId: string, startTime: number, endTime: number } countPerPage.value = 10; showAccountBalanceTrendsCharts.value = false; chartType.value = AccountBalanceTrendChartType.Default.type; - chartDataDateAggregationType.value = undefined; + chartDataDateAggregationType.value = ChartDateAggregationType.Day.type; showState.value = true; loading.value = true; diff --git a/src/views/desktop/app/settings/tabs/AppStatisticsSettingTab.vue b/src/views/desktop/app/settings/tabs/AppStatisticsSettingTab.vue index ca565ee9..3a329a59 100644 --- a/src/views/desktop/app/settings/tabs/AppStatisticsSettingTab.vue +++ b/src/views/desktop/app/settings/tabs/AppStatisticsSettingTab.vue @@ -114,6 +114,40 @@ + + + + + + + + + + + + + + + + + + @@ -140,13 +174,16 @@ const { allCategoricalChartDateRanges, allTrendChartTypes, allTrendChartDateRanges, + allAssetTrendsChartDateRanges, defaultChartDataType, defaultTimezoneType, defaultSortingType, defaultCategoricalChartType, defaultCategoricalChartDateRange, defaultTrendChartType, - defaultTrendChartDateRange + defaultTrendChartDateRange, + defaultAssetTrendsChartType, + defaultAssetTrendsChartDateRange } = useStatisticsSettingPageBase(); diff --git a/src/views/desktop/statistics/TransactionPage.vue b/src/views/desktop/statistics/TransactionPage.vue index 43e699b1..f66ca4a9 100644 --- a/src/views/desktop/statistics/TransactionPage.vue +++ b/src/views/desktop/statistics/TransactionPage.vue @@ -7,7 +7,8 @@
@@ -59,7 +60,7 @@ - +