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
@@ -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;
}
+26 -1
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>
+150 -9
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;