diff --git a/src/core/statistics.ts b/src/core/statistics.ts index c6a4a57b..43511e0b 100644 --- a/src/core/statistics.ts +++ b/src/core/statistics.ts @@ -119,7 +119,7 @@ export class ChartDataType implements TypeAndName { return this.availableAnalysisTypes[analysisType] || false; } - public static values(analysisType: StatisticsAnalysisType | undefined): ChartDataType[] { + public static values(analysisType?: StatisticsAnalysisType): ChartDataType[] { if (analysisType === undefined) { return ChartDataType.allInstances; } diff --git a/src/lib/common.ts b/src/lib/common.ts index a7ab849b..676f2f54 100644 --- a/src/lib/common.ts +++ b/src/lib/common.ts @@ -1,4 +1,4 @@ -import type { TypeAndDisplayName } from '@/core/base.ts'; +import type { TypeAndName, TypeAndDisplayName } from '@/core/base.ts'; // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type export function isFunction(val: unknown): val is Function { @@ -37,7 +37,7 @@ export function isBoolean(val: unknown): val is boolean { return typeof(val) === 'boolean'; } -export function isYearMonth(val: unknown): boolean { +export function isYearMonth(val: unknown): val is string { if (!isString(val)) { return false; } @@ -312,6 +312,16 @@ export function getItemByKeyValue(src: Record[] | Record formatUnixTime(unixTime, getLocalizedLongTimeFormat(), utcOffset, currentUtcOffset), formatUnixTimeToShortTime: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedShortTimeFormat(), utcOffset, currentUtcOffset), formatYearQuarter, + formatDateRange, getTimezoneDifferenceDisplayText, appendDigitGroupingSymbol: getNumberWithDigitGroupingSymbol, parseAmount: getParsedAmountNumber, diff --git a/src/stores/statistics.ts b/src/stores/statistics.ts index b8befaa2..7deef649 100644 --- a/src/stores/statistics.ts +++ b/src/stores/statistics.ts @@ -101,7 +101,24 @@ interface WritableTransactioTrendsAnalysisDataItem { items: TransactionTrendsAnalysisDataAmount[]; } -export interface TransactionStatisticsFilter { +export interface TransactionStatisticsPartialFilter { + chartDataType?: number; + categoricalChartType?: number; + categoricalChartDateType?: number; + categoricalChartStartTime?: number; + categoricalChartEndTime?: number; + trendChartType?: number; + trendChartDateType?: number; + trendChartStartYearMonth?: string; + trendChartEndYearMonth?: string; + filterAccountIds?: Record; + filterCategoryIds?: Record; + tagIds?: string; + tagFilterType?: number; + sortingType?: number; +} + +export interface TransactionStatisticsFilter extends TransactionStatisticsPartialFilter { chartDataType: number; categoricalChartType: number; categoricalChartDateType: number; @@ -672,7 +689,7 @@ export const useStatisticsStore = defineStore('statistics', () => { transactionStatisticsStateInvalid.value = true; } - function initTransactionStatisticsFilter(analysisType: StatisticsAnalysisType, filter?: TransactionStatisticsFilter): void { + function initTransactionStatisticsFilter(analysisType: StatisticsAnalysisType, filter?: TransactionStatisticsPartialFilter): void { if (filter && isInteger(filter.chartDataType)) { transactionStatisticsFilter.value.chartDataType = filter.chartDataType; } else { @@ -810,7 +827,7 @@ export const useStatisticsStore = defineStore('statistics', () => { } } - function updateTransactionStatisticsFilter(filter: TransactionStatisticsFilter): boolean { + function updateTransactionStatisticsFilter(filter: TransactionStatisticsPartialFilter): boolean { let changed = false; if (filter && isInteger(filter.chartDataType) && transactionStatisticsFilter.value.chartDataType !== filter.chartDataType) { @@ -943,7 +960,7 @@ export const useStatisticsStore = defineStore('statistics', () => { return querys.join('&'); } - function getTransactionListPageParams(analysisType: StatisticsAnalysisType, itemId: string, dateRange: TimeRangeAndDateType): string { + function getTransactionListPageParams(analysisType: StatisticsAnalysisType, itemId: string, dateRange?: TimeRangeAndDateType): string { const querys: string[] = []; if (transactionStatisticsFilter.value.chartDataType === ChartDataType.IncomeByAccount.type diff --git a/src/views/base/statistics/StatisticsTransactionPageBase.ts b/src/views/base/statistics/StatisticsTransactionPageBase.ts new file mode 100644 index 00000000..ad7e6c56 --- /dev/null +++ b/src/views/base/statistics/StatisticsTransactionPageBase.ts @@ -0,0 +1,243 @@ +import { ref, computed } from 'vue'; + +import { useI18n } from '@/locales/helpers.ts'; + +import { useSettingsStore } from '@/stores/setting.ts'; +import { useUserStore } from '@/stores/user.ts'; +import { type TransactionStatisticsFilter, useStatisticsStore } from '@/stores/statistics.ts'; + +import type { TypeAndDisplayName } from '@/core/base.ts'; +import { type LocalizedDateRange, DateRangeScene, DateRange } from '@/core/datetime.ts'; +import { StatisticsAnalysisType, ChartDataType, ChartSortingType, ChartDateAggregationType } from '@/core/statistics.ts'; +import type { TransactionCategoricalAnalysisData, TransactionTrendsAnalysisData } from '@/models/transaction.ts'; + +import { limitText, findNameByType, findDisplayNameByType } from '@/lib/common.ts'; +import { getYearMonthFirstUnixTime, getYearMonthLastUnixTime } from '@/lib/datetime.ts'; + +export function useStatisticsTransactionPageBase() { + const { + tt, + getAllDateRanges, + getAllStatisticsSortingTypes, + getAllStatisticsDateAggregationTypes, + formatUnixTimeToLongDateTime, + formatUnixTimeToLongYearMonth, + formatDateRange, + formatAmountWithCurrency + } = useI18n(); + + const settingsStore = useSettingsStore(); + const userStore = useUserStore(); + const statisticsStore = useStatisticsStore(); + + const loading = ref(true); + const analysisType = ref(StatisticsAnalysisType.CategoricalAnalysis); + const trendDateAggregationType = ref(ChartDateAggregationType.Default.type); + + const showAccountBalance = computed(() => settingsStore.appSettings.showAccountBalance); + const defaultCurrency = computed(() => userStore.currentUserDefaultCurrency); + const firstDayOfWeek = computed(() => userStore.currentUserFirstDayOfWeek); + + const allDateRanges = computed(() => { + if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) { + return getAllDateRanges(DateRangeScene.Normal, true); + } else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) { + return getAllDateRanges(DateRangeScene.TrendAnalysis, true); + } else { + return []; + } + }); + const allSortingTypes = computed(() => getAllStatisticsSortingTypes()); + const allDateAggregationTypes = computed(() => getAllStatisticsDateAggregationTypes()); + + const query = computed(() => statisticsStore.transactionStatisticsFilter); + const queryChartDataCategory = computed(() => statisticsStore.categoricalAnalysisChartDataCategory); + const queryDateType = computed(() => { + if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) { + return query.value.categoricalChartDateType; + } else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) { + return query.value.trendChartDateType; + } else { + return null; + } + }); + + const queryStartTime = computed(() => { + if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) { + return formatUnixTimeToLongDateTime(query.value.categoricalChartStartTime); + } else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) { + return formatUnixTimeToLongYearMonth(getYearMonthFirstUnixTime(query.value.trendChartStartYearMonth)); + } else { + return ''; + } + }); + + const queryEndTime = computed(() => { + if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) { + return formatUnixTimeToLongDateTime(query.value.categoricalChartEndTime); + } else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) { + return formatUnixTimeToLongYearMonth(getYearMonthLastUnixTime(query.value.trendChartEndYearMonth)); + } else { + return ''; + } + }); + + const queryDateRangeName = computed(() => { + if (query.value.chartDataType === ChartDataType.AccountTotalAssets.type || + query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) { + return tt(DateRange.All.name); + } + + if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) { + 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 { + return ''; + } + }); + + const queryChartDataTypeName = computed(() => { + const queryChartDataTypeName = findNameByType(ChartDataType.values(), query.value.chartDataType) || 'Statistics'; + return tt(queryChartDataTypeName); + }); + + const querySortingTypeName = computed(() => { + const querySortingTypeName = findNameByType(ChartSortingType.values(), query.value.sortingType) || 'System Default'; + return tt(querySortingTypeName); + }); + + const queryTrendDateAggregationTypeName = computed(() => findDisplayNameByType(allDateAggregationTypes.value, trendDateAggregationType.value) || ''); + + const isQueryDateRangeChanged = computed(() => { + if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) { + if (query.value.chartDataType === ChartDataType.AccountTotalAssets.type || + query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) { + return false; + } + + return !!query.value.categoricalChartStartTime || !!query.value.categoricalChartEndTime; + } else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) { + return !!query.value.trendChartStartYearMonth || !!query.value.trendChartEndYearMonth; + } else { + return false; + } + }); + + const canShiftDateRange = computed(() => { + if (query.value.chartDataType === ChartDataType.AccountTotalAssets.type || query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) { + return false; + } + + if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) { + return query.value.categoricalChartDateType !== DateRange.All.type; + } else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) { + return query.value.trendChartDateType !== DateRange.All.type; + } else { + return false; + } + }); + + const showCustomDateRange = computed(() => { + if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) { + return query.value.categoricalChartDateType === DateRange.Custom.type && !!query.value.categoricalChartStartTime && !!query.value.categoricalChartEndTime; + } else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) { + return query.value.trendChartDateType === DateRange.Custom.type && !!query.value.trendChartStartYearMonth && !!query.value.trendChartEndYearMonth; + } else { + return false; + } + }); + + const showAmountInChart = computed(() => { + if (!showAccountBalance.value + && (query.value.chartDataType === ChartDataType.AccountTotalAssets.type || query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type)) { + return false; + } + + return true; + }); + + const totalAmountName = computed(() => { + if (query.value.chartDataType === ChartDataType.IncomeByAccount.type + || query.value.chartDataType === ChartDataType.IncomeByPrimaryCategory.type + || query.value.chartDataType === ChartDataType.IncomeBySecondaryCategory.type) { + return tt('Total Income'); + } else if (query.value.chartDataType === ChartDataType.ExpenseByAccount.type + || query.value.chartDataType === ChartDataType.ExpenseByPrimaryCategory.type + || query.value.chartDataType === ChartDataType.ExpenseBySecondaryCategory.type) { + return tt('Total Expense'); + } else if (query.value.chartDataType === ChartDataType.AccountTotalAssets.type) { + return tt('Total Assets'); + } else if (query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) { + return tt('Total Liabilities'); + } + + return tt('Total Amount'); + }); + + const showTotalAmountInTrendsChart = computed(() => { + return query.value.chartDataType !== ChartDataType.TotalExpense.type && + query.value.chartDataType !== ChartDataType.TotalIncome.type && + query.value.chartDataType !== ChartDataType.TotalBalance.type; + }); + + const translateNameInTrendsChart = computed(() => { + return query.value.chartDataType === ChartDataType.TotalExpense.type || + query.value.chartDataType === ChartDataType.TotalIncome.type || + query.value.chartDataType === ChartDataType.TotalBalance.type; + }); + + const categoricalAnalysisData = computed(() => statisticsStore.categoricalAnalysisData); + const trendsAnalysisData = computed(() => statisticsStore.trendsAnalysisData); + + function getDisplayAmount(amount: number, currency: string, textLimit?: number): string { + const finalAmount = formatAmountWithCurrency(amount, currency) as string; + + if (!showAccountBalance.value + && (query.value.chartDataType === ChartDataType.AccountTotalAssets.type + || query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) + ) { + return '***'; + } + + if (textLimit) { + return limitText(finalAmount, textLimit); + } + + return finalAmount; + } + + return { + // states + loading, + analysisType, + trendDateAggregationType, + // computed states + showAccountBalance, + defaultCurrency, + firstDayOfWeek, + allDateRanges, + allSortingTypes, + allDateAggregationTypes, + query, + queryChartDataCategory, + queryDateType, + queryStartTime, + queryEndTime, + queryDateRangeName, + queryChartDataTypeName, + querySortingTypeName, + queryTrendDateAggregationTypeName, + isQueryDateRangeChanged, + canShiftDateRange, + showCustomDateRange, + showAmountInChart, + totalAmountName, + showTotalAmountInTrendsChart, + translateNameInTrendsChart, + categoricalAnalysisData, + trendsAnalysisData, + // functions + getDisplayAmount + }; +} diff --git a/src/views/desktop/statistics/TransactionPage.vue b/src/views/desktop/statistics/TransactionPage.vue index 0074d005..ea4b8f29 100644 --- a/src/views/desktop/statistics/TransactionPage.vue +++ b/src/views/desktop/statistics/TransactionPage.vue @@ -6,13 +6,13 @@
- {{ $t('Chart Type') }} + {{ tt('Chart Type') }}
- {{ $t('Sort Order') }} + {{ tt('Sort Order') }} - {{ $t(dataType.name) }} - {{ $t(dataType.name) }} + v-for="dataType in ChartDataType.all()" v-show="dataType.isAvailableAnalysisType(queryAnalysisType)"> + {{ tt(dataType.name) }} + {{ tt(dataType.name) }} @@ -54,25 +54,25 @@ :ripple="false" :icon="true" @click="showNav = !showNav"> - {{ $t('Statistics & Analysis') }} + {{ tt('Statistics & Analysis') }} + :append-icon="(queryDateType === dateRange.type ? icons.check : undefined)" + v-for="dateRange in allDateRanges"> {{ dateRange.displayName }} -
+
{{ queryStartTime }}  - 
@@ -83,11 +83,11 @@ - + + v-if="queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && (initing || (categoricalAnalysisData && categoricalAnalysisData.items && categoricalAnalysisData.items.length))"> {{ totalAmountName }} - {{ $t('No transaction data') }} + v-else-if="!initing && ((queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && (!categoricalAnalysisData || !categoricalAnalysisData.items || !categoricalAnalysisData.items.length)) + || (queryAnalysisType === StatisticsAnalysisType.TrendAnalysis && (!trendsAnalysisData || !trendsAnalysisData.items || !trendsAnalysisData.items.length)))"> + {{ tt('No transaction data') }} - + - + -