add asset trends in statistics & analysis (#314)

This commit is contained in:
MaysWind
2025-11-09 22:51:46 +08:00
parent d3abb279e3
commit 4c8bb5a0b7
52 changed files with 1917 additions and 266 deletions

View File

@@ -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))

View File

@@ -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

View File

@@ -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

View File

@@ -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{

View File

@@ -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

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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

View File

@@ -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)) {

View File

@@ -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<YearMonthDay>;
DateTimeRangeType: number;
MonthRangeType: undefined;
};
monthly: {
ItemsType: YearMonthItems<Year1BasedMonth>;
DateTimeRangeType: undefined;
MonthRangeType: TextualYearMonth | '';
};
}
export interface CommonTrendsChartProps<T extends TrendsChartDateType> {
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<T extends TrendsChartDateType>(props: CommonTrendsChartProps<T>) {
const { tt } = useI18n();
const allDateRanges = computed<YearUnixTime[] | FiscalYearUnixTime[] | YearQuarterUnixTime[] | YearMonthUnixTime[] | YearMonthDayUnixTime[]>(() => {
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
};
}

View File

@@ -263,7 +263,7 @@ function onLegendSelectChanged(e: { selected: Record<string, boolean> }): void {
@media (min-width: 600px) {
.pie-chart-container {
height: 610px;
height: 650px;
}
}

View File

@@ -193,7 +193,7 @@ const chartOptions = computed<object>(() => {
@media (min-width: 600px) {
.radar-chart-container {
height: 610px;
height: 650px;
}
}

View File

@@ -1,5 +1,5 @@
<template>
<v-chart autoresize class="monthly-trends-chart-container" :class="{ 'transition-in': skeleton }" :option="chartOptions"
<v-chart autoresize class="trends-chart-container" :class="{ 'transition-in': skeleton }" :option="chartOptions"
@click="clickItem" @legendselectchanged="onLegendSelectChanged" />
</template>
@@ -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<T extends Year1BasedMonth> extends CommonMonthlyTrendsChartProps<T> {
interface DesktopTrendsChartProps<T extends TrendsChartDateType> extends CommonTrendsChartProps<T> {
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<DesktopMonthlyTrendsChartProps<YearMonthDataItem>>();
const props = defineProps<DesktopTrendsChartProps<TrendsChartDateType>>();
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<string[]>(() => {
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<MonthlyTrendsChartDataItem[]>(() => {
const allSeries: MonthlyTrendsChartDataItem[] = [];
const allSeries = computed<TrendsChartDataItem[]>(() => {
const allSeries: TrendsChartDataItem[] = [];
let maxAmount: number = 0;
for (const [item, index] of itemAndIndex(props.items)) {
@@ -161,23 +177,41 @@ const allSeries = computed<MonthlyTrendsChartDataItem[]>(() => {
}
const allAmounts: number[] = [];
const dateRangeAmountMap: Record<string, YearMonthDataItem[]> = {};
const dateRangeAmountMap: Record<string, (Year1BasedMonth | YearMonthDay)[]> = {};
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<MonthlyTrendsChartDataItem[]>(() => {
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<MonthlyTrendsChartDataItem[]>(() => {
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<string, unknown>)[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<MonthlyTrendsChartDataItem[]>(() => {
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<object>(() => {
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({
</script>
<style scoped>
.monthly-trends-chart-container {
.trends-chart-container {
width: 100%;
height: 720px;
margin-top: 10px;
}
@media (min-width: 600px) {
.monthly-trends-chart-container {
height: 760px;
.trends-chart-container {
height: 790px;
}
}
</style>

View File

@@ -30,23 +30,31 @@
<f7-list-item :title="tt('No transaction data')"></f7-list-item>
</f7-list>
<f7-list v-else-if="!loading && allDisplayDataItems && allDisplayDataItems.data && allDisplayDataItems.data.length">
<f7-list v-if="!loading && allDisplayDataItems && allDisplayDataItems.data && allDisplayDataItems.data.length">
<f7-list-item v-if="allDisplayDataItems.legends && allDisplayDataItems.legends.length > 1">
<div class="display-flex" style="flex-wrap: wrap">
<div class="monthly-trends-bar-chart-legend display-flex align-items-center"
:class="{ 'monthly-trends-bar-chart-legend-unselected': !!unselectedLegends[legend.id] }"
<div class="trends-bar-chart-legend display-flex align-items-center"
:class="{ 'trends-bar-chart-legend-unselected': !!unselectedLegends[legend.id] }"
:key="idx"
v-for="(legend, idx) in allDisplayDataItems.legends"
@click="toggleLegend(legend)">
<f7-icon f7="app_fill" class="monthly-trends-bar-chart-legend-icon" :style="{ 'color': unselectedLegends[legend.id] ? '' : legend.color }"></f7-icon>
<span class="monthly-trends-bar-chart-legend-text">{{ legend.name }}</span>
<f7-icon f7="app_fill" class="trends-bar-chart-legend-icon" :style="{ 'color': unselectedLegends[legend.id] ? '' : legend.color }"></f7-icon>
<span class="trends-bar-chart-legend-text">{{ legend.name }}</span>
</div>
</div>
</f7-list-item>
</f7-list>
<f7-list :key="`trends-bar-chart-${allDisplayDataItemsVersion}`"
:virtual-list="useVirtualList"
:virtual-list-params="useVirtualList ? { items: allDisplayDataItems.data, renderExternal, height: 'auto' } : undefined"
v-if="!loading && allDisplayDataItems && allDisplayDataItems.data && allDisplayDataItems.data.length">
<f7-list-item link="#"
:key="idx"
:key="item.index"
:class="{ 'statistics-list-item': true, 'statistics-list-item-stacked': stacked, 'statistics-list-item-non-stacked': !stacked }"
v-for="(item, idx) in allDisplayDataItems.data"
:style="useVirtualList ? `top: ${virtualDataItems.topPosition}px` : undefined"
:virtual-list-index="item.index"
v-for="item in (useVirtualList ? virtualDataItems.items : allDisplayDataItems.data)"
@click="clickItem(item)"
>
<template #media>
@@ -105,21 +113,32 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { ref, computed, watch } from 'vue';
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 { type Year1BasedMonth, type UnixTimeRange, DateRangeScene } from '@/core/datetime.ts';
import {
type UnixTimeRange,
DateRangeScene
} from '@/core/datetime.ts';
import type { ColorStyleValue } from '@/core/color.ts';
import { ChartDateAggregationType } from '@/core/statistics.ts';
import {
ChartDataAggregationType,
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 {
isNumber
@@ -144,37 +163,44 @@ interface TrendsBarChartLegend {
readonly displayOrders: number[];
}
interface MonthlyTrendsBarChartDataAmount extends SortableTransactionStatisticDataItem, TrendsBarChartLegend {
interface TrendsBarChartDataAmount extends SortableTransactionStatisticDataItem, TrendsBarChartLegend {
totalAmount: number;
}
interface MonthlyTrendsBarChartDataItem {
interface TrendsBarChartDataItem {
index: number;
dateRange: UnixTimeRange;
displayDateRange: string;
items: MonthlyTrendsBarChartDataAmount[];
items: TrendsBarChartDataAmount[];
totalAmount: number;
totalPositiveAmount: number;
maxAmount: number;
percent: number;
}
interface MonthlyTrendsBarChartData {
readonly data: MonthlyTrendsBarChartDataItem[];
interface TrendsBarChartVirtualListData {
items: TrendsBarChartDataItem[],
topPosition: number
}
interface TrendsBarChartData {
readonly data: TrendsBarChartDataItem[];
readonly legends: TrendsBarChartLegend[];
}
interface MobileMonthlyTrendsChartProps<T extends Year1BasedMonth> extends CommonMonthlyTrendsChartProps<T> {
interface MobileTrendsChartProps<T extends TrendsChartDateType> extends CommonTrendsChartProps<T> {
loading?: boolean;
}
const props = defineProps<MobileMonthlyTrendsChartProps<YearMonthDataItem>>();
const props = defineProps<MobileTrendsChartProps<TrendsChartDateType>>();
const emit = defineEmits<{
(e: 'click', value: MonthlyTrendsBarChartClickEvent): void;
(e: 'click', value: TrendsBarChartClickEvent): void;
}>();
const {
tt,
formatUnixTimeToShortDate,
formatUnixTimeToGregorianLikeShortYear,
formatUnixTimeToGregorianLikeShortYearMonth,
formatYearQuarterToGregorianLikeYearQuarter,
@@ -182,14 +208,22 @@ const {
formatAmountToLocalizedNumeralsWithCurrency
} = useI18n();
const { allDateRanges, getItemName } = useMonthlyTrendsChartBase(props);
const { allDateRanges, getItemName } = useTrendsChartBase(props);
const userStore = useUserStore();
const allDisplayDataItemsVersion = ref<number>(0);
const unselectedLegends = ref<Record<string, boolean>>({});
const allDisplayDataItems = computed<MonthlyTrendsBarChartData>(() => {
const allDateRangeItemsMap: Record<string, MonthlyTrendsBarChartDataAmount[]> = {};
const virtualDataItems = ref<TrendsBarChartVirtualListData>({
items: [],
topPosition: 0
});
const useVirtualList = computed<boolean>(() => allDisplayDataItems.value.legends.length <= 1 || props.stacked);
const allDisplayDataItems = computed<TrendsBarChartData>(() => {
const allDateRangeItemsMap: Record<string, TrendsBarChartDataAmount[]> = {};
const legends: TrendsBarChartLegend[] = [];
for (const [item, index] of itemAndIndex(props.items)) {
@@ -212,31 +246,57 @@ const allDisplayDataItems = computed<MonthlyTrendsBarChartData>(() => {
continue;
}
const dateRangeItemMap: Record<string, MonthlyTrendsBarChartDataAmount> = {};
const dateRangeItemMap: Record<string, TrendsBarChartDataAmount> = {};
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 value = (dataItem as unknown as Record<string, unknown>)[props.valueField];
if (dateRangeItemMap[dateRangeKey]) {
dateRangeItemMap[dateRangeKey]!.totalAmount += (props.valueField && isNumber(dataItem[props.valueField])) ? dataItem[props.valueField] as number : 0;
if (isNumber(value)) {
if (props.dataAggregationType === ChartDataAggregationType.Sum) {
dateRangeItemMap[dateRangeKey]!.totalAmount += value;
} else if (props.dataAggregationType === ChartDataAggregationType.Last) {
dateRangeItemMap[dateRangeKey]!.totalAmount = value;
}
}
} else {
const allDataItems: MonthlyTrendsBarChartDataAmount[] = allDateRangeItemsMap[dateRangeKey] || [];
const finalDataItem: MonthlyTrendsBarChartDataAmount = Object.assign({}, legend, {
totalAmount: (props.valueField && isNumber(dataItem[props.valueField])) ? dataItem[props.valueField] as number : 0
const allDataItems: TrendsBarChartDataAmount[] = allDateRangeItemsMap[dateRangeKey] || [];
const finalDataItem: TrendsBarChartDataAmount = Object.assign({}, legend, {
totalAmount: isNumber(value) ? value : 0
});
allDataItems.push(finalDataItem);
@@ -246,7 +306,7 @@ const allDisplayDataItems = computed<MonthlyTrendsBarChartData>(() => {
}
}
const finalDataItems: MonthlyTrendsBarChartDataItem[] = [];
const finalDataItems: TrendsBarChartDataItem[] = [];
let maxTotalAmount = 0;
for (const dateRange of allDateRanges.value) {
@@ -260,6 +320,8 @@ const allDisplayDataItems = computed<MonthlyTrendsBarChartData>(() => {
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 displayDateRange = '';
@@ -270,8 +332,10 @@ const allDisplayDataItems = computed<MonthlyTrendsBarChartData>(() => {
displayDateRange = formatUnixTimeToGregorianLikeFiscalYear(dateRange.minUnixTime);
} else if (props.dateAggregationType === ChartDateAggregationType.Quarter.type && 'quarter' in dateRange) {
displayDateRange = formatYearQuarterToGregorianLikeYearQuarter(dateRange.year, dateRange.quarter);
} else { // if (props.dateAggregationType === ChartDateAggregationType.Month.type) {
} else if (props.dateAggregationType === ChartDateAggregationType.Month.type) {
displayDateRange = formatUnixTimeToGregorianLikeShortYearMonth(dateRange.minUnixTime);
} else if (props.dateAggregationType === ChartDateAggregationType.Day.type && props.chartMode === 'daily') {
displayDateRange = formatUnixTimeToShortDate(dateRange.minUnixTime);
}
const dataItems = allDateRangeItemsMap[dateRangeKey] || [];
@@ -297,7 +361,8 @@ const allDisplayDataItems = computed<MonthlyTrendsBarChartData>(() => {
maxTotalAmount = totalAmount;
}
const finalDataItem: MonthlyTrendsBarChartDataItem = {
const finalDataItem: TrendsBarChartDataItem = {
index: finalDataItems.length,
dateRange: dateRange,
displayDateRange: displayDateRange,
items: dataItems,
@@ -324,7 +389,7 @@ const allDisplayDataItems = computed<MonthlyTrendsBarChartData>(() => {
};
});
function clickItem(item: MonthlyTrendsBarChartDataItem): void {
function clickItem(item: TrendsBarChartDataItem): void {
let itemId = '';
for (const item of props.items) {
@@ -349,19 +414,33 @@ function clickItem(item: MonthlyTrendsBarChartDataItem): 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;
}
}
}
@@ -384,28 +463,38 @@ function toggleLegend(legend: TrendsBarChartLegend): void {
unselectedLegends.value[legend.id] = true;
}
}
function renderExternal(vl: unknown, vlData: TrendsBarChartVirtualListData): void {
virtualDataItems.value = vlData;
}
watch(allDisplayDataItems, () => {
allDisplayDataItemsVersion.value++;
}, {
deep: true
});
</script>
<style>
.monthly-trends-bar-chart-legend {
.trends-bar-chart-legend {
margin-inline-end: 4px;
cursor: pointer;
}
.monthly-trends-bar-chart-legend-icon.f7-icons {
.trends-bar-chart-legend-icon.f7-icons {
font-size: var(--ebk-trends-bar-chart-legend-icon-font-size);
margin-inline-end: 2px;
}
.monthly-trends-bar-chart-legend-unselected .monthly-trends-bar-chart-legend-icon.f7-icons {
.trends-bar-chart-legend-unselected .trends-bar-chart-legend-icon.f7-icons {
color: #cccccc;
}
.monthly-trends-bar-chart-legend-text {
.trends-bar-chart-legend-text {
font-size: var(--ebk-trends-bar-chart-legend-text-font-size);
}
.monthly-trends-bar-chart-legend-unselected .monthly-trends-bar-chart-legend-text {
.trends-bar-chart-legend-unselected .trends-bar-chart-legend-text {
color: #cccccc;
}
</style>

View File

@@ -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<number, DateRange> = {};
// 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;

View File

@@ -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<string, UserAp
'statistics.defaultCategoricalChartDataRangeType': UserApplicationCloudSettingType.Number,
'statistics.defaultTrendChartType': UserApplicationCloudSettingType.Number,
'statistics.defaultTrendChartDataRangeType': UserApplicationCloudSettingType.Number,
'statistics.defaultAssetTrendsChartType': UserApplicationCloudSettingType.Number,
'statistics.defaultAssetTrendsChartDataRangeType': UserApplicationCloudSettingType.Number,
};
export const DEFAULT_APPLICATION_SETTINGS: ApplicationSettings = {
@@ -168,6 +173,8 @@ export const DEFAULT_APPLICATION_SETTINGS: ApplicationSettings = {
defaultCategoricalChartDataRangeType: DEFAULT_CATEGORICAL_CHART_DATA_RANGE.type,
defaultTrendChartType: TrendChartType.Default.type,
defaultTrendChartDataRangeType: DEFAULT_TREND_CHART_DATA_RANGE.type,
defaultAssetTrendsChartType: TrendChartType.Default.type,
defaultAssetTrendsChartDataRangeType: DEFAULT_ASSET_TRENDS_CHART_DATA_RANGE.type,
}
};

View File

@@ -3,7 +3,13 @@ import { DateRange } from '@/core/datetime.ts';
export enum StatisticsAnalysisType {
CategoricalAnalysis = 0,
TrendAnalysis = 1
TrendAnalysis = 1,
AssetTrends = 2
}
export enum ChartDataAggregationType {
Sum = 0,
Last = 1
}
export class CategoricalChartType implements TypeAndName {
@@ -115,16 +121,18 @@ export class ChartDataType implements TypeAndName {
public static readonly IncomeByAccount = new ChartDataType(3, 'Income By Account', false, false, StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis);
public static readonly IncomeByPrimaryCategory = new ChartDataType(4, 'Income By Primary Category', false, false, StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis);
public static readonly IncomeBySecondaryCategory = new ChartDataType(5, 'Income By Secondary Category', false, false, StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis);
public static readonly AccountTotalAssets = new ChartDataType(6, 'Account Total Assets', false, false, StatisticsAnalysisType.CategoricalAnalysis);
public static readonly AccountTotalLiabilities = new ChartDataType(7, 'Account Total Liabilities', false, false, StatisticsAnalysisType.CategoricalAnalysis);
public static readonly AccountTotalAssets = new ChartDataType(6, 'Account Total Assets', false, false, StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.AssetTrends);
public static readonly AccountTotalLiabilities = new ChartDataType(7, 'Account Total Liabilities', false, false, StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.AssetTrends);
public static readonly TotalOutflows = new ChartDataType(13, 'Total Outflows', false, false, StatisticsAnalysisType.TrendAnalysis);
public static readonly TotalExpense = new ChartDataType(8, 'Total Expense', false, false, StatisticsAnalysisType.TrendAnalysis);
public static readonly TotalInflows = new ChartDataType(14, 'Total Inflows', false, false, StatisticsAnalysisType.TrendAnalysis);
public static readonly TotalIncome = new ChartDataType(9, 'Total Income', false, false, StatisticsAnalysisType.TrendAnalysis);
public static readonly NetCashFlow = new ChartDataType(15, 'Net Cash Flow', false, false, StatisticsAnalysisType.TrendAnalysis);
public static readonly NetIncome = new ChartDataType(10, 'Net Income', false, false, StatisticsAnalysisType.TrendAnalysis);
public static readonly NetWorth = new ChartDataType(17, 'Net Worth', false, false, StatisticsAnalysisType.AssetTrends);
public static readonly Default = ChartDataType.ExpenseByPrimaryCategory;
public static readonly DefaultForAssetTrends = ChartDataType.NetWorth;
public readonly type: number;
public readonly name: string;
@@ -221,28 +229,55 @@ export class ChartDateAggregationType {
private static readonly allInstances: ChartDateAggregationType[] = [];
private static readonly allInstancesByType: Record<number, ChartDateAggregationType> = {};
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<number, boolean>;
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;

View File

@@ -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);

View File

@@ -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(),

View File

@@ -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<ApiResponse<TransactionStatisticTrendsResponseItem[]>>(`v1/transactions/statistics/trends.json?use_transaction_timezone=${req.useTransactionTimezone}` + (queryParams.length ? '&' + queryParams.join('&') : ''));
},
getTransactionStatisticsAssetTrends: (req: TransactionStatisticAssetTrendsRequest): ApiResponsePromise<TransactionStatisticAssetTrendsResponseItem[]> => {
const queryParams = [];
if (req.startTime) {
queryParams.push(`start_time=${req.startTime}`);
}
if (req.endTime) {
queryParams.push(`end_time=${req.endTime}`);
}
return axios.get<ApiResponse<TransactionStatisticAssetTrendsResponseItem[]>>('v1/transactions/statistics/asset_trends.json' + (queryParams.length ? '?' + queryParams.join('&') : ''));
},
getTransactionAmounts: (params: TransactionAmountsRequestParams, excludeAccountIds: string[], excludeCategoryIds: string[]): ApiResponsePromise<TransactionAmountsResponse> => {
const req = TransactionAmountsRequest.of(params);
let queryParams = req.buildQuery();

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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()),

View File

@@ -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",

View File

@@ -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": "年ごとに集計",

View File

@@ -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": "연도별 집계",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": "Агрегировать по годам",

View File

@@ -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": "รวมตามปี",

View File

@@ -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": "Агрегувати за роками",

View File

@@ -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",

View File

@@ -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": "按年聚合",

View File

@@ -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": "依年份彙整",

View File

@@ -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);

View File

@@ -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<string, unknown> {}
export interface YearMonthDayDataItem extends YearMonthDay, Record<string, unknown> {}
export interface YearMonthItems<T extends Year1BasedMonth> extends Record<string, unknown> {
readonly items: T[];
}
export interface YearMonthDayItems<T extends YearMonthDay> extends Record<string, unknown> {
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<string, unkn
readonly totalAmount: number;
}
export interface TransactionAssetTrendsAnalysisData {
readonly items: TransactionAssetTrendsAnalysisDataItem[];
}
export interface TransactionAssetTrendsAnalysisDataItem extends Record<string, unknown>, TransactionStatisticDataItemBase {
readonly items: TransactionAssetTrendsAnalysisDataAmount[];
}
export interface TransactionAssetTrendsAnalysisDataAmount extends Record<string, unknown>, YearMonthDay {
readonly year: number;
readonly month: number;
readonly day: number;
readonly totalAmount: number;
}
export type TransactionAmountsResponse = PartialRecord<TransactionAmountsRequestType, TransactionAmountsResponseItem>;
export interface TransactionAmountsResponseItem {

View File

@@ -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']
})
},
{

View File

@@ -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,

View File

@@ -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<string,
percent?: number;
}
interface WritableTransactionTrendsAnalysisDataItem extends Record<string, unknown> {
interface WritableTransactionTrendsAnalysisDataItem extends Record<string, unknown>, TransactionTrendsAnalysisDataItem {
name: string;
type: TransactionStatisticDataItemType;
id: string;
@@ -95,6 +107,18 @@ interface WritableTransactionTrendsAnalysisDataItem extends Record<string, unkno
items: TransactionTrendsAnalysisDataAmount[];
}
interface WritableTransactionAssetTrendsAnalysisDataItem extends Record<string, unknown>, 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<string, boolean>;
filterCategoryIds?: Record<string, boolean>;
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<string, boolean>;
filterCategoryIds: Record<string, boolean>;
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<TransactionStatisticResponse | null>(null);
const transactionCategoryTrendsData = ref<TransactionStatisticTrendsResponseItem[]>([]);
const transactionAssetTrendsData = ref<TransactionStatisticAssetTrendsResponseItem[]>([]);
const transactionStatisticsStateInvalid = ref<boolean>(true);
const categoricalAnalysisChartDataCategory = computed<string>(() => {
@@ -709,6 +746,227 @@ export const useStatisticsStore = defineStore('statistics', () => {
return trendsData;
});
const assetTrendsDataWithAccountInfo = computed<TransactionStatisticAssetTrendsResponseItemWithInfo[]>(() => {
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<string, TransactionStatisticAssetTrendsResponseDataItem> = {};
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<string, boolean> = {};
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<TransactionAssetTrendsAnalysisData | null>(() => {
if (!assetTrendsDataWithAccountInfo.value || !assetTrendsDataWithAccountInfo.value.length) {
return null;
}
const combinedDataMap: Record<string, WritableTransactionAssetTrendsAnalysisDataItem> = {};
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<TransactionStatisticAssetTrendsResponseItem[]> {
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
};
});

View File

@@ -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<string>(() => userStore.currentUserDefaultCurrency);
const allChartTypes = computed<TypeAndDisplayName[]>(() => getAllAccountBalanceTrendChartTypes());
const allDateAggregationTypes = computed<TypeAndDisplayName[]>(() => getAllStatisticsDateAggregationTypesWithShortName());
const allDateAggregationTypes = computed<TypeAndDisplayName[]>(() => getAllStatisticsDateAggregationTypesWithShortName(StatisticsAnalysisType.AssetTrends));
const currentAccount = computed(() => allAccountsMap.value[accountId.value]);
const currentAccountCurrency = computed<string>(() => currentAccount.value?.currency ?? defaultCurrency.value);

View File

@@ -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 }
]
}
];

View File

@@ -27,6 +27,7 @@ export function useStatisticsSettingPageBase() {
const allCategoricalChartDateRanges = computed<LocalizedDateRange[]>(() => getAllDateRanges(DateRangeScene.Normal, false));
const allTrendChartTypes = computed<TypeAndDisplayName[]>(() => getAllTrendChartTypes());
const allTrendChartDateRanges = computed<LocalizedDateRange[]>(() => getAllDateRanges(DateRangeScene.TrendAnalysis, false));
const allAssetTrendsChartDateRanges = computed<LocalizedDateRange[]>(() => getAllDateRanges(DateRangeScene.AssetTrends, false));
const defaultChartDataType = computed<number>({
get: () => settingsStore.appSettings.statistics.defaultChartDataType,
@@ -63,6 +64,16 @@ export function useStatisticsSettingPageBase() {
set: (value: number) => settingsStore.setStatisticsDefaultTrendChartDateRange(value)
});
const defaultAssetTrendsChartType = computed<number>({
get: () => settingsStore.appSettings.statistics.defaultAssetTrendsChartType,
set: (value: number) => settingsStore.setStatisticsDefaultAssetTrendsChartType(value)
});
const defaultAssetTrendsChartDateRange = computed<number>({
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
};
}

View File

@@ -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<boolean>(true);
const analysisType = ref<StatisticsAnalysisType>(StatisticsAnalysisType.CategoricalAnalysis);
const trendDateAggregationType = ref<number>(ChartDateAggregationType.Default.type);
const assetTrendsDateAggregationType = ref<number>(ChartDateAggregationType.Default.type);
const showAccountBalance = computed<boolean>(() => settingsStore.appSettings.showAccountBalance);
const defaultCurrency = computed<string>(() => 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<TypeAndDisplayName[]>(() => getAllStatisticsSortingTypes());
const allDateAggregationTypes = computed<TypeAndDisplayName[]>(() => getAllStatisticsDateAggregationTypes());
const allTrendAnalysisDateAggregationTypes = computed<TypeAndDisplayName[]>(() => getAllStatisticsDateAggregationTypes(StatisticsAnalysisType.TrendAnalysis));
const allAssetTrendsDateAggregationTypes = computed<TypeAndDisplayName[]>(() => getAllStatisticsDateAggregationTypes(StatisticsAnalysisType.AssetTrends));
const query = computed<TransactionStatisticsFilter>(() => statisticsStore.transactionStatisticsFilter);
const queryChartDataCategory = computed<string>(() => 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<string>(() => {
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<string>(() => findDisplayNameByType(allDateAggregationTypes.value, trendDateAggregationType.value) || '');
const queryTrendDateAggregationTypeName = computed<string>(() => findDisplayNameByType(allTrendAnalysisDateAggregationTypes.value, trendDateAggregationType.value) || '');
const queryAssetTrendsDateAggregationTypeName = computed<string>(() => findDisplayNameByType(allAssetTrendsDateAggregationTypes.value, assetTrendsDateAggregationType.value) || '');
const isQueryDateRangeChanged = computed<boolean>(() => {
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<boolean>(() => {
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<boolean>(() => {
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<boolean>(() => {
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<boolean>(() => {
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<boolean>(() => {
if (query.value.chartDataType === ChartDataType.AccountTotalAssets.type || query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) {
return false;
}
return true;
return canUseServerCustomFilter.value;
});
const canUseKeywordFilter = computed<boolean>(() => {
if (query.value.chartDataType === ChartDataType.AccountTotalAssets.type || query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) {
return false;
}
return true;
return canUseServerCustomFilter.value;
});
const showAmountInChart = computed<boolean>(() => {
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<boolean>(() => {
@@ -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<TransactionCategoricalOverviewAnalysisData | null>(() => statisticsStore.categoricalOverviewAnalysisData);
const categoricalAnalysisData = computed<TransactionCategoricalAnalysisData>(() => statisticsStore.categoricalAnalysisData);
const trendsAnalysisData = computed<TransactionTrendsAnalysisData | null>(() => statisticsStore.trendsAnalysisData);
const assetTrendsData = computed<TransactionAssetTrendsAnalysisData | null>(() => 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,

View File

@@ -29,10 +29,6 @@
v-for="type in allChartTypes"></v-list-item>
<v-divider class="my-2"/>
<v-list-subheader :title="tt('Time Granularity')"/>
<v-list-item :prepend-icon="mdiCalendarTodayOutline"
:append-icon="chartDataDateAggregationType === undefined ? mdiCheck : undefined"
:title="tt('granularity.Daily')"
@click="chartDataDateAggregationType = undefined"></v-list-item>
<v-list-item :key="dateAggregationType.type"
:prepend-icon="chartDataDateAggregationTypeIconMap[dateAggregationType.type]"
:append-icon="chartDataDateAggregationType === dateAggregationType.type ? mdiCheck : undefined"
@@ -360,6 +356,7 @@ const chartTypeIconMap = {
};
const chartDataDateAggregationTypeIconMap = {
[ChartDateAggregationType.Day.type]: mdiCalendarTodayOutline,
[ChartDateAggregationType.Month.type]: mdiCalendarMonthOutline,
[ChartDateAggregationType.Quarter.type]: mdiLayersTripleOutline,
[ChartDateAggregationType.Year.type]: mdiLayersTripleOutline,
@@ -376,7 +373,7 @@ const currentPage = ref<number>(1);
const countPerPage = ref<number>(10);
const showAccountBalanceTrendsCharts = ref<boolean>(false);
const chartType = ref<number>(AccountBalanceTrendChartType.Default.type);
const chartDataDateAggregationType = ref<number | undefined>(undefined);
const chartDataDateAggregationType = ref<number>(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;

View File

@@ -114,6 +114,40 @@
</v-card>
</v-col>
<v-col cols="12">
<v-card :title="tt('Asset Trends Settings')">
<v-form>
<v-card-text>
<v-row>
<v-col cols="12" md="6">
<v-select
item-title="displayName"
item-value="type"
persistent-placeholder
:label="tt('Default Chart Type')"
:placeholder="tt('Default Chart Type')"
:items="allTrendChartTypes"
v-model="defaultAssetTrendsChartType"
/>
</v-col>
<v-col cols="12" md="6">
<v-select
item-title="displayName"
item-value="type"
persistent-placeholder
:label="tt('Default Date Range')"
:placeholder="tt('Default Date Range')"
:items="allAssetTrendsChartDateRanges"
v-model="defaultAssetTrendsChartDateRange"
/>
</v-col>
</v-row>
</v-card-text>
</v-form>
</v-card>
</v-col>
<v-col cols="12">
<account-filter-settings-card type="statisticsDefault" :auto-save="true" />
</v-col>
@@ -140,13 +174,16 @@ const {
allCategoricalChartDateRanges,
allTrendChartTypes,
allTrendChartDateRanges,
allAssetTrendsChartDateRanges,
defaultChartDataType,
defaultTimezoneType,
defaultSortingType,
defaultCategoricalChartType,
defaultCategoricalChartDateRange,
defaultTrendChartType,
defaultTrendChartDateRange
defaultTrendChartDateRange,
defaultAssetTrendsChartType,
defaultAssetTrendsChartDateRange
} = useStatisticsSettingPageBase();
</script>

View File

@@ -7,7 +7,8 @@
<div class="mx-6 my-4">
<btn-vertical-group :disabled="loading" :buttons="[
{ name: tt('Categorical Analysis'), value: StatisticsAnalysisType.CategoricalAnalysis },
{ name: tt('Trend Analysis'), value: StatisticsAnalysisType.TrendAnalysis }
{ name: tt('Trend Analysis'), value: StatisticsAnalysisType.TrendAnalysis },
{ name: tt('Asset Trends'), value: StatisticsAnalysisType.AssetTrends }
]" v-model="queryAnalysisType" />
</div>
<v-divider />
@@ -59,7 +60,7 @@
<v-main>
<v-window class="d-flex flex-grow-1 disable-tab-transition w-100-window-container" v-model="activeTab">
<v-window-item value="statisticsPage">
<v-card variant="flat" :min-height="queryAnalysisType === StatisticsAnalysisType.TrendAnalysis ? '860' : '760'">
<v-card variant="flat" :min-height="queryAnalysisType === StatisticsAnalysisType.TrendAnalysis || queryAnalysisType === StatisticsAnalysisType.AssetTrends ? '900' : '800'">
<template #title>
<div class="title-and-toolbar d-flex align-center">
<v-btn class="me-3 d-md-none" density="compact" color="default" variant="plain"
@@ -73,7 +74,7 @@
@click="shiftDateRange(-1)"/>
<v-menu location="bottom">
<template #activator="{ props }">
<v-btn :disabled="loading || queryChartDataType === ChartDataType.AccountTotalAssets.type || queryChartDataType === ChartDataType.AccountTotalLiabilities.type"
<v-btn :disabled="loading || !canChangeDateRange"
v-bind="props">{{ queryDateRangeName }}</v-btn>
</template>
<v-list :selected="[queryDateType]">
@@ -110,12 +111,28 @@
<v-list-item class="cursor-pointer" :key="aggregationType.type" :value="aggregationType.type"
:append-icon="(trendDateAggregationType === aggregationType.type ? mdiCheck : undefined)"
:title="aggregationType.displayName"
v-for="aggregationType in allDateAggregationTypes"
v-for="aggregationType in allTrendAnalysisDateAggregationTypes"
@click="setTrendDateAggregationType(aggregationType.type)">
</v-list-item>
</v-list>
</v-menu>
<v-menu location="bottom" v-if="queryAnalysisType === StatisticsAnalysisType.AssetTrends">
<template #activator="{ props }">
<v-btn class="ms-3" color="default" variant="outlined"
:prepend-icon="mdiCalendarRangeOutline" :disabled="loading"
v-bind="props">{{ queryAssetTrendsDateAggregationTypeName }}</v-btn>
</template>
<v-list>
<v-list-item class="cursor-pointer" :key="aggregationType.type" :value="aggregationType.type"
:append-icon="(assetTrendsDateAggregationType === aggregationType.type ? mdiCheck : undefined)"
:title="aggregationType.displayName"
v-for="aggregationType in allAssetTrendsDateAggregationTypes"
@click="setAssetTrendsDateAggregationType(aggregationType.type)">
</v-list-item>
</v-list>
</v-menu>
<v-btn density="compact" color="default" variant="text" size="24"
class="ms-2" :icon="true" :loading="loading" @click="reload(true)">
<template #loader>
@@ -205,10 +222,11 @@
</v-card-text>
<v-card-text class="statistics-overview-title pt-0"
v-else-if="!initing && (
v-else-if="!loading && (
(queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && isQuerySpecialChartType && queryChartDataType === ChartDataType.Overview.type && (!categoricalOverviewAnalysisData || !categoricalOverviewAnalysisData.items || !categoricalOverviewAnalysisData.items.length))
|| (queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && !isQuerySpecialChartType && (!categoricalAnalysisData || !categoricalAnalysisData.items || !categoricalAnalysisData.items.length))
|| (queryAnalysisType === StatisticsAnalysisType.TrendAnalysis && (!trendsAnalysisData || !trendsAnalysisData.items || !trendsAnalysisData.items.length))
|| (queryAnalysisType === StatisticsAnalysisType.AssetTrends && (!assetTrendsData || !assetTrendsData.items || !assetTrendsData.items.length))
)">
<span class="statistics-subtitle statistics-overview-empty-tip">{{ tt('No transaction data') }}</span>
</v-card-text>
@@ -345,11 +363,15 @@
</v-card-text>
<v-card-text :class="{ 'readonly': loading }" v-if="queryAnalysisType === StatisticsAnalysisType.TrendAnalysis">
<monthly-trends-chart
<trends-chart
chart-mode="monthly"
:type="queryChartType"
:start-time="undefined"
:end-time="undefined"
:start-year-month="query.trendChartStartYearMonth"
:end-year-month="query.trendChartEndYearMonth"
:sorting-type="querySortingType"
:data-aggregation-type="ChartDataAggregationType.Sum"
:date-aggregation-type="trendDateAggregationType"
:fiscal-year-start="fiscalYearStart"
:items="[]"
@@ -360,11 +382,15 @@
color-field="color"
v-if="initing"
/>
<monthly-trends-chart
<trends-chart
chart-mode="monthly"
:type="queryChartType"
:start-time="undefined"
:end-time="undefined"
:start-year-month="query.trendChartStartYearMonth"
:end-year-month="query.trendChartEndYearMonth"
:sorting-type="querySortingType"
:data-aggregation-type="ChartDataAggregationType.Sum"
:date-aggregation-type="trendDateAggregationType"
:fiscal-year-start="fiscalYearStart"
:items="trendsAnalysisData && trendsAnalysisData.items && trendsAnalysisData.items.length ? trendsAnalysisData.items : []"
@@ -384,6 +410,55 @@
@click="onClickTrendChartItem"
/>
</v-card-text>
<v-card-text :class="{ 'readonly': loading }" v-if="queryAnalysisType === StatisticsAnalysisType.AssetTrends">
<trends-chart
chart-mode="daily"
:type="queryChartType"
:start-time="query.assetTrendsChartStartTime"
:end-time="query.assetTrendsChartEndTime"
:start-year-month="undefined"
:end-year-month="undefined"
:sorting-type="querySortingType"
:data-aggregation-type="ChartDataAggregationType.Last"
:date-aggregation-type="assetTrendsDateAggregationType"
:fiscal-year-start="fiscalYearStart"
:items="[]"
:skeleton="true"
id-field="id"
name-field="name"
value-field="value"
color-field="color"
v-if="initing"
/>
<trends-chart
chart-mode="daily"
:type="queryChartType"
:start-time="query.assetTrendsChartStartTime"
:end-time="query.assetTrendsChartEndTime"
:start-year-month="undefined"
:end-year-month="undefined"
:sorting-type="querySortingType"
:data-aggregation-type="ChartDataAggregationType.Last"
:date-aggregation-type="assetTrendsDateAggregationType"
:fiscal-year-start="fiscalYearStart"
:items="assetTrendsData && assetTrendsData.items && assetTrendsData.items.length ? assetTrendsData.items : []"
:translate-name="translateNameInTrendsChart"
:show-value="showAmountInChart"
:enable-click-item="true"
:default-currency="defaultCurrency"
:stacked="showStackedInTrendsChart"
:show-total-amount-in-tooltip="showTotalAmountInTrendsChart"
ref="dailyTrendsChart"
id-field="id"
name-field="name"
value-field="totalAmount"
hidden-field="hidden"
display-orders-field="displayOrders"
v-else-if="!initing && assetTrendsData && assetTrendsData.items && assetTrendsData.items.length"
@click="onClickTrendChartItem"
/>
</v-card-text>
</v-card>
</v-window-item>
</v-window>
@@ -429,7 +504,7 @@
<script setup lang="ts">
import SnackBar from '@/components/desktop/SnackBar.vue';
import MonthlyTrendsChart from '@/components/desktop/MonthlyTrendsChart.vue';
import TrendsChart from '@/components/desktop/TrendsChart.vue';
import AccountFilterSettingsCard from '@/views/desktop/common/cards/AccountFilterSettingsCard.vue';
import CategoryFilterSettingsCard from '@/views/desktop/common/cards/CategoryFilterSettingsCard.vue';
import TransactionTagFilterSettingsCard from '@/views/desktop/common/cards/TransactionTagFilterSettingsCard.vue';
@@ -450,6 +525,7 @@ import type { TypeAndDisplayName } from '@/core/base.ts';
import { type TextualYearMonth, type TimeRangeAndDateType, DateRangeScene, DateRange } from '@/core/datetime.ts';
import { ThemeType } from '@/core/theme.ts';
import {
ChartDataAggregationType,
StatisticsAnalysisType,
CategoricalChartType,
ChartDataType,
@@ -488,7 +564,7 @@ import {
} from '@mdi/js';
type SnackBarType = InstanceType<typeof SnackBar>;
type MonthlyTrendsChartType = InstanceType<typeof MonthlyTrendsChart>;
type TrendsChartType = InstanceType<typeof TrendsChart>;
type ExportDialogType = InstanceType<typeof ExportDialog>;
interface TransactionStatisticsProps {
@@ -505,6 +581,7 @@ interface TransactionStatisticsProps {
initKeyword?: string;
initSortingType?: string,
initTrendDateAggregationType?: string
initAssetTrendsDateAggregationType?: string
}
const props = defineProps<TransactionStatisticsProps>();
@@ -525,12 +602,14 @@ const {
loading,
analysisType,
trendDateAggregationType,
assetTrendsDateAggregationType,
defaultCurrency,
firstDayOfWeek,
fiscalYearStart,
allDateRanges,
allSortingTypes,
allDateAggregationTypes,
allTrendAnalysisDateAggregationTypes,
allAssetTrendsDateAggregationTypes,
query,
queryChartDataCategory,
queryDateType,
@@ -538,6 +617,8 @@ const {
queryEndTime,
queryDateRangeName,
queryTrendDateAggregationTypeName,
queryAssetTrendsDateAggregationTypeName,
canChangeDateRange,
canShiftDateRange,
canUseCategoryFilter,
canUseTagFilter,
@@ -551,6 +632,7 @@ const {
categoricalOverviewAnalysisData,
categoricalAnalysisData,
trendsAnalysisData,
assetTrendsData,
canShowCustomDateRange,
getTransactionCategoricalAnalysisDataItemDisplayColor,
getDisplayAmount
@@ -561,7 +643,8 @@ const transactionCategoriesStore = useTransactionCategoriesStore();
const statisticsStore = useStatisticsStore();
const snackbar = useTemplateRef<SnackBarType>('snackbar');
const monthlyTrendsChart = useTemplateRef<MonthlyTrendsChartType>('monthlyTrendsChart');
const monthlyTrendsChart = useTemplateRef<TrendsChartType>('monthlyTrendsChart');
const dailyTrendsChart = useTemplateRef<TrendsChartType>('dailyTrendsChart');
const exportDialog = useTemplateRef<ExportDialogType>('exportDialog');
const activeTab = ref<string>('statisticsPage');
@@ -582,6 +665,8 @@ const statisticsDataHasData = computed<boolean>(() => {
return !!categoricalAnalysisData.value && !!categoricalAnalysisData.value.items && categoricalAnalysisData.value.items.length > 0;
} else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) {
return !!trendsAnalysisData.value && !!trendsAnalysisData.value.items && trendsAnalysisData.value.items.length > 0 && !!monthlyTrendsChart.value;
} else if (analysisType.value === StatisticsAnalysisType.AssetTrends) {
return !!assetTrendsData.value && !!assetTrendsData.value.items && assetTrendsData.value.items.length > 0 && !!dailyTrendsChart.value;
}
return false;
@@ -592,6 +677,8 @@ const allChartTypes = computed<TypeAndDisplayName[]>(() => {
return getAllCategoricalChartTypes(true);
} else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) {
return getAllTrendChartTypes();
} else if (analysisType.value === StatisticsAnalysisType.AssetTrends) {
return getAllTrendChartTypes();
} else {
return [];
}
@@ -610,6 +697,8 @@ const queryChartType = computed<number | undefined>({
return query.value.categoricalChartType;
} else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) {
return query.value.trendChartType;
} else if (analysisType.value === StatisticsAnalysisType.AssetTrends) {
return query.value.assetTrendsChartType;
} else {
return undefined;
}
@@ -654,7 +743,7 @@ const statisticsTextColor = computed<string>(() => {
});
function getFilterLinkUrl(): string {
return `/statistics/transaction?${statisticsStore.getTransactionStatisticsPageParams(analysisType.value, trendDateAggregationType.value)}`;
return `/statistics/transaction?${statisticsStore.getTransactionStatisticsPageParams(analysisType.value, trendDateAggregationType.value, assetTrendsDateAggregationType.value)}`;
}
function getTransactionItemLinkUrl(itemId: string, dateRange?: TimeRangeAndDateType): string {
@@ -718,6 +807,29 @@ function init(initProps: TransactionStatisticsProps): void {
if (initProps.initTrendDateAggregationType) {
trendDateAggregationType.value = parseInt(initProps.initTrendDateAggregationType);
}
} else if (initProps.initAnalysisType === StatisticsAnalysisType.AssetTrends.toString()) {
filter.assetTrendsChartType = initProps.initChartType ? parseInt(initProps.initChartType) : undefined;
filter.assetTrendsChartDateType = initProps.initChartDateType ? parseInt(initProps.initChartDateType) : undefined;
filter.assetTrendsChartStartTime = initProps.initStartTime ? parseInt(initProps.initStartTime) : undefined;
filter.assetTrendsChartEndTime = initProps.initEndTime ? parseInt(initProps.initEndTime) : undefined;
if (filter.assetTrendsChartDateType !== query.value.assetTrendsChartDateType) {
needReload = true;
} else if (filter.assetTrendsChartDateType === DateRange.Custom.type) {
if (filter.assetTrendsChartStartTime !== query.value.assetTrendsChartStartTime
|| filter.assetTrendsChartEndTime !== query.value.assetTrendsChartEndTime) {
needReload = true;
}
}
if (initProps.initAnalysisType !== analysisType.value.toString()) {
analysisType.value = StatisticsAnalysisType.AssetTrends;
needReload = true;
}
if (initProps.initAssetTrendsDateAggregationType) {
assetTrendsDateAggregationType.value = parseInt(initProps.initAssetTrendsDateAggregationType);
}
}
if (!isDefined(initProps.initAnalysisType)) {
@@ -745,6 +857,10 @@ function init(initProps: TransactionStatisticsProps): void {
return statisticsStore.loadTrendAnalysis({
force: false
}) as Promise<unknown>;
} else if (analysisType.value === StatisticsAnalysisType.AssetTrends) {
return statisticsStore.loadAssetTrends({
force: false
}) as Promise<unknown>;
} else {
return Promise.reject('An error occurred');
}
@@ -780,7 +896,8 @@ function reload(force: boolean): Promise<unknown> | null {
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) {
if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) {
dispatchPromise = statisticsStore.loadCategoricalAnalysis({
force: force
@@ -789,12 +906,22 @@ function reload(force: boolean): Promise<unknown> | null {
dispatchPromise = statisticsStore.loadTrendAnalysis({
force: force
});
} else if (analysisType.value === StatisticsAnalysisType.AssetTrends) {
dispatchPromise = statisticsStore.loadAssetTrends({
force: force
});
}
} else if (query.value.chartDataType === ChartDataType.AccountTotalAssets.type ||
query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) {
dispatchPromise = accountsStore.loadAllAccounts({
force: force
});
if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) {
dispatchPromise = accountsStore.loadAllAccounts({
force: force
});
} else if (analysisType.value === StatisticsAnalysisType.AssetTrends) {
dispatchPromise = statisticsStore.loadAssetTrends({
force: force
});
}
}
if (dispatchPromise) {
@@ -822,13 +949,21 @@ function setAnalysisType(type: StatisticsAnalysisType): void {
}
if (!ChartDataType.isAvailableForAnalysisType(query.value.chartDataType, type)) {
let defaultChartDataType: ChartDataType = ChartDataType.Default;
if (type === StatisticsAnalysisType.AssetTrends) {
defaultChartDataType = ChartDataType.DefaultForAssetTrends;
}
statisticsStore.updateTransactionStatisticsFilter({
chartDataType: ChartDataType.Default.type
chartDataType: defaultChartDataType.type
});
}
if (analysisType.value !== StatisticsAnalysisType.TrendAnalysis) {
trendDateAggregationType.value = ChartDateAggregationType.Month.type;
if (analysisType.value !== StatisticsAnalysisType.TrendAnalysis && type === StatisticsAnalysisType.TrendAnalysis) {
trendDateAggregationType.value = ChartDateAggregationType.Default.type;
} else if (analysisType.value !== StatisticsAnalysisType.AssetTrends && type === StatisticsAnalysisType.AssetTrends) {
assetTrendsDateAggregationType.value = ChartDateAggregationType.Default.type;
}
analysisType.value = type;
@@ -848,6 +983,10 @@ function setChartType(type?: number): void {
changed = statisticsStore.updateTransactionStatisticsFilter({
trendChartType: type
});
} else if (analysisType.value === StatisticsAnalysisType.AssetTrends) {
changed = statisticsStore.updateTransactionStatisticsFilter({
assetTrendsChartType: type
});
}
if (changed) {
@@ -888,6 +1027,15 @@ function setTrendDateAggregationType(type: number): void {
}
}
function setAssetTrendsDateAggregationType(type: number): void {
const changed = assetTrendsDateAggregationType.value !== type;
assetTrendsDateAggregationType.value = type;
if (changed) {
router.push(getFilterLinkUrl());
}
}
function setDateFilter(dateType: number): void {
if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) {
if (dateType === DateRange.Custom.type) { // Custom
@@ -903,6 +1051,13 @@ function setDateFilter(dateType: number): void {
} else if (query.value.trendChartDateType === dateType) {
return;
}
} else if (analysisType.value === StatisticsAnalysisType.AssetTrends) {
if (dateType === DateRange.Custom.type) { // Custom
showCustomDateRangeDialog.value = true;
return;
} else if (query.value.assetTrendsChartDateType === dateType) {
return;
}
}
const dateRange = getDateRangeByDateType(dateType, firstDayOfWeek.value, fiscalYearStart.value);
@@ -925,6 +1080,12 @@ function setDateFilter(dateType: number): void {
trendChartStartYearMonth: getGregorianCalendarYearAndMonthFromUnixTime(dateRange.minTime),
trendChartEndYearMonth: getGregorianCalendarYearAndMonthFromUnixTime(dateRange.maxTime)
});
} else if (analysisType.value === StatisticsAnalysisType.AssetTrends) {
changed = statisticsStore.updateTransactionStatisticsFilter({
assetTrendsChartDateType: dateRange.dateType,
assetTrendsChartStartTime: dateRange.minTime,
assetTrendsChartEndTime: dateRange.maxTime
});
}
if (changed) {
@@ -961,6 +1122,16 @@ function setCustomDateFilter(startTime: number | TextualYearMonth, endTime: numb
});
showCustomMonthRangeDialog.value = false;
} else if (analysisType.value === StatisticsAnalysisType.AssetTrends && isNumber(startTime) && isNumber(endTime)) {
const chartDateType = getDateTypeByDateRange(startTime, endTime, firstDayOfWeek.value, fiscalYearStart.value, DateRangeScene.AssetTrends);
changed = statisticsStore.updateTransactionStatisticsFilter({
assetTrendsChartDateType: chartDateType,
assetTrendsChartStartTime: startTime,
assetTrendsChartEndTime: endTime
});
showCustomDateRangeDialog.value = false;
}
if (changed) {
@@ -993,6 +1164,18 @@ function shiftDateRange(scale: number): void {
trendChartStartYearMonth: getGregorianCalendarYearAndMonthFromUnixTime(newDateRange.minTime),
trendChartEndYearMonth: getGregorianCalendarYearAndMonthFromUnixTime(newDateRange.maxTime)
});
} else if (analysisType.value === StatisticsAnalysisType.AssetTrends) {
if (query.value.assetTrendsChartDateType === DateRange.All.type) {
return;
}
const newDateRange = getShiftedDateRangeAndDateType(query.value.assetTrendsChartStartTime, query.value.assetTrendsChartEndTime, scale, firstDayOfWeek.value, fiscalYearStart.value, DateRangeScene.AssetTrends);
changed = statisticsStore.updateTransactionStatisticsFilter({
assetTrendsChartDateType: newDateRange.dateType,
assetTrendsChartStartTime: newDateRange.minTime,
assetTrendsChartEndTime: newDateRange.maxTime
});
}
if (changed) {
@@ -1033,6 +1216,10 @@ function setTagFilter(changed: boolean): void {
}
function setKeywordFilter(keyword: string): void {
if (analysisType.value === StatisticsAnalysisType.AssetTrends) {
return;
}
if (query.value.keyword === keyword) {
return;
}
@@ -1078,6 +1265,12 @@ function exportResults(): void {
headers: exportData.headers || [],
data: exportData.data || []
});
} else if (analysisType.value === StatisticsAnalysisType.AssetTrends && assetTrendsData.value && assetTrendsData.value.items && dailyTrendsChart.value) {
const exportData = dailyTrendsChart.value.exportData();
exportDialog.value?.open({
headers: exportData.headers || [],
data: exportData.data || []
});
}
}
@@ -1125,7 +1318,8 @@ onBeforeRouteUpdate((to) => {
initTagFilterType: (to.query['tagFilterType'] as string | null) || undefined,
initKeyword: (to.query['keyword'] as string | null) || undefined,
initSortingType: (to.query['sortingType'] as string | null) || undefined,
initTrendDateAggregationType: (to.query['trendDateAggregationType'] as string | null) || undefined
initTrendDateAggregationType: (to.query['trendDateAggregationType'] as string | null) || undefined,
initAssetTrendsDateAggregationType: (to.query['assetTrendsDateAggregationType'] as string | null) || undefined
});
} else {
init({});

View File

@@ -1795,12 +1795,16 @@ init(props);
<style>
.transaction-keyword-filter .v-input--density-compact {
--v-input-control-height: 36px !important;
--v-input-control-height: 38px !important;
--v-input-padding-top: 5px !important;
--v-input-padding-bottom: 5px !important;
--v-input-chips-margin-top: 0px !important;
--v-input-chips-margin-bottom: 0px !important;
inline-size: 20rem;
.v-field__input {
min-block-size: 38px !important;
}
}
.transaction-list-datetime-range {

View File

@@ -281,14 +281,6 @@
<f7-popover class="chart-data-date-aggregation-type-popover-menu"
v-model:opened="showChartDataDateAggregationTypePopover">
<f7-list dividers>
<f7-list-item :title="tt('granularity.Daily')"
:class="{ 'list-item-selected': chartDataDateAggregationType === undefined }"
key="daily"
@click="setChartDataDateAggregationType(undefined)">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="chartDataDateAggregationType === undefined"></f7-icon>
</template>
</f7-list-item>
<f7-list-item :title="dateAggregationType.displayName"
:class="{ 'list-item-selected': chartDataDateAggregationType === dateAggregationType.type }"
:key="dateAggregationType.type"
@@ -358,6 +350,7 @@ import { TextDirection } from '@/core/text.ts';
import { type TimeRangeAndDateType, DateRange, DateRangeScene } from '@/core/datetime.ts';
import { AccountType } from '@/core/account.ts';
import { TransactionType } from '@/core/transaction.ts';
import { ChartDateAggregationType } from '@/core/statistics.ts';
import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '@/consts/transaction.ts';
import { type TransactionReconciliationStatementResponseItem } from '@/models/transaction.ts';
@@ -436,7 +429,7 @@ const loading = ref<boolean>(false);
const loadingError = ref<unknown | null>(null);
const queryDateRangeType = ref<number>(DateRange.ThisMonth.type);
const showAccountBalanceTrendsCharts = ref<boolean>(false);
const chartDataDateAggregationType = ref<number | undefined>(undefined);
const chartDataDateAggregationType = ref<number>(ChartDateAggregationType.Day.type);
const transactionToDelete = ref<TransactionReconciliationStatementResponseItem | null>(null);
const newClosingBalance = ref<number>(0);
const showDisplayModePopover = ref<boolean>(false);
@@ -489,10 +482,6 @@ const allReconciliationStatementVirtualListItems = computed<ReconciliationStatem
});
const chartDataDateAggregationTypeDisplayName = computed<string>(() => {
if (chartDataDateAggregationType.value === undefined) {
return tt('granularity.Daily');
}
return findDisplayNameByType(allDateAggregationTypes.value, chartDataDateAggregationType.value) || tt('Unknown');
});
@@ -681,7 +670,7 @@ function removeTransaction(transaction: TransactionReconciliationStatementRespon
});
}
function setChartDataDateAggregationType(type: number | undefined): void {
function setChartDataDateAggregationType(type: number): void {
chartDataDateAggregationType.value = type;
showChartDataDateAggregationTypePopover.value = false;
}

View File

@@ -128,6 +128,28 @@
</list-item-selection-popup>
</f7-list-item>
</f7-list>
<f7-block-title>{{ tt('Asset Trends Settings') }}</f7-block-title>
<f7-list strong inset dividers>
<f7-list-item
link="#"
:title="tt('Default Date Range')"
:after="findDisplayNameByType(allAssetTrendsChartDateRanges, defaultAssetTrendsChartDateRange)"
@click="showDefaultAssetTrendsChartDateRangePopup = true"
>
<list-item-selection-popup value-type="item"
key-field="type" value-field="type"
title-field="displayName"
:title="tt('Default Date Range')"
:enable-filter="true"
:filter-placeholder="tt('Date Range')"
:filter-no-items-text="tt('No results')"
:items="allAssetTrendsChartDateRanges"
v-model:show="showDefaultAssetTrendsChartDateRangePopup"
v-model="defaultAssetTrendsChartDateRange">
</list-item-selection-popup>
</f7-list-item>
</f7-list>
</f7-page>
</template>
@@ -145,12 +167,14 @@ const {
allCategoricalChartTypes,
allCategoricalChartDateRanges,
allTrendChartDateRanges,
allAssetTrendsChartDateRanges,
defaultChartDataType,
defaultTimezoneType,
defaultSortingType,
defaultCategoricalChartType,
defaultCategoricalChartDateRange,
defaultTrendChartDateRange
defaultTrendChartDateRange,
defaultAssetTrendsChartDateRange
} = useStatisticsSettingPageBase();
import { findDisplayNameByType } from '@/lib/common.ts';
@@ -161,4 +185,5 @@ const showDefaultSortingTypePopup = ref<boolean>(false);
const showDefaultCategoricalChartTypePopup = ref<boolean>(false);
const showDefaultCategoricalChartDateRangePopup = ref<boolean>(false);
const showDefaultTrendChartDateRangePopup = ref<boolean>(false);
const showDefaultAssetTrendsChartDateRangePopup = ref<boolean>(false);
</script>

View File

@@ -45,6 +45,20 @@
</template>
</f7-list-item>
</f7-list-group>
<f7-list-group>
<f7-list-item group-title>
<small>{{ tt('Asset Trends') }}</small>
</f7-list-item>
<f7-list-item :title="tt(dataType.name)"
:class="{ 'list-item-selected': analysisType === StatisticsAnalysisType.AssetTrends && query.chartDataType === dataType.type }"
:key="dataType.type"
v-for="dataType in ChartDataType.values(StatisticsAnalysisType.AssetTrends)"
@click="setChartDataType(StatisticsAnalysisType.AssetTrends, dataType.type)">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="analysisType === StatisticsAnalysisType.AssetTrends && query.chartDataType === dataType.type"></f7-icon>
</template>
</f7-list-item>
</f7-list-group>
</f7-list>
</f7-popover>
@@ -203,11 +217,15 @@
</div>
</f7-card-header>
<f7-card-content style="margin-top: -14px" :padding="false">
<monthly-trends-bar-chart
<trends-bar-chart
chart-mode="monthly"
:loading="loading || reloading"
:start-time="undefined"
:end-time="undefined"
:start-year-month="query.trendChartStartYearMonth"
:end-year-month="query.trendChartEndYearMonth"
:sorting-type="query.sortingType"
:data-aggregation-type="ChartDataAggregationType.Sum"
:date-aggregation-type="trendDateAggregationType"
:fiscal-year-start="fiscalYearStart"
:items="trendsAnalysisData && trendsAnalysisData.items && trendsAnalysisData.items.length ? trendsAnalysisData.items : []"
@@ -224,6 +242,42 @@
</f7-card-content>
</f7-card>
<f7-card v-else-if="analysisType === StatisticsAnalysisType.AssetTrends">
<f7-card-header class="no-border display-block">
<div class="statistics-chart-header display-flex full-line justify-content-space-between">
<div></div>
<div class="align-self-flex-end">
<span style="margin-inline-end: 4px;">{{ tt('Sort by') }}</span>
<f7-link href="#" popover-open=".sorting-type-popover-menu">{{ querySortingTypeName }}</f7-link>
</div>
</div>
</f7-card-header>
<f7-card-content style="margin-top: -14px" :padding="false">
<trends-bar-chart
chart-mode="daily"
:loading="loading || reloading"
:start-time="query.assetTrendsChartStartTime"
:end-time="query.assetTrendsChartEndTime"
:start-year-month="undefined"
:end-year-month="undefined"
:sorting-type="query.sortingType"
:data-aggregation-type="ChartDataAggregationType.Last"
:date-aggregation-type="assetTrendsDateAggregationType"
:fiscal-year-start="fiscalYearStart"
:items="assetTrendsData && assetTrendsData.items && assetTrendsData.items.length ? assetTrendsData.items : []"
:stacked="showStackedInTrendsChart"
:translate-name="translateNameInTrendsChart"
:default-currency="defaultCurrency"
id-field="id"
name-field="name"
value-field="totalAmount"
hidden-field="hidden"
display-orders-field="displayOrders"
@click="onClickTrendChartItem"
/>
</f7-card-content>
</f7-card>
<f7-popover class="sorting-type-popover-menu"
v-model:opened="showSortingTypePopover">
<f7-list dividers>
@@ -243,7 +297,7 @@
<f7-link :class="{ 'disabled': reloading || !canShiftDateRange }" @click="shiftDateRange(-1)">
<f7-icon class="icon-with-direction" f7="arrow_left_square"></f7-icon>
</f7-link>
<f7-link :class="{ 'tabbar-text-with-ellipsis': true, 'disabled': reloading || query.chartDataType === ChartDataType.AccountTotalAssets.type || query.chartDataType === ChartDataType.AccountTotalLiabilities.type }" popover-open=".date-popover-menu">
<f7-link :class="{ 'tabbar-text-with-ellipsis': true, 'disabled': reloading || !canChangeDateRange }" popover-open=".date-popover-menu">
<span :class="{ 'tabbar-item-changed': isQueryDateRangeChanged }">{{ queryDateRangeName }}</span>
</f7-link>
<f7-link :class="{ 'disabled': reloading || !canShiftDateRange }" @click="shiftDateRange(1)">
@@ -253,6 +307,10 @@
v-if="analysisType === StatisticsAnalysisType.TrendAnalysis">
<span :class="{ 'tabbar-item-changed': trendDateAggregationType !== ChartDateAggregationType.Default.type }">{{ queryTrendDateAggregationTypeName }}</span>
</f7-link>
<f7-link :class="{ 'tabbar-text-with-ellipsis': true, 'disabled': reloading }" popover-open=".date-aggregation-popover-menu"
v-if="analysisType === StatisticsAnalysisType.AssetTrends">
<span :class="{ 'tabbar-item-changed': assetTrendsDateAggregationType !== ChartDateAggregationType.Default.type }">{{ queryAssetTrendsDateAggregationTypeName }}</span>
</f7-link>
<f7-link class="tabbar-text-with-ellipsis" :key="chartType.type"
v-for="chartType in allChartTypes" @click="setChartType(chartType.type)">
<span :class="{ 'tabbar-item-changed': queryChartType === chartType.type }">{{ chartType.displayName }}</span>
@@ -286,17 +344,28 @@
<f7-popover class="date-aggregation-popover-menu"
v-model:opened="showDateAggregationPopover"
@popover:open="scrollPopoverToSelectedItem">
<f7-list dividers>
<f7-list dividers v-if="analysisType === StatisticsAnalysisType.TrendAnalysis">
<f7-list-item :title="aggregationType.displayName"
:class="{ 'list-item-selected': trendDateAggregationType === aggregationType.type }"
:key="aggregationType.type"
v-for="aggregationType in allDateAggregationTypes"
v-for="aggregationType in allTrendAnalysisDateAggregationTypes"
@click="setTrendDateAggregationType(aggregationType.type)">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="trendDateAggregationType === aggregationType.type"></f7-icon>
</template>
</f7-list-item>
</f7-list>
<f7-list dividers v-else-if="analysisType === StatisticsAnalysisType.AssetTrends">
<f7-list-item :title="aggregationType.displayName"
:class="{ 'list-item-selected': assetTrendsDateAggregationType === aggregationType.type }"
:key="aggregationType.type"
v-for="aggregationType in allAssetTrendsDateAggregationTypes"
@click="setAssetTrendsDateAggregationType(aggregationType.type)">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="assetTrendsDateAggregationType === aggregationType.type"></f7-icon>
</template>
</f7-list-item>
</f7-list>
</f7-popover>
<date-range-selection-sheet :title="tt('Custom Date Range')"
@@ -348,6 +417,7 @@ import type { TypeAndDisplayName } from '@/core/base.ts';
import { TextDirection } from '@/core/text.ts';
import { type TextualYearMonth, type TimeRangeAndDateType, DateRangeScene, DateRange } from '@/core/datetime.ts';
import {
ChartDataAggregationType,
StatisticsAnalysisType,
CategoricalChartType,
ChartDataType,
@@ -383,12 +453,14 @@ const {
loading,
analysisType,
trendDateAggregationType,
assetTrendsDateAggregationType,
defaultCurrency,
firstDayOfWeek,
fiscalYearStart,
allDateRanges,
allSortingTypes,
allDateAggregationTypes,
allTrendAnalysisDateAggregationTypes,
allAssetTrendsDateAggregationTypes,
query,
queryChartDataCategory,
queryDateType,
@@ -398,7 +470,9 @@ const {
queryChartDataTypeName,
querySortingTypeName,
queryTrendDateAggregationTypeName,
queryAssetTrendsDateAggregationTypeName,
isQueryDateRangeChanged,
canChangeDateRange,
canShiftDateRange,
canUseCategoryFilter,
canUseTagFilter,
@@ -410,6 +484,7 @@ const {
translateNameInTrendsChart,
categoricalAnalysisData,
trendsAnalysisData,
assetTrendsData,
canShowCustomDateRange,
getTransactionCategoricalAnalysisDataItemDisplayColor,
getDisplayAmount
@@ -445,6 +520,8 @@ const queryChartType = computed<number | undefined>({
return query.value.categoricalChartType;
} else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) {
return query.value.trendChartType;
} else if (analysisType.value === StatisticsAnalysisType.AssetTrends) {
return query.value.assetTrendsChartType;
} else {
return undefined;
}
@@ -473,6 +550,10 @@ function init(): void {
return statisticsStore.loadTrendAnalysis({
force: false
}) as Promise<unknown>;
} else if (analysisType.value === StatisticsAnalysisType.AssetTrends) {
return statisticsStore.loadAssetTrends({
force: false
}) as Promise<unknown>;
} else {
return Promise.reject('An error occurred');
}
@@ -507,7 +588,8 @@ function reload(done?: () => void): void {
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) {
if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) {
dispatchPromise = statisticsStore.loadCategoricalAnalysis({
force: force
@@ -516,12 +598,22 @@ function reload(done?: () => void): void {
dispatchPromise = statisticsStore.loadTrendAnalysis({
force: force
});
} else if (analysisType.value === StatisticsAnalysisType.AssetTrends) {
dispatchPromise = statisticsStore.loadAssetTrends({
force: force
});
}
} else if (query.value.chartDataType === ChartDataType.AccountTotalAssets.type ||
query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) {
dispatchPromise = accountsStore.loadAllAccounts({
force: force
});
if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) {
dispatchPromise = accountsStore.loadAllAccounts({
force: force
});
} else if (analysisType.value === StatisticsAnalysisType.AssetTrends) {
dispatchPromise = statisticsStore.loadAssetTrends({
force: force
});
}
}
if (dispatchPromise) {
@@ -556,6 +648,10 @@ function setChartType(type?: number): void {
statisticsStore.updateTransactionStatisticsFilter({
trendChartType: type
});
} else if (analysisType.value === StatisticsAnalysisType.AssetTrends) {
statisticsStore.updateTransactionStatisticsFilter({
assetTrendsChartType: type
});
}
}
@@ -603,6 +699,11 @@ function setTrendDateAggregationType(type: number): void {
showDateAggregationPopover.value = false;
}
function setAssetTrendsDateAggregationType(type: number): void {
assetTrendsDateAggregationType.value = type;
showDateAggregationPopover.value = false;
}
function setDateFilter(dateType: number): void {
if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) {
if (dateType === DateRange.Custom.type) { // Custom
@@ -620,6 +721,14 @@ function setDateFilter(dateType: number): void {
} else if (query.value.trendChartDateType === dateType) {
return;
}
} else if (analysisType.value === StatisticsAnalysisType.AssetTrends) {
if (dateType === DateRange.Custom.type) { // Custom
showCustomDateRangeSheet.value = true;
showDatePopover.value = false;
return;
} else if (query.value.assetTrendsChartDateType === dateType) {
return;
}
}
const dateRange = getDateRangeByDateType(dateType, firstDayOfWeek.value, fiscalYearStart.value);
@@ -642,6 +751,12 @@ function setDateFilter(dateType: number): void {
trendChartStartYearMonth: getGregorianCalendarYearAndMonthFromUnixTime(dateRange.minTime),
trendChartEndYearMonth: getGregorianCalendarYearAndMonthFromUnixTime(dateRange.maxTime)
});
} else if (analysisType.value === StatisticsAnalysisType.AssetTrends) {
changed = statisticsStore.updateTransactionStatisticsFilter({
assetTrendsChartDateType: dateRange.dateType,
assetTrendsChartStartTime: dateRange.minTime,
assetTrendsChartEndTime: dateRange.maxTime
});
}
showDatePopover.value = false;
@@ -678,6 +793,16 @@ function setCustomDateFilter(startTime: number | TextualYearMonth, endTime: numb
});
showCustomMonthRangeSheet.value = false;
} else if (analysisType.value === StatisticsAnalysisType.AssetTrends && isNumber(startTime) && isNumber(endTime)) {
const chartDateType = getDateTypeByDateRange(startTime, endTime, firstDayOfWeek.value, fiscalYearStart.value, DateRangeScene.AssetTrends);
changed = statisticsStore.updateTransactionStatisticsFilter({
assetTrendsChartDateType: chartDateType,
assetTrendsChartStartTime: startTime,
assetTrendsChartEndTime: endTime
});
showCustomDateRangeSheet.value = false;
}
if (changed) {
@@ -708,6 +833,18 @@ function shiftDateRange(scale: number): void {
trendChartStartYearMonth: getGregorianCalendarYearAndMonthFromUnixTime(newDateRange.minTime),
trendChartEndYearMonth: getGregorianCalendarYearAndMonthFromUnixTime(newDateRange.maxTime)
});
} else if (analysisType.value === StatisticsAnalysisType.AssetTrends) {
if (query.value.assetTrendsChartDateType === DateRange.All.type) {
return;
}
const newDateRange = getShiftedDateRangeAndDateType(query.value.assetTrendsChartStartTime, query.value.assetTrendsChartEndTime, scale, firstDayOfWeek.value, fiscalYearStart.value, DateRangeScene.AssetTrends);
changed = statisticsStore.updateTransactionStatisticsFilter({
assetTrendsChartDateType: newDateRange.dateType,
assetTrendsChartStartTime: newDateRange.minTime,
assetTrendsChartEndTime: newDateRange.maxTime
});
}
if (changed) {
@@ -728,6 +865,10 @@ function filterTags(): void {
}
function filterDescription(): void {
if (analysisType.value === StatisticsAnalysisType.AssetTrends) {
return;
}
showPrompt('Filter transaction description', query.value.keyword, value => {
if (query.value.keyword === value) {
return;